/*
* Copyright 2012 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Eclipse Public License version 1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jboss.forge.addon.git;
import static org.jboss.forge.addon.git.constants.GitConstants.GIT_DIRECTORY;
import static org.jboss.forge.addon.git.constants.GitConstants.GIT_REFS_HEADS;
import static org.jboss.forge.addon.git.constants.GitConstants.GIT_REMOTE_ORIGIN;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CherryPickResult;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryBuilder;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.merge.MergeMessageFormatter;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.jboss.forge.addon.git.exceptions.CantMergeCommitException;
import org.jboss.forge.addon.resource.DirectoryResource;
import org.jboss.forge.addon.resource.FileResource;
import org.jboss.forge.furnace.util.Strings;
/**
* Convenience tools for interacting with the Git version control system.
*
* @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
* @author <a href="mailto:jevgeni.zelenkov@gmail.com">Jevgeni Zelenkov</a>
*
*/
public class GitUtilsImpl implements GitUtils
{
@Override
public Git clone(final DirectoryResource dir, final String repoUri) throws GitAPIException
{
CloneCommand clone = Git.cloneRepository().setURI(repoUri)
.setDirectory(dir.getUnderlyingResourceObject());
Git git = clone.call();
return git;
}
@Override
public Git git(final DirectoryResource dir) throws IOException
{
RepositoryBuilder db = new RepositoryBuilder().findGitDir(dir.getUnderlyingResourceObject());
return new Git(db.build());
}
@Override
public Ref checkout(final Git git, final String remote, final boolean createBranch,
final SetupUpstreamMode mode, final boolean force)
throws GitAPIException
{
CheckoutCommand checkout = git.checkout();
checkout.setCreateBranch(createBranch);
checkout.setName(remote);
checkout.setForce(force);
checkout.setUpstreamMode(mode);
return checkout.call();
}
@Override
public Ref checkout(final Git git, final Ref localRef, final SetupUpstreamMode mode,
final boolean force) throws GitAPIException
{
CheckoutCommand checkout = git.checkout();
checkout.setName(Repository.shortenRefName(localRef.getName()));
checkout.setForce(force);
checkout.setUpstreamMode(mode);
return checkout.call();
}
@Override
public FetchResult fetch(final Git git, final String remote, final String refSpec, final int timeout,
final boolean fsck, final boolean dryRun,
final boolean thin,
final boolean prune) throws GitAPIException
{
FetchCommand fetch = git.fetch();
fetch.setCheckFetchedObjects(fsck);
fetch.setRemoveDeletedRefs(prune);
if (refSpec != null)
fetch.setRefSpecs(new RefSpec(refSpec));
if (timeout >= 0)
fetch.setTimeout(timeout);
fetch.setDryRun(dryRun);
fetch.setRemote(remote);
fetch.setThin(thin);
fetch.setProgressMonitor(new TextProgressMonitor());
FetchResult result = fetch.call();
return result;
}
@Override
public Git init(final DirectoryResource dir) throws IOException
{
FileResource<?> gitDir = dir.getChildDirectory(GIT_DIRECTORY).reify(FileResource.class);
gitDir.mkdirs();
RepositoryBuilder db = new RepositoryBuilder().setGitDir(gitDir.getUnderlyingResourceObject()).setup();
Git git = new Git(db.build());
git.getRepository().create();
return git;
}
@Override
public PullResult pull(final Git git, final int timeout) throws GitAPIException
{
PullCommand pull = git.pull();
if (timeout >= 0)
pull.setTimeout(timeout);
pull.setProgressMonitor(new TextProgressMonitor());
PullResult result = pull.call();
return result;
}
@Override
public List<Ref> getRemoteBranches(final Git repo) throws GitAPIException
{
List<Ref> results = new ArrayList<>();
try
{
FetchResult fetch = repo.fetch().setRemote(GIT_REMOTE_ORIGIN).call();
Collection<Ref> refs = fetch.getAdvertisedRefs();
for (Ref ref : refs)
{
if (ref.getName().startsWith(GIT_REFS_HEADS))
{
results.add(ref);
}
}
}
catch (InvalidRemoteException e)
{
e.printStackTrace();
}
return results;
}
@Override
public List<Ref> getLocalBranches(final Git repo) throws GitAPIException
{
// returns only local branches by default
return repo.branchList().call();
}
@Override
public String getCurrentBranchName(final Git repo) throws IOException
{
return repo.getRepository().getBranch();
}
@Override
public Ref switchBranch(final Git repo, final String branchName)
{
try
{
return repo.checkout().setName(branchName).call();
}
catch (GitAPIException e)
{
throw new RuntimeException("Couldn't switch to branch " + branchName + ": " + e.getMessage(), e);
}
}
@Override
public List<String> getLogForCurrentBranch(final Git repo) throws GitAPIException
{
List<String> results = new ArrayList<>();
Iterable<RevCommit> commits = repo.log().call();
for (RevCommit commit : commits)
results.add(commit.getFullMessage());
return results;
}
@Override
public List<String> getLogForBranch(final Git repo, String branchName) throws GitAPIException,
IOException
{
String oldBranch = repo.getRepository().getBranch();
repo.checkout().setName(branchName).call();
List<String> results = getLogForCurrentBranch(repo);
repo.checkout().setName(oldBranch).call();
return results;
}
@Override
public void add(final Git repo, String filepattern) throws GitAPIException
{
repo.add().addFilepattern(filepattern).call();
}
@Override
public void addAll(final Git repo) throws GitAPIException
{
repo.add().addFilepattern(".").call();
}
@Override
public void commit(final Git repo, String message) throws GitAPIException
{
repo.commit().setMessage(message).call();
}
@Override
public void commitAll(final Git repo, String message) throws GitAPIException
{
repo.commit().setMessage(message).setAll(true).call();
}
@Override
public void stashCreate(final Git repo) throws GitAPIException
{
repo.stashCreate().call();
}
@Override
public void stashApply(final Git repo, String... stashRef) throws GitAPIException
{
if (stashRef.length >= 1 && !Strings.isNullOrEmpty(stashRef[0]))
{
repo.stashApply().setStashRef(stashRef[0]).call();
}
else
{
repo.stashApply().call();
}
}
@Override
public void stashDrop(final Git repo) throws GitAPIException
{
repo.stashDrop().call();
}
@Override
public void cherryPick(final Git repo, Ref commit) throws GitAPIException
{
repo.cherryPick().include(commit).call();
}
@Override
public CherryPickResult cherryPickNoMerge(final Git git, Ref src) throws GitAPIException,
CantMergeCommitException
{
// Does the same as the original git-cherryPick
// except commiting after running merger
Repository repo = git.getRepository();
RevCommit newHead = null;
List<Ref> cherryPickedRefs = new LinkedList<Ref>();
RevWalk revWalk = new RevWalk(repo);
try
{
// get the head commit
Ref headRef = repo.getRef(Constants.HEAD);
if (headRef == null)
throw new NoHeadException(
JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
RevCommit headCommit = revWalk.parseCommit(headRef.getObjectId());
newHead = headCommit;
// get the commit to be cherry-picked
// handle annotated tags
ObjectId srcObjectId = src.getPeeledObjectId();
if (srcObjectId == null)
srcObjectId = src.getObjectId();
RevCommit srcCommit = revWalk.parseCommit(srcObjectId);
// get the parent of the commit to cherry-pick
if (srcCommit.getParentCount() == 0)
throw new CantMergeCommitException("Commit with zero parents cannot be merged");
if (srcCommit.getParentCount() > 1)
throw new MultipleParentsNotAllowedException(
MessageFormat.format(
JGitText.get().canOnlyCherryPickCommitsWithOneParent,
srcCommit.name(),
Integer.valueOf(srcCommit.getParentCount())));
RevCommit srcParent = srcCommit.getParent(0);
revWalk.parseHeaders(srcParent);
ResolveMerger merger = (ResolveMerger) MergeStrategy.RESOLVE
.newMerger(repo);
merger.setWorkingTreeIterator(new FileTreeIterator(repo));
merger.setBase(srcParent.getTree());
if (merger.merge(headCommit, srcCommit))
{
DirCacheCheckout dco = new DirCacheCheckout(repo,
headCommit.getTree(), repo.lockDirCache(),
merger.getResultTreeId());
dco.setFailOnConflict(true);
dco.checkout();
cherryPickedRefs.add(src);
}
else
{
if (merger.failed())
return new CherryPickResult(merger.getFailingPaths());
// there are merge conflicts
String message = new MergeMessageFormatter()
.formatWithConflicts(srcCommit.getFullMessage(),
merger.getUnmergedPaths());
repo.writeCherryPickHead(srcCommit.getId());
repo.writeMergeCommitMsg(message);
return CherryPickResult.CONFLICT;
}
}
catch (IOException e)
{
throw new JGitInternalException(
MessageFormat.format(
JGitText.get().exceptionCaughtDuringExecutionOfCherryPickCommand,
e), e);
}
finally
{
revWalk.release();
}
return new CherryPickResult(newHead, cherryPickedRefs);
}
@Override
public void resetHard(final Git repo, String newBase) throws GitAPIException
{
repo.reset().setMode(ResetCommand.ResetType.HARD).setRef(newBase).call();
}
@Override
public Ref createBranch(Git git, String branchName) throws GitAPIException
{
Ref newBranch = git.branchCreate().setName(branchName).call();
if (newBranch == null)
throw new RuntimeException("Couldn't create new branch " + branchName);
return newBranch;
}
@Override
public void close(final Git repo)
{
if (repo != null)
{
repo.close();
}
}
}