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