switch to ReentrantLock: this will provide ability to use tryLock
[teamcity/git-plugin.git] / git-server / src / jetbrains / buildServer / buildTriggers / vcs / git / GitMergeSupport.java
1 /*
2  * Copyright 2000-2018 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git;
18
19 import com.intellij.openapi.diagnostic.Logger;
20 import jetbrains.buildServer.vcs.*;
21 import org.eclipse.jgit.dircache.DirCache;
22 import org.eclipse.jgit.dircache.DirCacheBuilder;
23 import org.eclipse.jgit.lib.*;
24 import org.eclipse.jgit.merge.MergeStrategy;
25 import org.eclipse.jgit.merge.ResolveMerger;
26 import org.eclipse.jgit.revwalk.RevCommit;
27 import org.eclipse.jgit.revwalk.RevSort;
28 import org.eclipse.jgit.revwalk.RevWalk;
29 import org.eclipse.jgit.revwalk.filter.RevFilter;
30 import org.eclipse.jgit.transport.RefSpec;
31 import org.eclipse.jgit.transport.RemoteRefUpdate;
32 import org.eclipse.jgit.transport.Transport;
33 import org.jetbrains.annotations.NotNull;
34
35 import java.io.IOException;
36 import java.util.*;
37 import java.util.concurrent.locks.ReentrantLock;
38
39 import static java.util.Arrays.asList;
40
41 public class GitMergeSupport implements MergeSupport, GitServerExtension {
42
43   private static final Logger LOG = Logger.getInstance(GitMergeSupport.class.getName());
44
45   private final GitVcsSupport myVcs;
46   private final CommitLoader myCommitLoader;
47   private final RepositoryManager myRepositoryManager;
48   private final TransportFactory myTransportFactory;
49   private final ServerPluginConfig myPluginConfig;
50
51   public GitMergeSupport(@NotNull GitVcsSupport vcs,
52                          @NotNull CommitLoader commitLoader,
53                          @NotNull RepositoryManager repositoryManager,
54                          @NotNull TransportFactory transportFactory,
55                          @NotNull ServerPluginConfig pluginConfig) {
56     myVcs = vcs;
57     myCommitLoader = commitLoader;
58     myRepositoryManager = repositoryManager;
59     myTransportFactory = transportFactory;
60     myPluginConfig = pluginConfig;
61     myVcs.addExtension(this);
62   }
63
64   @NotNull
65   public MergeResult merge(@NotNull VcsRoot root,
66                            @NotNull String srcRevision,
67                            @NotNull String dstBranch,
68                            @NotNull String message,
69                            @NotNull MergeOptions options) throws VcsException {
70     LOG.info("Merge in root " + root + ", revision " + srcRevision + ", destination " + dstBranch);
71     OperationContext context = myVcs.createContext(root, "merge");
72     GitVcsRoot gitRoot = context.getGitRoot();
73     return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
74       try {
75         Repository db = context.getRepository();
76         int attemptsLeft = myPluginConfig.getMergeRetryAttempts();
77         MergeResult result;
78         do {
79           try {
80             result = doMerge(context, gitRoot, db, srcRevision, dstBranch, message, options);
81             if (result.isMergePerformed() && result.isSuccess()) {
82               LOG.info("Merge successfully finished in root " + root + ", revision " + srcRevision + ", destination " + dstBranch);
83               return result;
84             }
85             attemptsLeft--;
86             LOG.info("Merge was not successful, root " + root + ", revision " + srcRevision + ", destination " + dstBranch + ", attempts left " + attemptsLeft);
87           } catch (IOException e) {
88             LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e);
89             return MergeResult.createMergeError(e.getMessage());
90           } catch (VcsException e) {
91             LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e);
92             return MergeResult.createMergeError(e.getMessage());
93           }
94         } while (attemptsLeft > 0);
95         return result;
96       } catch (Exception e) {
97         throw context.wrapException(e);
98       } finally {
99         context.close();
100       }
101     });
102   }
103
104
105   @NotNull
106   public Map<MergeTask, MergeResult> tryMerge(@NotNull VcsRoot root,
107                                               @NotNull List<MergeTask> tasks,
108                                               @NotNull MergeOptions options) throws VcsException {
109     Map<MergeTask, MergeResult> mergeResults = new HashMap<MergeTask, MergeResult>();
110     OperationContext context = myVcs.createContext(root, "merge");
111     GitVcsRoot gitRoot = context.getGitRoot();
112     return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
113       try {
114         Repository db = context.getRepository();
115         for (MergeTask t : tasks) {
116           ObjectId src = ObjectId.fromString(t.getSourceRevision());
117           ObjectId dst = ObjectId.fromString(t.getDestinationRevision());
118           ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true);
119           try {
120             boolean success = merger.merge(dst, src);
121             if (success) {
122               mergeResults.put(t, MergeResult.createMergeSuccessResult());
123             } else {
124               mergeResults.put(t, MergeResult.createMergeError(merger.getUnmergedPaths()));
125             }
126           } catch (IOException mergeException) {
127             mergeResults.put(t, MergeResult.createMergeError(mergeException.getMessage()));
128           }
129         }
130       } catch (Exception e) {
131         throw context.wrapException(e);
132       } finally {
133         context.close();
134       }
135       return mergeResults;
136     });
137   }
138
139   @NotNull
140   private MergeResult doMerge(@NotNull OperationContext context,
141                               @NotNull GitVcsRoot gitRoot,
142                               @NotNull Repository db,
143                               @NotNull String srcRevision,
144                               @NotNull String dstBranch,
145                               @NotNull String message,
146                               @NotNull MergeOptions options) throws IOException, VcsException {
147     RefSpec spec = new RefSpec().setSource(GitUtils.expandRef(dstBranch)).setDestination(GitUtils.expandRef(dstBranch)).setForceUpdate(true);
148     myCommitLoader.fetch(db, gitRoot.getRepositoryFetchURL(), asList(spec), new FetchSettings(gitRoot.getAuthSettings()));
149     RevCommit srcCommit = myCommitLoader.findCommit(db, srcRevision);
150     if (srcCommit == null)
151       srcCommit = myCommitLoader.loadCommit(context, gitRoot, srcRevision);
152
153     Ref dstRef = db.getRef(dstBranch);
154     RevCommit dstBranchLastCommit = myCommitLoader.loadCommit(context, gitRoot, dstRef.getObjectId().name());
155     ObjectId commitId;
156     try {
157       commitId = mergeCommits(gitRoot, db, srcCommit, dstBranchLastCommit, message, options);
158     } catch (MergeFailedException e) {
159       LOG.debug("Merge error, root " + gitRoot + ", revision " + srcRevision + ", destination " + dstBranch, e);
160       return MergeResult.createMergeError(e.getConflicts());
161     }
162
163     ReentrantLock lock = myRepositoryManager.getWriteLock(gitRoot.getRepositoryDir());
164     lock.lock();
165     try {
166       final Transport tn = myTransportFactory.createTransport(db, gitRoot.getRepositoryPushURL(), gitRoot.getAuthSettings(),
167                                                               myPluginConfig.getPushTimeoutSeconds());
168       try {
169         RemoteRefUpdate ru = new RemoteRefUpdate(db, null, commitId, GitUtils.expandRef(dstBranch), false, null, dstBranchLastCommit);
170         tn.push(NullProgressMonitor.INSTANCE, Collections.singletonList(ru));
171         switch (ru.getStatus()) {
172           case UP_TO_DATE:
173           case OK:
174             return MergeResult.createMergeSuccessResult();
175           default:
176             return MergeResult.createMergeError("Push failed, " + ru.getMessage());
177         }
178       } catch (IOException e) {
179         LOG.debug("Error while pushing a merge commit, root " + gitRoot + ", revision " + srcRevision + ", destination " + dstBranch, e);
180         throw e;
181       } finally {
182         tn.close();
183       }
184     } finally {
185       lock.unlock();
186     }
187   }
188
189
190   @NotNull
191   private ObjectId mergeCommits(@NotNull GitVcsRoot gitRoot,
192                                 @NotNull Repository db,
193                                 @NotNull RevCommit srcCommit,
194                                 @NotNull RevCommit dstCommit,
195                                 @NotNull String message,
196                                 @NotNull MergeOptions options) throws IOException, MergeFailedException {
197     if (!alwaysCreateMergeCommit(options)) {
198       RevWalk walk = new RevWalk(db);
199       try {
200         if (walk.isMergedInto(walk.parseCommit(dstCommit), walk.parseCommit(srcCommit))) {
201           LOG.debug("Commit " + srcCommit.name() + " already merged into " + dstCommit + ", skip the merge");
202           return srcCommit;
203         }
204       } finally {
205         walk.release();
206       }
207     }
208
209     if (tryRebase(options)) {
210       LOG.debug("Run rebase, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name());
211       try {
212         return rebase(gitRoot, db, srcCommit, dstCommit);
213       } catch (MergeFailedException e) {
214         if (enforceLinearHistory(options)) {
215           LOG.debug("Rebase failed, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name(), e);
216           throw e;
217         }
218       } catch (IOException e) {
219         if (enforceLinearHistory(options)) {
220           LOG.debug("Rebase failed, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name(), e);
221           throw e;
222         }
223       }
224     }
225
226     ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true);
227     boolean mergeSuccessful = merger.merge(dstCommit, srcCommit);
228     if (!mergeSuccessful) {
229       List<String> conflicts = merger.getUnmergedPaths();
230       Collections.sort(conflicts);
231       LOG.debug("Merge failed with conflicts, root " + gitRoot + ", revision " + srcCommit.name() + ", destination " + dstCommit.name() +
232                 ", conflicts " + conflicts);
233       throw new MergeFailedException(conflicts);
234     }
235
236     ObjectInserter inserter = db.newObjectInserter();
237     DirCache dc = DirCache.newInCore();
238     DirCacheBuilder dcb = dc.builder();
239
240     dcb.addTree(new byte[]{}, 0, db.getObjectDatabase().newReader(), merger.getResultTreeId());
241     inserter.flush();
242     dcb.finish();
243
244     ObjectId writtenTreeId = dc.writeTree(inserter);
245
246     CommitBuilder commitBuilder = new CommitBuilder();
247     commitBuilder.setCommitter(gitRoot.getTagger(db));
248     commitBuilder.setAuthor(gitRoot.getTagger(db));
249     commitBuilder.setMessage(message);
250     commitBuilder.addParentId(dstCommit);
251     commitBuilder.addParentId(srcCommit);
252     commitBuilder.setTreeId(writtenTreeId);
253
254     ObjectId commitId = inserter.insert(commitBuilder);
255     inserter.flush();
256     return commitId;
257   }
258
259
260   @NotNull
261   private ObjectId rebase(@NotNull GitVcsRoot gitRoot,
262                           @NotNull Repository db,
263                           @NotNull RevCommit srcCommit,
264                           @NotNull RevCommit dstCommit) throws IOException, MergeFailedException {
265     RevWalk walk = new RevWalk(db);
266     try {
267       RevCommit src = walk.parseCommit(srcCommit);
268       RevCommit dst = walk.parseCommit(dstCommit);
269       walk.markStart(src);
270       walk.markStart(dst);
271       walk.setRevFilter(RevFilter.MERGE_BASE);
272       RevCommit base = walk.next();
273
274       Map<ObjectId, RevCommit> tree2commit = new HashMap<ObjectId, RevCommit>();
275       RevCommit c;
276
277       if (base != null) {
278         walk.reset();
279         walk.setRevFilter(RevFilter.ALL);
280         walk.markStart(dst);
281         walk.markUninteresting(base);
282         while ((c = walk.next()) != null) {
283           tree2commit.put(c.getTree().getId(), c);
284         }
285       }
286
287       walk.reset();
288       walk.markStart(src);
289       walk.markUninteresting(dst);
290       walk.sort(RevSort.TOPO);
291       walk.sort(RevSort.REVERSE);
292
293       Map<RevCommit, RevCommit> orig2rebased = new HashMap<RevCommit, RevCommit>();
294       List<RevCommit> toRebase = new ArrayList<RevCommit>();
295       while ((c = walk.next()) != null) {
296         ObjectId treeId = c.getTree().getId();
297         RevCommit existing = tree2commit.get(treeId);
298         if (existing != null) {
299           orig2rebased.put(c, existing);
300         } else {
301           if (c.getParentCount() > 1) {
302             throw new MergeFailedException(asList("Rebase of merge commits is not supported"));
303           } else {
304             toRebase.add(c);
305           }
306         }
307       }
308
309       orig2rebased.put(toRebase.get(0).getParent(0), dstCommit);
310       ObjectInserter inserter = db.newObjectInserter();
311       for (RevCommit commit : toRebase) {
312         RevCommit p = commit.getParent(0);
313         RevCommit b = orig2rebased.get(p);
314         ObjectId rebased = rebaseCommit(gitRoot, db, inserter, commit, b);
315         orig2rebased.put(commit, walk.parseCommit(rebased));
316       }
317
318       return orig2rebased.get(toRebase.get(toRebase.size() - 1));
319     } finally {
320       walk.release();
321     }
322   }
323
324
325   @NotNull
326   private ObjectId rebaseCommit(@NotNull GitVcsRoot gitRoot,
327                                 @NotNull Repository db,
328                                 @NotNull ObjectInserter inserter,
329                                 @NotNull RevCommit original,
330                                 @NotNull RevCommit base) throws IOException, MergeFailedException {
331     final RevCommit parentCommit = original.getParent(0);
332
333     if (base.equals(parentCommit))
334       return original;
335
336     ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true);
337     merger.setBase(parentCommit);
338     merger.merge(original, base);
339
340     if (merger.getResultTreeId() == null)
341       throw new MergeFailedException(merger.getUnmergedPaths());
342
343
344     if (base.getTree().getId().equals(merger.getResultTreeId()))
345       return base;
346
347     final CommitBuilder cb = new CommitBuilder();
348     cb.setTreeId(merger.getResultTreeId());
349     cb.setParentId(base);
350     cb.setAuthor(GitServerUtil.getAuthorIdent(original));
351     cb.setCommitter(gitRoot.getTagger(db));
352     cb.setMessage(GitServerUtil.getFullMessage(original));
353     final ObjectId objectId = inserter.insert(cb);
354     inserter.flush();
355     return objectId;
356   }
357
358
359   private boolean tryRebase(@NotNull MergeOptions options) {
360     String value = options.getOption("git.merge.rebase");
361     if (value == null)
362       return false;
363     return Boolean.valueOf(value);
364   }
365
366
367   private boolean enforceLinearHistory(@NotNull MergeOptions options) {
368     String value = options.getOption("git.merge.enforceLinearHistory");
369     if (value == null)
370       return false;
371     return Boolean.valueOf(value);
372   }
373
374
375   private boolean alwaysCreateMergeCommit(@NotNull MergeOptions options) {
376     String value = options.getOption("teamcity.merge.policy");
377     if (value == null)
378       return true;
379     return "alwaysCreateMergeCommit".equals(value);
380   }
381
382
383   private static class MergeFailedException extends Exception {
384     private List<String> myConflicts;
385
386     private MergeFailedException(@NotNull List<String> conflicts) {
387       myConflicts = conflicts;
388     }
389
390     @NotNull
391     public List<String> getConflicts() {
392       return myConflicts;
393     }
394   }
395 }