/* Copyright (c) 2012-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.cli.porcelain; import java.io.IOException; import java.util.List; import jline.console.ConsoleReader; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.Ansi.Color; import org.locationtech.geogig.api.GeoGIG; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.SymRef; import org.locationtech.geogig.api.plumbing.RefParse; import org.locationtech.geogig.api.porcelain.BranchCreateOp; import org.locationtech.geogig.api.porcelain.BranchDeleteOp; import org.locationtech.geogig.api.porcelain.BranchListOp; import org.locationtech.geogig.api.porcelain.BranchRenameOp; import org.locationtech.geogig.cli.AbstractCommand; import org.locationtech.geogig.cli.CLICommand; import org.locationtech.geogig.cli.GeogigCLI; import org.locationtech.geogig.cli.annotation.ObjectDatabaseReadOnly; import org.locationtech.geogig.cli.annotation.StagingDatabaseReadOnly; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; /** * With no arguments the command will display all existing branches with the current branch * highlighted with the asterisk. The {@code -r} option will list only remote branches and the * {@code -a} option will list both local and remote branches. Adding the {@code --color} option * with the value of auto, always, or never will add or remove color from the listing. With the * {@code -v} option it will list the branches along with the commit id and commit message that the * branch is currently on. * <p> * With a branch name specified it will create a branch of off the current branch. If a start point * is specified as well then it will be created off of the given start point. If the -c option is * given it will automatically checkout the branch once it is created. * <p> * With the -d option with a branch name specified will delete that branch. You cannot delete the * branch that you are currently on, checkout a different branch to delete it. Also with the -d * option you can list multiple branches for deletion. * <p> * With the -m option you can specify an oldBranchName to rename with the given newBranchName or you * can rename the current branch by not specifying oldBranchName. With the --force option you can * rename a branch to a name that already exists as a branch, however this will delete the other * branch. * <p> * CLI proxy {@link BranchListOp}, {@link BranchCreateOp}, {@link BranchDeleteOp}, * {@link BranchRenameOp} * <p> * Usage: * <ul> * <li> {@code geogig branch [-c] <branchname>[<startpoint>]}: Creates a new branch with the given * branchname at the specified startpoint and checks it out immediately * <li> {@code geogig branch [--color=always] [-v] [-a]}: Lists all branches (Local and Remote) in * color with commit id and commit message * <li> {@code geogig branch [-r]}: List only remote branches * <li> {@code geogig branch [--delete] <branchname>...}: Deletes all the branches listed unless HEAD * is pointing to it * <li> {@code geogig branch [--force] [--rename] [<oldBranchName>] <newBranchName>}: Renames a * branch specified by oldBranchName or current branch if no oldBranchName is given to newBranchName * </ul> * * @see BranchListOp * @see BranchCreateOp * @see BranchDeleteOp * @see BranchRenameOp */ @ObjectDatabaseReadOnly @StagingDatabaseReadOnly @Parameters(commandNames = "branch", commandDescription = "List, create, or delete branches") public class Branch extends AbstractCommand implements CLICommand { @Parameter(description = "<branch name> [<start point>]") private List<String> branchName = Lists.newArrayList(); @Parameter(names = { "--checkout", "-c" }, description = "automatically checkout the new branch when the command is used to create a branch") private boolean checkout; @Parameter(names = { "--delete", "-d" }) private boolean delete = false; @Parameter(names = { "--orphan", "-o" }, description = "create an orphan branch") private boolean orphan = false; @Parameter(names = { "--force", "-f" }, description = "Force renaming/creating of a branch if the specified branc name already exists") private boolean force = false; @Parameter(names = { "--verbose", "-v", "Verbose output for list mode. Shows branch commit id and commit message." }) private boolean verbose = false; @Parameter(names = { "--remote", "-r" }, description = "List or delete (if used with -d) the remote-tracking branches.") private boolean remotes = false; @Parameter(names = { "--all", "-a" }, description = "List all branches, both local and remote") private boolean all = false; @Parameter(names = { "--rename", "-m" }, description = "Rename branch ") private boolean rename = false; @Override public void runInternal(final GeogigCLI cli) throws IOException { final GeoGIG geogig = cli.getGeogig(); final ConsoleReader console = cli.getConsole(); if (delete) { checkParameter(!branchName.isEmpty(), "no name specified for deletion"); for (String br : branchName) { Optional<? extends Ref> deletedBranch; deletedBranch = geogig.command(BranchDeleteOp.class).setName(br).call(); checkParameter(deletedBranch.isPresent(), "No branch called '%s'.", br); console.println(String.format("Deleted branch '%s'.", br)); } return; } checkParameter(branchName.size() < 3, "too many arguments: %s", branchName); if (rename) { checkParameter(!branchName.isEmpty(), "You must specify a branch to rename."); if (branchName.size() == 1) { Optional<Ref> headRef = geogig.command(RefParse.class).setName(Ref.HEAD).call(); geogig.command(BranchRenameOp.class).setNewName(branchName.get(0)).setForce(force) .call(); if (headRef.isPresent()) { SymRef ref = (SymRef) headRef.get(); console.println("renamed branch '" + ref.getTarget().substring(Ref.HEADS_PREFIX.length()) + "' to '" + branchName.get(0) + "'"); } } else { geogig.command(BranchRenameOp.class).setOldName(branchName.get(0)) .setNewName(branchName.get(1)).setForce(force).call(); console.println("renamed branch '" + branchName.get(0) + "' to '" + branchName.get(1) + "'"); } return; } if (branchName.isEmpty()) { listBranches(cli); return; } final String branch = branchName.get(0); final String origin = branchName.size() > 1 ? branchName.get(1) : Ref.HEAD; Ref newBranch = geogig.command(BranchCreateOp.class).setName(branch).setForce(force) .setOrphan(orphan).setAutoCheckout(checkout).setSource(origin).call(); console.println("Created branch " + newBranch.getName()); } private void listBranches(GeogigCLI cli) throws IOException { final ConsoleReader console = cli.getConsole(); final GeoGIG geogig = cli.getGeogig(); boolean local = all || !(remotes); boolean remote = all || remotes; ImmutableList<Ref> branches = geogig.command(BranchListOp.class).setLocal(local) .setRemotes(remote).call(); final Ref currentHead = geogig.command(RefParse.class).setName(Ref.HEAD).call().get(); final int largest = verbose ? largestLenght(branches) : 0; for (Ref branchRef : branches) { final String branchRefName = branchRef.getName(); Ansi ansi = newAnsi(console.getTerminal()); if ((currentHead instanceof SymRef) && ((SymRef) currentHead).getTarget().equals(branchRefName)) { ansi.a("* ").fg(Color.GREEN); } else { ansi.a(" "); } // print unqualified names for local branches String branchName = refDisplayString(branchRef); ansi.a(branchName); ansi.reset(); if (verbose) { ansi.a(Strings.repeat(" ", 1 + (largest - branchName.length()))); ansi.a(branchRef.getObjectId().toString().substring(0, 7)).a(" "); Optional<RevCommit> commit = findCommit(geogig, branchRef); if (commit.isPresent()) { ansi.a(messageTitle(commit.get())); } } console.println(ansi.toString()); } } private String messageTitle(RevCommit commit) { String message = Optional.fromNullable(commit.getMessage()).or(""); int newline = message.indexOf('\n'); return newline == -1 ? message : message.substring(0, newline); } /** * @param branchRef * @return */ private Optional<RevCommit> findCommit(GeoGIG geogig, Ref branchRef) { ObjectId commitId = branchRef.getObjectId(); if (commitId.isNull()) { return Optional.absent(); } RevCommit commit = geogig.getRepository().getCommit(commitId); return Optional.of(commit); } /** * @param branches * @return */ private int largestLenght(ImmutableList<Ref> branches) { int len = 0; for (Ref ref : branches) { len = Math.max(len, refDisplayString(ref).length()); } return len; } private String refDisplayString(Ref ref) { String branchName = ref.getName(); if (branchName.startsWith(Ref.HEADS_PREFIX)) { branchName = ref.localName(); } else if (branchName.startsWith(Ref.REMOTES_PREFIX)) { branchName = branchName.substring(Ref.REMOTES_PREFIX.length()); } if (ref instanceof SymRef) { branchName += " -> " + Ref.localName(((SymRef) ref).getTarget()); } return branchName; } }