net.morimekta.gittool.cmd.Branch.java Source code

Java tutorial

Introduction

Here is the source code for net.morimekta.gittool.cmd.Branch.java

Source

/*
 * Copyright 2017 (c) Stein Eldar Johnsen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.morimekta.gittool.cmd;

import net.morimekta.console.InputLine;
import net.morimekta.console.InputSelection;
import net.morimekta.console.LinePrinter;
import net.morimekta.console.Terminal;
import net.morimekta.console.args.ArgumentParser;
import net.morimekta.console.chr.Char;
import net.morimekta.console.chr.CharUtil;
import net.morimekta.console.chr.Color;
import net.morimekta.console.util.STTY;
import net.morimekta.gittool.GitTool;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static net.morimekta.console.chr.Color.BOLD;
import static net.morimekta.console.chr.Color.CLEAR;
import static net.morimekta.console.chr.Color.DIM;
import static net.morimekta.console.chr.Color.GREEN;
import static net.morimekta.console.chr.Color.RED;
import static net.morimekta.console.chr.Color.YELLOW;

/**
 * Interactively manage branches.
 */
public class Branch extends Command {
    private int longestBranchName = 0;
    private int longestRemoteName = 0;

    private enum BranchAction {
        CHECKOUT, DELETE, RENAME, SET_DIFFBASE,
    }

    private class BranchInfo {
        Ref ref;

        boolean current = false;
        boolean uncommitted = false;

        String name = null;
        String diffbase = null;
        String remote = null;

        int commits = 0;
        int missing = 0;

        private void clr(StringBuilder builder, Color baseColor) {
            builder.append(CLEAR);
            if (baseColor != null) {
                builder.append(baseColor);
            }
        }

        String branchLine(Color baseColor) {
            StringBuilder builder = new StringBuilder();
            if (baseColor != null) {
                builder.append(baseColor);
            }
            if (current) {
                builder.append("* ").append(GREEN);
            } else {
                builder.append("  ").append(YELLOW);
            }
            builder.append(Strings.padEnd(name, longestBranchName, ' '));
            clr(builder, baseColor);
            if (diffbase.equals(name)) {
                builder.append("    ").append(Strings.padEnd("", longestRemoteName, ' '));
            } else if (remote != null) {
                builder.append(" <- ").append(DIM).append(Strings.padEnd(remote, longestRemoteName, ' '));
                clr(builder, baseColor);
            } else {
                builder.append(" d: ").append(new Color(YELLOW, DIM))
                        .append(Strings.padEnd(diffbase, longestRemoteName, ' '));
                clr(builder, baseColor);
            }

            if (commits > 0 || missing > 0) {
                builder.append(" [");
                if (commits == 0) {
                    builder.append(CLR_SUBS).append("-").append(missing);
                    clr(builder, baseColor);
                } else if (missing == 0) {
                    builder.append(CLR_ADDS).append("+").append(commits);
                    clr(builder, baseColor);
                } else {
                    builder.append(CLR_ADDS).append("+").append(commits);
                    clr(builder, baseColor);
                    builder.append(",").append(CLR_SUBS).append("-").append(missing);
                    clr(builder, baseColor);
                }

                if (remote != null && !diffbase.equals(remote)) {
                    builder.append(",").append(BOLD).append("%%");
                    clr(builder, baseColor);
                }

                builder.append("]");
            }

            if (uncommitted) {
                builder.append(" -- ").append(new Color(BOLD, RED)).append("MOD");
                clr(builder, baseColor);
                builder.append(" --");
            }

            return builder.toString();
        }

        String selectionLine(Color baseColor) {
            StringBuilder builder = new StringBuilder();

            builder.append(Color.DIM);
            builder.append(name);
            builder.append(CLEAR);
            if (baseColor != null) {
                builder.append(baseColor);
            }
            return builder.toString();
        }
    }

    private List<BranchInfo> branches = new LinkedList<>();

    public Branch(ArgumentParser parent) {
        super(parent);
    }

    @Override
    public ArgumentParser makeParser() {
        return new ArgumentParser(getParent().getProgram() + " branch", getParent().getVersion(),
                "Manage branches interactively.");
    }

    private String branchName(Ref ref) {
        if (ref.getName().startsWith("refs/heads/")) {
            return ref.getName().substring(11);
        }
        return ref.getName();
    }

    private boolean hasUncommitted() throws GitAPIException {
        return git.diff().setShowNameAndStatusOnly(true).setCached(true).call().size() > 0
                || git.diff().setShowNameAndStatusOnly(true).setCached(false).call().size() > 0;
    }

    private InputSelection.Reaction onSelect(BranchInfo info, LinePrinter printer) {
        if (info.current) {
            printer.println("Already on branch " + new Color(YELLOW, DIM) + info.name + CLEAR + "...");
            return InputSelection.Reaction.EXIT;
        } else if (currentInfo.uncommitted) {
            printer.warn("Current branch has uncommitted changes");
            return InputSelection.Reaction.STAY;
        } else {
            action = BranchAction.CHECKOUT;
            return InputSelection.Reaction.SELECT;
        }
    }

    private InputSelection.Reaction onDelete(BranchInfo info, LinePrinter printer) {
        if (info.current) {
            printer.warn("Unable to delete current branch");
            return InputSelection.Reaction.STAY;
        }
        action = BranchAction.DELETE;
        return InputSelection.Reaction.SELECT;
    }

    private InputSelection.Reaction onSetDiffbase(BranchInfo ignore1, LinePrinter ignore2) {
        action = BranchAction.SET_DIFFBASE;
        return InputSelection.Reaction.SELECT;
    }

    private InputSelection.Reaction onRename(BranchInfo branch, LinePrinter printer) {
        if (branch.name.equals(gt.getDefaultBranch())) {
            printer.warn("Not allowed to rename default branch");
            return InputSelection.Reaction.STAY;
        }
        action = BranchAction.RENAME;
        return InputSelection.Reaction.SELECT;
    }

    private InputSelection.Reaction onExit(BranchInfo ignore1, LinePrinter ignore2) {
        return InputSelection.Reaction.EXIT;
    }

    private BranchInfo currentInfo = null;
    private Repository repository = null;
    private BranchAction action = null;
    private Git git = null;
    private GitTool gt = null;
    private String prompt = null;

    private BranchInfo refreshBranchList(String selected) throws IOException, GitAPIException {
        branches.clear();

        ListBranchCommand bl = git.branchList();
        List<Ref> refs = bl.call();

        String current = repository.getFullBranch();
        BranchInfo selectedInfo = null;

        prompt = "Manage branches from <untracked>:";
        for (Ref ref : refs) {
            BranchInfo info = new BranchInfo();
            info.ref = ref;

            info.name = branchName(ref);
            if (ref.getName().equals(current)) {
                info.current = true;
                info.uncommitted = hasUncommitted();
                currentInfo = info;
            }
            if (selected == null) {
                if (ref.getName().equals(current)) {
                    selectedInfo = info;
                }
            } else {
                if (info.name.equals(selected)) {
                    selectedInfo = info;
                }
            }

            info.diffbase = gt.getDiffbase(info.name);
            info.remote = gt.getRemote(info.name);

            Ref currentRef = repository.getRef(gt.refName(info.name));
            Ref diffWithRef = repository.getRef(gt.refName(info.diffbase));

            ObjectId currentHead = currentRef.getObjectId();
            ObjectId diffWithHead = diffWithRef.getObjectId();

            List<RevCommit> localCommits = ImmutableList
                    .copyOf(git.log().addRange(diffWithHead, currentHead).call());
            List<RevCommit> remoteCommits = ImmutableList
                    .copyOf(git.log().addRange(currentHead, diffWithHead).call());

            info.commits = localCommits.size();
            info.missing = remoteCommits.size();

            longestBranchName = Math.max(longestBranchName, CharUtil.printableWidth(info.name));
            longestRemoteName = Math.max(longestRemoteName, CharUtil.printableWidth(info.diffbase));

            branches.add(info);
        }

        Comparator<BranchInfo> comparator = (l, r) -> {
            if (l.name.equals(gt.getDefaultBranch())) {
                return -1;
            }
            if (r.name.equals(gt.getDefaultBranch())) {
                return 1;
            }
            return l.name.compareTo(r.name);
        };
        branches.sort(comparator);
        return selectedInfo == null ? currentInfo : selectedInfo;
    }

    @Override
    public void execute(GitTool gt) throws IOException {
        this.gt = gt;

        ArrayList<InputSelection.Command<BranchInfo>> actions = new ArrayList<>(5);
        actions.add(new InputSelection.Command<>(Char.CR, "select", this::onSelect, true));
        actions.add(new InputSelection.Command<>('D', "delete", this::onDelete));
        actions.add(new InputSelection.Command<>('q', "quit", this::onExit));
        actions.add(new InputSelection.Command<>('d', "set diffbase", this::onSetDiffbase));
        actions.add(new InputSelection.Command<>('m', "move", this::onRename));

        STTY tty = new STTY();
        try (Terminal terminal = new Terminal(tty)) {
            try (Repository repositoryResource = gt.getRepository()) {
                this.repository = repositoryResource;
                this.git = new Git(repositoryResource);
                BranchInfo tmp = refreshBranchList(null);
                prompt = "Manage branches from '" + currentInfo.name + "':";

                while (true) {
                    action = null;
                    try {
                        InputSelection<BranchInfo> selection = new InputSelection<>(terminal, prompt, branches,
                                actions, BranchInfo::branchLine);
                        tmp = selection.select(tmp);
                    } catch (UncheckedIOException e) {
                        // Most likely: User interrupted:
                        // <ESC>, <CTRL-C> etc.
                        System.out.println(e.getMessage());
                        return;
                    }
                    if (tmp == null) {
                        // command action EXIT.
                        return;
                    }
                    if (action == null) {
                        continue;
                    }
                    final BranchInfo selected = tmp;

                    switch (action) {
                    case CHECKOUT: {
                        Ref ref = git.checkout().setName(selected.name).call();
                        if (ref == null) {
                            terminal.error("No ref from checkout op...");
                            break;
                        }
                        return;
                    }
                    case DELETE: {
                        if (selected.commits == 0 || terminal
                                .confirm("Do you really want to delete branch " + YELLOW + selected.name + CLEAR
                                        + " with " + GREEN + "+" + selected.commits + CLEAR + " commits?")) {
                            git.branchDelete().setBranchNames(selected.name).call();
                            terminal.info("Deleted branch " + RED + selected.name + CLEAR + "!");
                            tmp = currentInfo;
                        } else {
                            terminal.info("Delete canceled.");
                            return;
                        }
                        tmp = refreshBranchList(tmp.name);
                        terminal.println();
                        break;
                    }
                    case RENAME: {
                        String name;
                        try {
                            InputLine input = new InputLine(terminal,
                                    "New name for " + YELLOW + selected.name + CLEAR);
                            name = input.readLine(selected.name);
                        } catch (UncheckedIOException e) {
                            // Most likely user interruption.
                            terminal.info(e.getMessage());
                            terminal.println();
                            break;
                        }
                        if (selected.name.equals(name)) {
                            terminal.info("Same same same...");
                            terminal.println();
                            break;
                        }

                        Ref ref = git.branchRename().setOldName(selected.name).setNewName(name).call();
                        if (ref == null) {
                            terminal.error("No ref from branch rename operation...");
                            return;
                        }
                        terminal.println();
                        tmp = refreshBranchList(selected.name);
                        break;
                    }
                    case SET_DIFFBASE: {
                        if (selected.name.equals(gt.getDefaultBranch())) {
                            // TODO: Replace list with remotes only...
                            terminal.warn(format("Setting diffbase on %s%s%s branch!", Color.BOLD, selected.name,
                                    Color.CLEAR));
                            terminal.println();
                            break;
                        }

                        List<BranchInfo> options = new LinkedList<>(branches).stream().filter(b -> {
                            // cannot have self as diffbase
                            if (b == selected)
                                return false;
                            // avoid circular diffs.
                            return !selected.name.equals(b.diffbase);
                        }).collect(Collectors.toList());
                        if (options.size() == 0) {
                            terminal.info("No possible diffbase branches for " + selected.name);
                            break;
                        }
                        terminal.println();

                        ArrayList<InputSelection.Command<BranchInfo>> diffbaseActions = new ArrayList<>(5);
                        diffbaseActions.add(new InputSelection.Command<>(Char.CR, "select",
                                (br, lp) -> InputSelection.Reaction.SELECT, true));
                        diffbaseActions.add(new InputSelection.Command<>('c', "clear", (br, lp) -> {
                            try {
                                StoredConfig config = git.getRepository().getConfig();
                                config.unset("branch", selected.name, "diffbase");
                                config.save();
                                return InputSelection.Reaction.EXIT;
                            } catch (IOException e) {
                                throw new RuntimeException(e.getMessage(), e);
                            }
                        }));
                        diffbaseActions.add(new InputSelection.Command<>('q', "quit",
                                (br, lp) -> InputSelection.Reaction.EXIT));

                        InputSelection<BranchInfo> selection = new InputSelection<>(terminal,
                                "Select diffbase for '" + selected.name + "':", options, diffbaseActions,
                                BranchInfo::selectionLine);
                        BranchInfo oldDiffbase = branches.stream().filter(b -> b.name.equals(selected.diffbase))
                                .findFirst().orElse(null);
                        BranchInfo newDiffbase = selection.select(oldDiffbase);
                        if (newDiffbase != null) {
                            StoredConfig config = git.getRepository().getConfig();
                            config.setString("branch", selected.name, "diffbase", newDiffbase.name);
                            config.save();
                            tmp = refreshBranchList(selected.name);
                        }

                        terminal.println();
                        break;
                    }
                    default: {
                        terminal.warn("Not implemented: " + action.name());
                        terminal.println();
                        break;
                    }
                    }
                }
            } catch (GitAPIException e) {
                terminal.fatal("GIT: " + e.getMessage());
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
    }
}