Java tutorial
/* * Copyright 2000-2014 JetBrains s.r.o. * * 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 jetbrains.buildServer.buildTriggers.vcs.git; import com.intellij.openapi.diagnostic.Logger; import jetbrains.buildServer.vcs.*; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.lib.*; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.transport.PushConnection; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.Transport; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.*; import static java.util.Arrays.asList; public class GitMergeSupport implements MergeSupport, GitServerExtension { private static final Logger LOG = Logger.getInstance(GitMergeSupport.class.getName()); private final GitVcsSupport myVcs; private final CommitLoader myCommitLoader; private final RepositoryManager myRepositoryManager; private final TransportFactory myTransportFactory; private final ServerPluginConfig myPluginConfig; public GitMergeSupport(@NotNull GitVcsSupport vcs, @NotNull CommitLoader commitLoader, @NotNull RepositoryManager repositoryManager, @NotNull TransportFactory transportFactory, @NotNull ServerPluginConfig pluginConfig) { myVcs = vcs; myCommitLoader = commitLoader; myRepositoryManager = repositoryManager; myTransportFactory = transportFactory; myPluginConfig = pluginConfig; myVcs.addExtension(this); } @NotNull public MergeResult merge(@NotNull VcsRoot root, @NotNull String srcRevision, @NotNull String dstBranch, @NotNull String message, @NotNull MergeOptions options) throws VcsException { LOG.info("Merge in root " + root + ", revision " + srcRevision + ", destination " + dstBranch); OperationContext context = myVcs.createContext(root, "merge"); try { GitVcsRoot gitRoot = context.getGitRoot(); Repository db = context.getRepository(); int attemptsLeft = 3; MergeResult result = MergeResult.createMergeSuccessResult(); while (attemptsLeft > 0) { try { result = doMerge(context, gitRoot, db, srcRevision, dstBranch, message, options); if (result.isMergePerformed() && result.isSuccess()) { LOG.info("Merge successfully finished in root " + root + ", revision " + srcRevision + ", destination " + dstBranch); return result; } attemptsLeft--; LOG.info("Merge was not successful, root " + root + ", revision " + srcRevision + ", destination " + dstBranch + ", attempts left " + attemptsLeft); } catch (IOException e) { LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e); return MergeResult.createMergeError(e.getMessage()); } catch (VcsException e) { LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e); return MergeResult.createMergeError(e.getMessage()); } } return result; } catch (Exception e) { throw context.wrapException(e); } finally { context.close(); } } @NotNull public Map<MergeTask, MergeResult> tryMerge(@NotNull VcsRoot root, @NotNull List<MergeTask> tasks, @NotNull MergeOptions options) throws VcsException { Map<MergeTask, MergeResult> mergeResults = new HashMap<MergeTask, MergeResult>(); OperationContext context = myVcs.createContext(root, "merge"); try { Repository db = context.getRepository(); for (MergeTask t : tasks) { ObjectId src = ObjectId.fromString(t.getSourceRevision()); ObjectId dst = ObjectId.fromString(t.getDestinationRevision()); ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true); try { boolean success = merger.merge(dst, src); if (success) { mergeResults.put(t, MergeResult.createMergeSuccessResult()); } else { mergeResults.put(t, MergeResult.createMergeError(merger.getUnmergedPaths())); } } catch (IOException mergeException) { mergeResults.put(t, MergeResult.createMergeError(mergeException.getMessage())); } } } catch (Exception e) { throw context.wrapException(e); } finally { context.close(); } return mergeResults; } @NotNull private MergeResult doMerge(@NotNull OperationContext context, @NotNull GitVcsRoot gitRoot, @NotNull Repository db, @NotNull String srcRevision, @NotNull String dstBranch, @NotNull String message, @NotNull MergeOptions options) throws IOException, VcsException { RefSpec spec = new RefSpec().setSource(GitUtils.expandRef(dstBranch)) .setDestination(GitUtils.expandRef(dstBranch)).setForceUpdate(true); myCommitLoader.fetch(db, gitRoot.getRepositoryFetchURL(), asList(spec), new FetchSettings(gitRoot.getAuthSettings())); RevCommit srcCommit = myCommitLoader.findCommit(db, srcRevision); if (srcCommit == null) srcCommit = myCommitLoader.loadCommit(context, gitRoot, srcRevision); Ref dstRef = db.getRef(dstBranch); RevCommit dstBranchLastCommit = myCommitLoader.loadCommit(context, gitRoot, dstRef.getObjectId().name()); ObjectId commitId; try { commitId = mergeCommits(gitRoot, db, srcCommit, dstBranchLastCommit, message, options); } catch (MergeFailedException e) { LOG.debug("Merge error, root " + gitRoot + ", revision " + srcRevision + ", destination " + dstBranch, e); return MergeResult.createMergeError(e.getConflicts()); } synchronized (myRepositoryManager.getWriteLock(gitRoot.getRepositoryDir())) { final Transport tn = myTransportFactory.createTransport(db, gitRoot.getRepositoryPushURL(), gitRoot.getAuthSettings(), myPluginConfig.getPushTimeoutSeconds()); try { final PushConnection c = tn.openPush(); try { RemoteRefUpdate ru = new RemoteRefUpdate(db, null, commitId, GitUtils.expandRef(dstBranch), false, null, dstBranchLastCommit); c.push(NullProgressMonitor.INSTANCE, Collections.singletonMap(GitUtils.expandRef(dstBranch), ru)); switch (ru.getStatus()) { case UP_TO_DATE: case OK: return MergeResult.createMergeSuccessResult(); default: return MergeResult.createMergeError("Push failed, " + ru.getMessage()); } } finally { c.close(); } } catch (IOException e) { LOG.debug("Error while pushing a merge commit, root " + gitRoot + ", revision " + srcRevision + ", destination " + dstBranch, e); throw e; } finally { tn.close(); } } } @NotNull private ObjectId mergeCommits(@NotNull GitVcsRoot gitRoot, @NotNull Repository db, @NotNull RevCommit srcCommit, @NotNull RevCommit dstCommit, @NotNull String message, @NotNull MergeOptions options) throws IOException, MergeFailedException { if (!alwaysCreateMergeCommit(options)) { RevWalk walk = new RevWalk(db); try { if (walk.isMergedInto(walk.parseCommit(dstCommit), walk.parseCommit(srcCommit))) { LOG.debug("Commit " + srcCommit.name() + " already merged into " + dstCommit + ", skip the merge"); return srcCommit; } } finally { walk.release(); } } if (tryRebase(options)) { LOG.debug("Run rebase, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name()); try { return rebase(gitRoot, db, srcCommit, dstCommit); } catch (MergeFailedException e) { if (enforceLinearHistory(options)) { LOG.debug("Rebase failed, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name(), e); throw e; } } catch (IOException e) { if (enforceLinearHistory(options)) { LOG.debug("Rebase failed, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name(), e); throw e; } } } ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true); boolean mergeSuccessful = merger.merge(dstCommit, srcCommit); if (!mergeSuccessful) { List<String> conflicts = merger.getUnmergedPaths(); Collections.sort(conflicts); LOG.debug("Merge failed with conflicts, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name() + ", conflicts " + conflicts); throw new MergeFailedException(conflicts); } ObjectInserter inserter = db.newObjectInserter(); DirCache dc = DirCache.newInCore(); DirCacheBuilder dcb = dc.builder(); dcb.addTree(new byte[] {}, 0, db.getObjectDatabase().newReader(), merger.getResultTreeId()); inserter.flush(); dcb.finish(); ObjectId writtenTreeId = dc.writeTree(inserter); CommitBuilder commitBuilder = new CommitBuilder(); commitBuilder.setCommitter(gitRoot.getTagger(db)); commitBuilder.setAuthor(gitRoot.getTagger(db)); commitBuilder.setMessage(message); commitBuilder.addParentId(dstCommit); commitBuilder.addParentId(srcCommit); commitBuilder.setTreeId(writtenTreeId); ObjectId commitId = inserter.insert(commitBuilder); inserter.flush(); return commitId; } @NotNull private ObjectId rebase(@NotNull GitVcsRoot gitRoot, @NotNull Repository db, @NotNull RevCommit srcCommit, @NotNull RevCommit dstCommit) throws IOException, MergeFailedException { RevWalk walk = new RevWalk(db); try { RevCommit src = walk.parseCommit(srcCommit); RevCommit dst = walk.parseCommit(dstCommit); walk.markStart(src); walk.markStart(dst); walk.setRevFilter(RevFilter.MERGE_BASE); RevCommit base = walk.next(); Map<ObjectId, RevCommit> tree2commit = new HashMap<ObjectId, RevCommit>(); RevCommit c; if (base != null) { walk.reset(); walk.setRevFilter(RevFilter.ALL); walk.markStart(dst); walk.markUninteresting(base); while ((c = walk.next()) != null) { tree2commit.put(c.getTree().getId(), c); } } walk.reset(); walk.markStart(src); walk.markUninteresting(dst); walk.sort(RevSort.TOPO); walk.sort(RevSort.REVERSE); Map<RevCommit, RevCommit> orig2rebased = new HashMap<RevCommit, RevCommit>(); List<RevCommit> toRebase = new ArrayList<RevCommit>(); while ((c = walk.next()) != null) { ObjectId treeId = c.getTree().getId(); RevCommit existing = tree2commit.get(treeId); if (existing != null) { orig2rebased.put(c, existing); } else { if (c.getParentCount() > 1) { throw new MergeFailedException(asList("Rebase of merge commits is not supported")); } else { toRebase.add(c); } } } orig2rebased.put(toRebase.get(0).getParent(0), dstCommit); ObjectInserter inserter = db.newObjectInserter(); for (RevCommit commit : toRebase) { RevCommit p = commit.getParent(0); RevCommit b = orig2rebased.get(p); ObjectId rebased = rebaseCommit(gitRoot, db, inserter, commit, b); orig2rebased.put(commit, walk.parseCommit(rebased)); } return orig2rebased.get(toRebase.get(toRebase.size() - 1)); } finally { walk.release(); } } @NotNull private ObjectId rebaseCommit(@NotNull GitVcsRoot gitRoot, @NotNull Repository db, @NotNull ObjectInserter inserter, @NotNull RevCommit original, @NotNull RevCommit base) throws IOException, MergeFailedException { final RevCommit parentCommit = original.getParent(0); if (base.equals(parentCommit)) return original; ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true); merger.setBase(parentCommit); merger.merge(original, base); if (merger.getResultTreeId() == null) throw new MergeFailedException(merger.getUnmergedPaths()); if (base.getTree().getId().equals(merger.getResultTreeId())) return base; final CommitBuilder cb = new CommitBuilder(); cb.setTreeId(merger.getResultTreeId()); cb.setParentId(base); cb.setAuthor(GitServerUtil.getAuthorIdent(original)); cb.setCommitter(gitRoot.getTagger(db)); cb.setMessage(GitServerUtil.getFullMessage(original)); final ObjectId objectId = inserter.insert(cb); inserter.flush(); return objectId; } private boolean tryRebase(@NotNull MergeOptions options) { String value = options.getOption("git.merge.rebase"); if (value == null) return false; return Boolean.valueOf(value); } private boolean enforceLinearHistory(@NotNull MergeOptions options) { String value = options.getOption("git.merge.enforceLinearHistory"); if (value == null) return false; return Boolean.valueOf(value); } private boolean alwaysCreateMergeCommit(@NotNull MergeOptions options) { String value = options.getOption("teamcity.merge.policy"); if (value == null) return true; return "alwaysCreateMergeCommit".equals(value); } private static class MergeFailedException extends Exception { private List<String> myConflicts; private MergeFailedException(@NotNull List<String> conflicts) { myConflicts = conflicts; } @NotNull public List<String> getConflicts() { return myConflicts; } } }