Line data Source code
1 : // Copyright (C) 2016 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.patch;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 :
19 : import com.google.common.flogger.FluentLogger;
20 : import com.google.gerrit.common.UsedAt;
21 : import com.google.gerrit.entities.RefNames;
22 : import com.google.gerrit.metrics.Counter1;
23 : import com.google.gerrit.metrics.Description;
24 : import com.google.gerrit.metrics.Field;
25 : import com.google.gerrit.metrics.MetricMaker;
26 : import com.google.gerrit.metrics.Timer1;
27 : import com.google.gerrit.server.GerritPersonIdent;
28 : import com.google.gerrit.server.config.GerritServerConfig;
29 : import com.google.gerrit.server.git.InMemoryInserter;
30 : import com.google.gerrit.server.git.MergeUtil;
31 : import com.google.gerrit.server.logging.Metadata;
32 : import com.google.gerrit.server.update.RepoView;
33 : import com.google.inject.Inject;
34 : import com.google.inject.Provider;
35 : import com.google.inject.Singleton;
36 : import java.io.IOException;
37 : import java.util.Optional;
38 : import org.eclipse.jgit.dircache.DirCache;
39 : import org.eclipse.jgit.lib.CommitBuilder;
40 : import org.eclipse.jgit.lib.Config;
41 : import org.eclipse.jgit.lib.ObjectId;
42 : import org.eclipse.jgit.lib.ObjectInserter;
43 : import org.eclipse.jgit.lib.ObjectReader;
44 : import org.eclipse.jgit.lib.PersonIdent;
45 : import org.eclipse.jgit.lib.Ref;
46 : import org.eclipse.jgit.lib.Repository;
47 : import org.eclipse.jgit.merge.ResolveMerger;
48 : import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
49 : import org.eclipse.jgit.revwalk.RevCommit;
50 : import org.eclipse.jgit.revwalk.RevObject;
51 : import org.eclipse.jgit.revwalk.RevWalk;
52 : import org.eclipse.jgit.transport.ReceiveCommand;
53 :
54 : /**
55 : * Utility class for creating an auto-merge commit of a merge commit.
56 : *
57 : * <p>An auto-merge commit is the result of merging the 2 parents of a merge commit automatically.
58 : * If there are conflicts the auto-merge commit contains Git conflict markers that indicate these
59 : * conflicts.
60 : *
61 : * <p>Creating auto-merge commits for octopus merges (merge commits with more than 2 parents) is not
62 : * supported. In this case the auto-merge is created between the first 2 parent commits.
63 : *
64 : * <p>All created auto-merge commits are stored in the repository of their merge commit as {@code
65 : * refs/cache-automerge/} branches. These branches serve:
66 : *
67 : * <ul>
68 : * <li>as a cache so that the each auto-merge gets computed only once
69 : * <li>as base for merge commits on which users can comment
70 : * </ul>
71 : *
72 : * <p>The second point means that these commits are referenced from NoteDb. The consequence of this
73 : * is that these refs should never be deleted.
74 : */
75 : @Singleton
76 : public class AutoMerger {
77 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
78 :
79 : public static final String AUTO_MERGE_MSG_PREFIX = "Auto-merge of ";
80 :
81 : @UsedAt(UsedAt.Project.GOOGLE)
82 : public static boolean cacheAutomerge(Config cfg) {
83 152 : return cfg.getBoolean("change", null, "cacheAutomerge", true);
84 : }
85 :
86 31 : private enum OperationType {
87 31 : CACHE_LOAD,
88 31 : IN_MEMORY_WRITE,
89 31 : ON_DISK_WRITE
90 : }
91 :
92 : private final Counter1<OperationType> counter;
93 : private final Timer1<OperationType> latency;
94 : private final Provider<PersonIdent> gerritIdentProvider;
95 : private final boolean save;
96 : private final ThreeWayMergeStrategy configuredMergeStrategy;
97 :
98 : @Inject
99 : AutoMerger(
100 : MetricMaker metricMaker,
101 : @GerritServerConfig Config cfg,
102 152 : @GerritPersonIdent Provider<PersonIdent> gerritIdentProvider) {
103 152 : Field<OperationType> operationTypeField =
104 152 : Field.ofEnum(OperationType.class, "type", Metadata.Builder::operationName)
105 152 : .description("The type of the operation (CACHE_LOAD, IN_MEMORY_WRITE, ON_DISK_WRITE).")
106 152 : .build();
107 152 : this.counter =
108 152 : metricMaker.newCounter(
109 : "git/auto-merge/num_operations",
110 152 : new Description("AutoMerge computations").setRate().setUnit("auto merge computations"),
111 : operationTypeField);
112 152 : this.latency =
113 152 : metricMaker.newTimer(
114 : "git/auto-merge/latency",
115 : new Description("AutoMerge computation latency")
116 152 : .setCumulative()
117 152 : .setUnit("milliseconds"),
118 : operationTypeField);
119 152 : this.save = cacheAutomerge(cfg);
120 152 : this.gerritIdentProvider = gerritIdentProvider;
121 152 : this.configuredMergeStrategy = MergeUtil.getMergeStrategy(cfg);
122 152 : }
123 :
124 : /**
125 : * Reads or creates an auto-merge commit of the parents of the given merge commit.
126 : *
127 : * <p>The result is read from Git or computed in-memory and not written back to Git. This method
128 : * exists for backwards compatibility only. All new changes have their auto-merge commits written
129 : * transactionally when the change or patch set is created.
130 : *
131 : * @return auto-merge commit. Headers of the returned RevCommit are parsed.
132 : */
133 : public RevCommit lookupFromGitOrMergeInMemory(
134 : Repository repo,
135 : RevWalk rw,
136 : InMemoryInserter ins,
137 : RevCommit merge,
138 : ThreeWayMergeStrategy mergeStrategy)
139 : throws IOException {
140 0 : checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
141 0 : Optional<RevCommit> existingCommit =
142 0 : lookupCommit(repo, rw, RefNames.refsCacheAutomerge(merge.name()));
143 0 : if (existingCommit.isPresent()) {
144 0 : counter.increment(OperationType.CACHE_LOAD);
145 0 : return existingCommit.get();
146 : }
147 0 : counter.increment(OperationType.IN_MEMORY_WRITE);
148 0 : logger.atInfo().log("Computing in-memory AutoMerge for %s", merge.name());
149 0 : try (Timer1.Context<OperationType> ignored = latency.start(OperationType.IN_MEMORY_WRITE)) {
150 0 : return rw.parseCommit(createAutoMergeCommit(repo.getConfig(), rw, ins, merge, mergeStrategy));
151 : }
152 : }
153 :
154 : /**
155 : * Creates an auto merge commit for the provided commit in case it is a merge commit. To be used
156 : * whenever Gerrit creates new patch sets.
157 : *
158 : * <p>Callers need to include the returned {@link ReceiveCommand} in their ref transaction.
159 : *
160 : * @return A {@link ReceiveCommand} wrapped in an {@link Optional} to be used in a {@link
161 : * org.eclipse.jgit.lib.BatchRefUpdate}. {@link Optional#empty()} in case we don't need an
162 : * auto merge commit.
163 : */
164 : public Optional<ReceiveCommand> createAutoMergeCommitIfNecessary(
165 : RepoView repoView, RevWalk rw, ObjectInserter ins, RevCommit maybeMergeCommit)
166 : throws IOException {
167 103 : if (maybeMergeCommit.getParentCount() != 2 || !save) {
168 103 : logger.atFine().log("AutoMerge not required");
169 103 : return Optional.empty();
170 : }
171 :
172 30 : String automergeRef = RefNames.refsCacheAutomerge(maybeMergeCommit.name());
173 30 : logger.atFine().log("AutoMerge ref=%s, mergeCommit=%s", automergeRef, maybeMergeCommit.name());
174 30 : if (repoView.getRef(automergeRef).isPresent()) {
175 1 : logger.atFine().log("AutoMerge alredy exists");
176 1 : return Optional.empty();
177 : }
178 :
179 30 : return Optional.of(
180 : new ReceiveCommand(
181 30 : ObjectId.zeroId(),
182 30 : createAutoMergeCommit(repoView, rw, ins, maybeMergeCommit),
183 : automergeRef));
184 : }
185 :
186 : /**
187 : * Creates an auto merge commit for the provided merge commit.
188 : *
189 : * <p>Callers are expected to ensure that the provided commit indeed has 2 parents.
190 : *
191 : * @return An auto-merge commit. Headers of the returned RevCommit are parsed.
192 : */
193 : ObjectId createAutoMergeCommit(
194 : RepoView repoView, RevWalk rw, ObjectInserter ins, RevCommit mergeCommit) throws IOException {
195 : ObjectId autoMerge;
196 31 : try (Timer1.Context<OperationType> ignored = latency.start(OperationType.ON_DISK_WRITE)) {
197 31 : autoMerge =
198 31 : createAutoMergeCommit(
199 31 : repoView.getConfig(), rw, ins, mergeCommit, configuredMergeStrategy);
200 : }
201 31 : counter.increment(OperationType.ON_DISK_WRITE);
202 31 : logger.atFine().log("Added %s AutoMerge ref update for commit", autoMerge.name());
203 31 : return autoMerge;
204 : }
205 :
206 : Optional<RevCommit> lookupCommit(Repository repo, RevWalk rw, String refName) throws IOException {
207 31 : Ref ref = repo.getRefDatabase().exactRef(refName);
208 31 : if (ref != null && ref.getObjectId() != null) {
209 30 : RevObject obj = rw.parseAny(ref.getObjectId());
210 30 : if (obj instanceof RevCommit) {
211 30 : return Optional.of((RevCommit) obj);
212 : }
213 : }
214 1 : return Optional.empty();
215 : }
216 :
217 : /**
218 : * Creates an auto-merge commit of the parents of the given merge commit.
219 : *
220 : * @return auto-merge commit. Headers of the returned RevCommit are parsed.
221 : */
222 : private ObjectId createAutoMergeCommit(
223 : Config repoConfig,
224 : RevWalk rw,
225 : ObjectInserter ins,
226 : RevCommit merge,
227 : ThreeWayMergeStrategy mergeStrategy)
228 : throws IOException {
229 31 : rw.parseHeaders(merge);
230 31 : ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(ins, repoConfig);
231 31 : DirCache dc = DirCache.newInCore();
232 31 : m.setDirCache(dc);
233 : // If we don't plan on saving results, use a fully in-memory inserter.
234 : // Using just a non-flushing wrapper is not sufficient, since in particular DfsInserter might
235 : // try to write to storage after exceeding an internal buffer size.
236 31 : m.setObjectInserter(ins instanceof InMemoryInserter ? new NonFlushingWrapper(ins) : ins);
237 :
238 31 : boolean couldMerge = m.merge(merge.getParents());
239 :
240 : ObjectId treeId;
241 31 : if (couldMerge) {
242 25 : treeId = m.getResultTreeId();
243 : } else {
244 24 : treeId =
245 24 : MergeUtil.mergeWithConflicts(
246 : rw,
247 : ins,
248 : dc,
249 : "HEAD",
250 24 : merge.getParent(0),
251 : "BRANCH",
252 24 : merge.getParent(1),
253 24 : m.getMergeResults());
254 : }
255 31 : logger.atFine().log("AutoMerge treeId=%s", treeId.name());
256 :
257 31 : rw.parseHeaders(merge);
258 : // For maximum stability, choose a single ident using the committer time of
259 : // the input commit, using the server name and timezone.
260 31 : PersonIdent ident =
261 : new PersonIdent(
262 31 : gerritIdentProvider.get(),
263 31 : merge.getCommitterIdent().getWhen(),
264 31 : gerritIdentProvider.get().getTimeZone());
265 31 : CommitBuilder cb = new CommitBuilder();
266 31 : cb.setAuthor(ident);
267 31 : cb.setCommitter(ident);
268 31 : cb.setTreeId(treeId);
269 31 : cb.setMessage(AUTO_MERGE_MSG_PREFIX + merge.name() + '\n');
270 31 : for (RevCommit p : merge.getParents()) {
271 31 : cb.addParentId(p);
272 : }
273 :
274 31 : ObjectId commitId = ins.insert(cb);
275 31 : logger.atFine().log("AutoMerge commitId=%s", commitId.name());
276 31 : ins.flush();
277 :
278 31 : if (ins instanceof InMemoryInserter) {
279 : // When using an InMemoryInserter we need to read back the values from that inserter because
280 : // they are not available.
281 0 : try (ObjectReader tmpReader = ins.newReader();
282 0 : RevWalk tmpRw = new RevWalk(tmpReader)) {
283 0 : return tmpRw.parseCommit(commitId);
284 : }
285 : }
286 :
287 31 : return rw.parseCommit(commitId);
288 : }
289 :
290 : private static class NonFlushingWrapper extends ObjectInserter.Filter {
291 : private final ObjectInserter ins;
292 :
293 0 : private NonFlushingWrapper(ObjectInserter ins) {
294 0 : this.ins = ins;
295 0 : }
296 :
297 : @Override
298 : protected ObjectInserter delegate() {
299 0 : return ins;
300 : }
301 :
302 : @Override
303 0 : public void flush() {}
304 :
305 : @Override
306 0 : public void close() {}
307 : }
308 : }
|