Line data Source code
1 : // Copyright (C) 2017 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.git;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 :
19 : import com.google.common.base.Strings;
20 : import com.google.common.collect.ImmutableListMultimap;
21 : import com.google.common.flogger.FluentLogger;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.Account;
24 : import com.google.gerrit.entities.Change;
25 : import com.google.gerrit.entities.PatchSet;
26 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
27 : import com.google.gerrit.extensions.api.changes.RevertInput;
28 : import com.google.gerrit.extensions.common.CommitInfo;
29 : import com.google.gerrit.extensions.restapi.BadRequestException;
30 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
31 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
32 : import com.google.gerrit.extensions.restapi.RestApiException;
33 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
34 : import com.google.gerrit.server.ChangeMessagesUtil;
35 : import com.google.gerrit.server.ChangeUtil;
36 : import com.google.gerrit.server.CommonConverters;
37 : import com.google.gerrit.server.CurrentUser;
38 : import com.google.gerrit.server.GerritPersonIdent;
39 : import com.google.gerrit.server.ReviewerSet;
40 : import com.google.gerrit.server.approval.ApprovalsUtil;
41 : import com.google.gerrit.server.change.ChangeInserter;
42 : import com.google.gerrit.server.change.ChangeMessages;
43 : import com.google.gerrit.server.change.NotifyResolver;
44 : import com.google.gerrit.server.extensions.events.ChangeReverted;
45 : import com.google.gerrit.server.mail.send.MessageIdGenerator;
46 : import com.google.gerrit.server.mail.send.RevertedSender;
47 : import com.google.gerrit.server.notedb.ChangeNotes;
48 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
49 : import com.google.gerrit.server.notedb.Sequences;
50 : import com.google.gerrit.server.query.change.ChangeData;
51 : import com.google.gerrit.server.query.change.InternalChangeQuery;
52 : import com.google.gerrit.server.update.BatchUpdate;
53 : import com.google.gerrit.server.update.BatchUpdateOp;
54 : import com.google.gerrit.server.update.ChangeContext;
55 : import com.google.gerrit.server.update.PostUpdateContext;
56 : import com.google.gerrit.server.update.UpdateException;
57 : import com.google.gerrit.server.util.CommitMessageUtil;
58 : import com.google.inject.Inject;
59 : import com.google.inject.Provider;
60 : import com.google.inject.Singleton;
61 : import java.io.IOException;
62 : import java.text.MessageFormat;
63 : import java.time.Instant;
64 : import java.util.ArrayList;
65 : import java.util.HashSet;
66 : import java.util.List;
67 : import java.util.Map;
68 : import java.util.Set;
69 : import org.eclipse.jgit.errors.ConfigInvalidException;
70 : import org.eclipse.jgit.errors.InvalidObjectIdException;
71 : import org.eclipse.jgit.errors.MissingObjectException;
72 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
73 : import org.eclipse.jgit.lib.CommitBuilder;
74 : import org.eclipse.jgit.lib.ObjectId;
75 : import org.eclipse.jgit.lib.ObjectInserter;
76 : import org.eclipse.jgit.lib.ObjectReader;
77 : import org.eclipse.jgit.lib.PersonIdent;
78 : import org.eclipse.jgit.lib.Ref;
79 : import org.eclipse.jgit.lib.Repository;
80 : import org.eclipse.jgit.revwalk.RevCommit;
81 : import org.eclipse.jgit.revwalk.RevWalk;
82 : import org.eclipse.jgit.util.ChangeIdUtil;
83 :
84 : /** Static utilities for working with {@link RevCommit}s. */
85 : @Singleton
86 : public class CommitUtil {
87 146 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
88 :
89 : private final GitRepositoryManager repoManager;
90 : private final Provider<PersonIdent> serverIdent;
91 : private final Sequences seq;
92 : private final ApprovalsUtil approvalsUtil;
93 : private final ChangeInserter.Factory changeInserterFactory;
94 : private final NotifyResolver notifyResolver;
95 : private final RevertedSender.Factory revertedSenderFactory;
96 : private final ChangeMessagesUtil cmUtil;
97 : private final ChangeNotes.Factory changeNotesFactory;
98 : private final ChangeReverted changeReverted;
99 : private final BatchUpdate.Factory updateFactory;
100 : private final MessageIdGenerator messageIdGenerator;
101 :
102 : @Inject
103 : CommitUtil(
104 : GitRepositoryManager repoManager,
105 : @GerritPersonIdent Provider<PersonIdent> serverIdent,
106 : Sequences seq,
107 : ApprovalsUtil approvalsUtil,
108 : ChangeInserter.Factory changeInserterFactory,
109 : NotifyResolver notifyResolver,
110 : RevertedSender.Factory revertedSenderFactory,
111 : ChangeMessagesUtil cmUtil,
112 : ChangeNotes.Factory changeNotesFactory,
113 : ChangeReverted changeReverted,
114 : BatchUpdate.Factory updateFactory,
115 145 : MessageIdGenerator messageIdGenerator) {
116 145 : this.repoManager = repoManager;
117 145 : this.serverIdent = serverIdent;
118 145 : this.seq = seq;
119 145 : this.approvalsUtil = approvalsUtil;
120 145 : this.changeInserterFactory = changeInserterFactory;
121 145 : this.notifyResolver = notifyResolver;
122 145 : this.revertedSenderFactory = revertedSenderFactory;
123 145 : this.cmUtil = cmUtil;
124 145 : this.changeNotesFactory = changeNotesFactory;
125 145 : this.changeReverted = changeReverted;
126 145 : this.updateFactory = updateFactory;
127 145 : this.messageIdGenerator = messageIdGenerator;
128 145 : }
129 :
130 : public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
131 4 : return toCommitInfo(commit, null);
132 : }
133 :
134 : public static CommitInfo toCommitInfo(RevCommit commit, @Nullable RevWalk walk)
135 : throws IOException {
136 4 : CommitInfo info = new CommitInfo();
137 4 : info.commit = commit.getName();
138 4 : info.author = CommonConverters.toGitPerson(commit.getAuthorIdent());
139 4 : info.committer = CommonConverters.toGitPerson(commit.getCommitterIdent());
140 4 : info.subject = commit.getShortMessage();
141 4 : info.message = commit.getFullMessage();
142 4 : info.parents = new ArrayList<>(commit.getParentCount());
143 4 : for (int i = 0; i < commit.getParentCount(); i++) {
144 4 : RevCommit p = walk == null ? commit.getParent(i) : walk.parseCommit(commit.getParent(i));
145 4 : CommitInfo parentInfo = new CommitInfo();
146 4 : parentInfo.commit = p.getName();
147 4 : parentInfo.subject = p.getShortMessage();
148 4 : info.parents.add(parentInfo);
149 : }
150 4 : return info;
151 : }
152 :
153 : /**
154 : * Allows creating a revert change.
155 : *
156 : * @param notes ChangeNotes of the change being reverted.
157 : * @param user Current User performing the revert.
158 : * @param input the RevertInput entity for conducting the revert.
159 : * @param timestamp timestamp for the created change.
160 : * @return ObjectId that represents the newly created commit.
161 : */
162 : public Change.Id createRevertChange(
163 : ChangeNotes notes, CurrentUser user, RevertInput input, Instant timestamp)
164 : throws RestApiException, UpdateException, ConfigInvalidException, IOException {
165 14 : String message = Strings.emptyToNull(input.message);
166 :
167 14 : try (Repository git = repoManager.openRepository(notes.getProjectName());
168 14 : ObjectInserter oi = git.newObjectInserter();
169 14 : ObjectReader reader = oi.newReader();
170 14 : RevWalk revWalk = new RevWalk(reader)) {
171 14 : ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
172 14 : ObjectId revCommit =
173 14 : createRevertCommit(message, notes, user, timestamp, oi, revWalk, generatedChangeId);
174 14 : return createRevertChangeFromCommit(
175 : revCommit, input, notes, user, generatedChangeId, timestamp, oi, revWalk, git);
176 0 : } catch (RepositoryNotFoundException e) {
177 0 : throw new ResourceNotFoundException(notes.getChangeId().toString(), e);
178 : }
179 : }
180 :
181 : /**
182 : * Wrapper function for creating a revert Commit.
183 : *
184 : * @param message Commit message for the revert commit.
185 : * @param notes ChangeNotes of the change being reverted.
186 : * @param user Current User performing the revert.
187 : * @param ts Timestamp of creation for the commit.
188 : * @return ObjectId that represents the newly created commit.
189 : */
190 : public ObjectId createRevertCommit(
191 : String message, ChangeNotes notes, CurrentUser user, Instant ts)
192 : throws RestApiException, IOException {
193 :
194 1 : try (Repository git = repoManager.openRepository(notes.getProjectName());
195 1 : ObjectInserter oi = git.newObjectInserter();
196 1 : ObjectReader reader = oi.newReader();
197 1 : RevWalk revWalk = new RevWalk(reader)) {
198 1 : return createRevertCommit(message, notes, user, ts, oi, revWalk, null);
199 0 : } catch (RepositoryNotFoundException e) {
200 0 : throw new ResourceNotFoundException(notes.getProjectName().toString(), e);
201 : }
202 : }
203 :
204 : /**
205 : * Creates a commit with the specified tree ID.
206 : *
207 : * @param oi ObjectInserter for inserting the newly created commit.
208 : * @param authorIdent of the new commit
209 : * @param committerIdent of the new commit
210 : * @param parentCommit of the new commit. Can be null.
211 : * @param commitMessage for the new commit.
212 : * @param treeId of the content for the new commit.
213 : * @return the newly created commit.
214 : * @throws IOException if fails to insert the commit.
215 : */
216 : public static ObjectId createCommitWithTree(
217 : ObjectInserter oi,
218 : PersonIdent authorIdent,
219 : PersonIdent committerIdent,
220 : @Nullable RevCommit parentCommit,
221 : String commitMessage,
222 : ObjectId treeId)
223 : throws IOException {
224 33 : logger.atFine().log("Creating commit with tree: %s", treeId.getName());
225 33 : CommitBuilder commit = new CommitBuilder();
226 33 : commit.setTreeId(treeId);
227 33 : if (parentCommit != null) {
228 28 : commit.setParentId(parentCommit);
229 : }
230 33 : commit.setAuthor(authorIdent);
231 33 : commit.setCommitter(committerIdent);
232 33 : commit.setMessage(commitMessage);
233 :
234 33 : ObjectId id = oi.insert(commit);
235 33 : oi.flush();
236 33 : return id;
237 : }
238 :
239 : /**
240 : * Creates a revert commit.
241 : *
242 : * @param message Commit message for the revert commit.
243 : * @param notes ChangeNotes of the change being reverted.
244 : * @param user Current User performing the revert.
245 : * @param ts Timestamp of creation for the commit.
246 : * @param oi ObjectInserter for inserting the newly created commit.
247 : * @param revWalk Used for parsing the original commit.
248 : * @param generatedChangeId The changeId for the commit message, can be null since it is not
249 : * needed for commits, only for changes.
250 : * @return ObjectId that represents the newly created commit.
251 : * @throws ResourceConflictException Can't revert the initial commit.
252 : * @throws IOException Thrown in case of I/O errors.
253 : */
254 : private ObjectId createRevertCommit(
255 : String message,
256 : ChangeNotes notes,
257 : CurrentUser user,
258 : Instant ts,
259 : ObjectInserter oi,
260 : RevWalk revWalk,
261 : @Nullable ObjectId generatedChangeId)
262 : throws ResourceConflictException, IOException {
263 :
264 14 : PatchSet patch = notes.getCurrentPatchSet();
265 14 : RevCommit commitToRevert = revWalk.parseCommit(patch.commitId());
266 14 : if (commitToRevert.getParentCount() == 0) {
267 1 : throw new ResourceConflictException("Cannot revert initial commit");
268 : }
269 :
270 14 : PersonIdent committerIdent = serverIdent.get();
271 14 : PersonIdent authorIdent =
272 14 : user.asIdentifiedUser().newCommitterIdent(ts, committerIdent.getZoneId());
273 :
274 14 : RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
275 14 : revWalk.parseHeaders(parentToCommitToRevert);
276 :
277 14 : Change changeToRevert = notes.getChange();
278 14 : String subject = changeToRevert.getSubject();
279 14 : if (subject.length() > 63) {
280 1 : subject = subject.substring(0, 59) + "...";
281 : }
282 14 : if (message == null) {
283 : message =
284 14 : MessageFormat.format(
285 14 : ChangeMessages.get().revertChangeDefaultMessage, subject, patch.commitId().name());
286 : }
287 14 : if (generatedChangeId != null) {
288 14 : message = ChangeIdUtil.insertId(message, generatedChangeId, true);
289 : }
290 :
291 14 : return createCommitWithTree(
292 14 : oi, authorIdent, committerIdent, commitToRevert, message, parentToCommitToRevert.getTree());
293 : }
294 :
295 : private Change.Id createRevertChangeFromCommit(
296 : ObjectId revertCommitId,
297 : RevertInput input,
298 : ChangeNotes notes,
299 : CurrentUser user,
300 : @Nullable ObjectId generatedChangeId,
301 : Instant ts,
302 : ObjectInserter oi,
303 : RevWalk revWalk,
304 : Repository git)
305 : throws IOException, RestApiException, UpdateException, ConfigInvalidException {
306 14 : RevCommit revertCommit = revWalk.parseCommit(revertCommitId);
307 14 : Change.Id changeId = Change.id(seq.nextChangeId());
308 14 : if (input.workInProgress) {
309 2 : input.notify = firstNonNull(input.notify, NotifyHandling.NONE);
310 : }
311 14 : NotifyResolver.Result notify =
312 14 : notifyResolver.resolve(firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
313 :
314 14 : Change changeToRevert = notes.getChange();
315 14 : ChangeInserter ins =
316 : changeInserterFactory
317 14 : .create(changeId, revertCommit, changeToRevert.getDest().branch())
318 14 : .setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
319 14 : ins.setMessage("Uploaded patch set 1.");
320 14 : ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
321 :
322 14 : ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
323 :
324 14 : Set<Account.Id> reviewers = new HashSet<>();
325 14 : reviewers.add(changeToRevert.getOwner());
326 14 : reviewers.addAll(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
327 14 : reviewers.remove(user.getAccountId());
328 14 : Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
329 14 : ccs.remove(user.getAccountId());
330 14 : ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
331 14 : ins.setRevertOf(notes.getChangeId());
332 14 : ins.setWorkInProgress(input.workInProgress);
333 :
334 14 : try (BatchUpdate bu = updateFactory.create(notes.getProjectName(), user, ts)) {
335 14 : bu.setRepository(git, revWalk, oi);
336 14 : bu.setNotify(notify);
337 14 : bu.insertChange(ins);
338 14 : if (!input.workInProgress) {
339 13 : addChangeRevertedNotificationOps(
340 13 : bu, changeToRevert.getId(), changeId, generatedChangeId.name());
341 : }
342 14 : bu.execute();
343 : }
344 14 : return changeId;
345 : }
346 :
347 : private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
348 : @Nullable Map<String, String> validationOptions) {
349 14 : if (validationOptions == null) {
350 14 : return ImmutableListMultimap.of();
351 : }
352 :
353 : ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
354 1 : ImmutableListMultimap.builder();
355 1 : validationOptions
356 1 : .entrySet()
357 1 : .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
358 1 : return validationOptionsBuilder.build();
359 : }
360 :
361 : /**
362 : * Notify the owners of a change that their change is being reverted.
363 : *
364 : * @param bu to append the notification actions to.
365 : * @param revertedChangeId to be notified.
366 : * @param revertingChangeId to notify about.
367 : * @param revertingChangeKey to notify about.
368 : */
369 : public void addChangeRevertedNotificationOps(
370 : BatchUpdate bu,
371 : Change.Id revertedChangeId,
372 : Change.Id revertingChangeId,
373 : String revertingChangeKey) {
374 14 : bu.addOp(revertingChangeId, new ChangeRevertedNotifyOp(revertedChangeId, revertingChangeId));
375 14 : bu.addOp(revertedChangeId, new PostRevertedMessageOp(revertingChangeKey));
376 14 : }
377 :
378 : private class ChangeRevertedNotifyOp implements BatchUpdateOp {
379 : private final Change.Id revertedChangeId;
380 : private final Change.Id revertingChangeId;
381 :
382 14 : ChangeRevertedNotifyOp(Change.Id revertedChangeId, Change.Id revertingChangeId) {
383 14 : this.revertedChangeId = revertedChangeId;
384 14 : this.revertingChangeId = revertingChangeId;
385 14 : }
386 :
387 : @Override
388 : public void postUpdate(PostUpdateContext ctx) throws Exception {
389 14 : ChangeData revertedChange =
390 14 : ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertedChangeId));
391 14 : ChangeData revertingChange =
392 14 : ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertingChangeId));
393 14 : changeReverted.fire(revertedChange, revertingChange, ctx.getWhen());
394 : try {
395 14 : RevertedSender emailSender =
396 14 : revertedSenderFactory.create(ctx.getProject(), revertedChange.getId());
397 14 : emailSender.setFrom(ctx.getAccountId());
398 14 : emailSender.setNotify(ctx.getNotify(revertedChangeId));
399 14 : emailSender.setMessageId(
400 14 : messageIdGenerator.fromChangeUpdate(
401 14 : ctx.getRepoView(), revertedChange.currentPatchSet().id()));
402 14 : emailSender.send();
403 0 : } catch (Exception err) {
404 0 : logger.atSevere().withCause(err).log(
405 : "Cannot send email for revert change %s", revertedChangeId);
406 14 : }
407 14 : }
408 : }
409 :
410 : private class PostRevertedMessageOp implements BatchUpdateOp {
411 : private final String revertingChangeKey;
412 :
413 14 : PostRevertedMessageOp(String revertingChangeKey) {
414 14 : this.revertingChangeKey = revertingChangeKey;
415 14 : }
416 :
417 : @Override
418 : public boolean updateChange(ChangeContext ctx) {
419 14 : cmUtil.setChangeMessage(
420 : ctx,
421 : "Created a revert of this change as I" + revertingChangeKey,
422 : ChangeMessagesUtil.TAG_REVERT);
423 14 : return true;
424 : }
425 : }
426 :
427 : /**
428 : * Returns the parent commit for a new commit.
429 : *
430 : * <p>If {@code baseSha1} is provided, the method verifies it can be used as a base. If {@code
431 : * baseSha1} is not provided the tip of the {@code destRef} is returned.
432 : *
433 : * @param project The name of the project.
434 : * @param changeQuery Used for looking up the base commit.
435 : * @param revWalk Used for parsing the base commit.
436 : * @param destRef The destination branch.
437 : * @param baseSha1 The hash of the base commit. Nullable.
438 : * @return the base commit. Either the commit matching the provided hash, or the direct parent if
439 : * a hash was not provided.
440 : * @throws IOException if the branch reference cannot be parsed.
441 : * @throws RestApiException if the base commit cannot be fetched.
442 : */
443 : public static RevCommit getBaseCommit(
444 : String project,
445 : InternalChangeQuery changeQuery,
446 : RevWalk revWalk,
447 : Ref destRef,
448 : @Nullable String baseSha1)
449 : throws IOException, RestApiException {
450 9 : RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId());
451 : // The tip commit of the destination ref is the default base for the newly created change.
452 9 : if (Strings.isNullOrEmpty(baseSha1)) {
453 8 : return destRefTip;
454 : }
455 :
456 : ObjectId baseObjectId;
457 : try {
458 4 : baseObjectId = ObjectId.fromString(baseSha1);
459 1 : } catch (InvalidObjectIdException e) {
460 1 : throw new BadRequestException(
461 1 : String.format("Base %s doesn't represent a valid SHA-1", baseSha1), e);
462 4 : }
463 :
464 : RevCommit baseCommit;
465 : try {
466 4 : baseCommit = revWalk.parseCommit(baseObjectId);
467 1 : } catch (MissingObjectException e) {
468 1 : throw new UnprocessableEntityException(
469 1 : String.format("Base %s doesn't exist", baseObjectId.name()), e);
470 4 : }
471 :
472 4 : changeQuery.enforceVisibility(true);
473 4 : List<ChangeData> changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), baseSha1);
474 :
475 4 : if (changeDatas.isEmpty()) {
476 3 : if (revWalk.isMergedInto(baseCommit, destRefTip)) {
477 : // The base commit is a merged commit with no change associated.
478 3 : return baseCommit;
479 : }
480 1 : throw new UnprocessableEntityException(
481 1 : String.format("Commit %s does not exist on branch %s", baseSha1, destRef.getName()));
482 3 : } else if (changeDatas.size() != 1) {
483 0 : throw new ResourceConflictException("Multiple changes found for commit " + baseSha1);
484 : }
485 :
486 3 : Change change = changeDatas.get(0).change();
487 3 : if (!change.isAbandoned()) {
488 : // The base commit is a valid change revision.
489 3 : return baseCommit;
490 : }
491 :
492 1 : throw new ResourceConflictException(
493 1 : String.format(
494 : "Change %s with commit %s is %s",
495 1 : change.getChangeId(), baseSha1, ChangeUtil.status(change)));
496 : }
497 : }
|