Line data Source code
1 : // Copyright (C) 2014 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.edit;
16 :
17 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
18 :
19 : import com.google.common.base.Charsets;
20 : import com.google.gerrit.common.Nullable;
21 : import com.google.gerrit.entities.BooleanProjectConfig;
22 : import com.google.gerrit.entities.Change;
23 : import com.google.gerrit.entities.PatchSet;
24 : import com.google.gerrit.entities.Project;
25 : import com.google.gerrit.entities.RefNames;
26 : import com.google.gerrit.extensions.restapi.AuthException;
27 : import com.google.gerrit.extensions.restapi.BadRequestException;
28 : import com.google.gerrit.extensions.restapi.MergeConflictException;
29 : import com.google.gerrit.extensions.restapi.RawInput;
30 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
31 : import com.google.gerrit.git.LockFailureException;
32 : import com.google.gerrit.server.ChangeUtil;
33 : import com.google.gerrit.server.CurrentUser;
34 : import com.google.gerrit.server.GerritPersonIdent;
35 : import com.google.gerrit.server.IdentifiedUser;
36 : import com.google.gerrit.server.PatchSetUtil;
37 : import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
38 : import com.google.gerrit.server.edit.tree.DeleteFileModification;
39 : import com.google.gerrit.server.edit.tree.RenameFileModification;
40 : import com.google.gerrit.server.edit.tree.RestoreFileModification;
41 : import com.google.gerrit.server.edit.tree.TreeCreator;
42 : import com.google.gerrit.server.edit.tree.TreeModification;
43 : import com.google.gerrit.server.index.change.ChangeIndexer;
44 : import com.google.gerrit.server.notedb.ChangeNotes;
45 : import com.google.gerrit.server.permissions.ChangePermission;
46 : import com.google.gerrit.server.permissions.PermissionBackend;
47 : import com.google.gerrit.server.permissions.PermissionBackendException;
48 : import com.google.gerrit.server.project.InvalidChangeOperationException;
49 : import com.google.gerrit.server.project.ProjectCache;
50 : import com.google.gerrit.server.util.CommitMessageUtil;
51 : import com.google.gerrit.server.util.time.TimeUtil;
52 : import com.google.inject.Inject;
53 : import com.google.inject.Provider;
54 : import com.google.inject.Singleton;
55 : import java.io.IOException;
56 : import java.time.Instant;
57 : import java.time.ZoneId;
58 : import java.util.List;
59 : import java.util.Objects;
60 : import java.util.Optional;
61 : import org.eclipse.jgit.diff.DiffAlgorithm;
62 : import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
63 : import org.eclipse.jgit.diff.RawText;
64 : import org.eclipse.jgit.diff.RawTextComparator;
65 : import org.eclipse.jgit.dircache.InvalidPathException;
66 : import org.eclipse.jgit.lib.BatchRefUpdate;
67 : import org.eclipse.jgit.lib.CommitBuilder;
68 : import org.eclipse.jgit.lib.NullProgressMonitor;
69 : import org.eclipse.jgit.lib.ObjectId;
70 : import org.eclipse.jgit.lib.ObjectInserter;
71 : import org.eclipse.jgit.lib.PersonIdent;
72 : import org.eclipse.jgit.lib.RefUpdate;
73 : import org.eclipse.jgit.lib.Repository;
74 : import org.eclipse.jgit.merge.MergeAlgorithm;
75 : import org.eclipse.jgit.merge.MergeChunk;
76 : import org.eclipse.jgit.merge.MergeResult;
77 : import org.eclipse.jgit.merge.MergeStrategy;
78 : import org.eclipse.jgit.merge.ThreeWayMerger;
79 : import org.eclipse.jgit.revwalk.RevCommit;
80 : import org.eclipse.jgit.revwalk.RevTree;
81 : import org.eclipse.jgit.revwalk.RevWalk;
82 : import org.eclipse.jgit.transport.ReceiveCommand;
83 :
84 : /**
85 : * Utility functions to manipulate change edits.
86 : *
87 : * <p>This class contains methods to modify edit's content. For retrieving, publishing and deleting
88 : * edit see {@link ChangeEditUtil}.
89 : *
90 : * <p>
91 : */
92 : @Singleton
93 : public class ChangeEditModifier {
94 :
95 : private final ZoneId zoneId;
96 : private final Provider<CurrentUser> currentUser;
97 : private final PermissionBackend permissionBackend;
98 : private final ChangeEditUtil changeEditUtil;
99 : private final PatchSetUtil patchSetUtil;
100 : private final ProjectCache projectCache;
101 : private final NoteDbEdits noteDbEdits;
102 :
103 : @Inject
104 : ChangeEditModifier(
105 : @GerritPersonIdent PersonIdent gerritIdent,
106 : ChangeIndexer indexer,
107 : Provider<CurrentUser> currentUser,
108 : PermissionBackend permissionBackend,
109 : ChangeEditUtil changeEditUtil,
110 : PatchSetUtil patchSetUtil,
111 145 : ProjectCache projectCache) {
112 145 : this.currentUser = currentUser;
113 145 : this.permissionBackend = permissionBackend;
114 145 : this.zoneId = gerritIdent.getZoneId();
115 145 : this.changeEditUtil = changeEditUtil;
116 145 : this.patchSetUtil = patchSetUtil;
117 145 : this.projectCache = projectCache;
118 :
119 145 : noteDbEdits = new NoteDbEdits(zoneId, indexer, currentUser);
120 145 : }
121 :
122 : /**
123 : * Creates a new change edit.
124 : *
125 : * @param repository the affected Git repository
126 : * @param notes the {@link ChangeNotes} of the change for which the change edit should be created
127 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
128 : * @throws InvalidChangeOperationException if a change edit already existed for the change
129 : */
130 : public void createEdit(Repository repository, ChangeNotes notes)
131 : throws AuthException, IOException, InvalidChangeOperationException,
132 : PermissionBackendException, ResourceConflictException {
133 18 : assertCanEdit(notes);
134 :
135 18 : Optional<ChangeEdit> changeEdit = lookupChangeEdit(notes);
136 18 : if (changeEdit.isPresent()) {
137 1 : throw new InvalidChangeOperationException(
138 1 : String.format("A change edit already exists for change %s", notes.getChangeId()));
139 : }
140 :
141 18 : PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
142 18 : ObjectId patchSetCommitId = currentPatchSet.commitId();
143 18 : noteDbEdits.createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.now());
144 18 : }
145 :
146 : /**
147 : * Rebase change edit on latest patch set
148 : *
149 : * @param repository the affected Git repository
150 : * @param notes the {@link ChangeNotes} of the change whose change edit should be rebased
151 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
152 : * @throws InvalidChangeOperationException if a change edit doesn't exist for the specified
153 : * change, the change edit is already based on the latest patch set, or the change represents
154 : * the root commit
155 : */
156 : public void rebaseEdit(Repository repository, ChangeNotes notes)
157 : throws AuthException, InvalidChangeOperationException, IOException,
158 : PermissionBackendException, ResourceConflictException {
159 2 : assertCanEdit(notes);
160 :
161 2 : Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
162 2 : if (!optionalChangeEdit.isPresent()) {
163 0 : throw new InvalidChangeOperationException(
164 0 : String.format("No change edit exists for change %s", notes.getChangeId()));
165 : }
166 2 : ChangeEdit changeEdit = optionalChangeEdit.get();
167 :
168 2 : PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
169 2 : if (isBasedOn(changeEdit, currentPatchSet)) {
170 1 : throw new InvalidChangeOperationException(
171 1 : String.format(
172 : "Change edit for change %s is already based on latest patch set %s",
173 1 : notes.getChangeId(), currentPatchSet.id()));
174 : }
175 :
176 1 : rebase(repository, changeEdit, currentPatchSet);
177 1 : }
178 :
179 : private void rebase(Repository repository, ChangeEdit changeEdit, PatchSet currentPatchSet)
180 : throws IOException, MergeConflictException, InvalidChangeOperationException {
181 1 : RevCommit currentEditCommit = changeEdit.getEditCommit();
182 1 : if (currentEditCommit.getParentCount() == 0) {
183 0 : throw new InvalidChangeOperationException(
184 : "Rebase change edit against root commit not supported");
185 : }
186 :
187 1 : RevCommit basePatchSetCommit = NoteDbEdits.lookupCommit(repository, currentPatchSet.commitId());
188 1 : RevTree basePatchSetTree = basePatchSetCommit.getTree();
189 :
190 1 : ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree);
191 1 : Instant nowTimestamp = TimeUtil.now();
192 1 : String commitMessage = currentEditCommit.getFullMessage();
193 1 : ObjectId newEditCommitId =
194 1 : createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
195 :
196 1 : noteDbEdits.baseEditOnDifferentPatchset(
197 : repository, changeEdit, currentPatchSet, currentEditCommit, newEditCommitId, nowTimestamp);
198 1 : }
199 :
200 : /**
201 : * Modifies the commit message of a change edit. If the change edit doesn't exist, a new one will
202 : * be created based on the current patch set.
203 : *
204 : * @param repository the affected Git repository
205 : * @param notes the {@link ChangeNotes} of the change whose change edit's message should be
206 : * modified
207 : * @param newCommitMessage the new commit message
208 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
209 : * @throws InvalidChangeOperationException if the commit message is the same as before
210 : * @throws BadRequestException if the commit message is malformed
211 : */
212 : public void modifyMessage(Repository repository, ChangeNotes notes, String newCommitMessage)
213 : throws AuthException, IOException, InvalidChangeOperationException,
214 : PermissionBackendException, BadRequestException, ResourceConflictException {
215 6 : modifyCommit(
216 : repository,
217 : notes,
218 : new ModificationIntention.LatestCommit(),
219 6 : CommitModification.builder().newCommitMessage(newCommitMessage).build());
220 6 : }
221 :
222 : /**
223 : * Modifies the contents of a file of a change edit. If the change edit doesn't exist, a new one
224 : * will be created based on the current patch set.
225 : *
226 : * @param repository the affected Git repository
227 : * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
228 : * @param filePath the path of the file whose contents should be modified
229 : * @param newContent the new file content
230 : * @param newGitFileMode the new file mode in octal format. {@code null} indicates no change
231 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
232 : * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
233 : * @throws InvalidChangeOperationException if the file already had the specified content
234 : * @throws ResourceConflictException if the project state does not permit the operation
235 : */
236 : public void modifyFile(
237 : Repository repository,
238 : ChangeNotes notes,
239 : String filePath,
240 : RawInput newContent,
241 : @Nullable Integer newGitFileMode)
242 : throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
243 : PermissionBackendException, ResourceConflictException {
244 18 : modifyCommit(
245 : repository,
246 : notes,
247 : new ModificationIntention.LatestCommit(),
248 18 : CommitModification.builder()
249 18 : .addTreeModification(
250 : new ChangeFileContentModification(filePath, newContent, newGitFileMode))
251 18 : .build());
252 18 : }
253 :
254 : /**
255 : * Deletes a file from the Git tree of a change edit. If the change edit doesn't exist, a new one
256 : * will be created based on the current patch set.
257 : *
258 : * @param repository the affected Git repository
259 : * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
260 : * @param file path of the file which should be deleted
261 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
262 : * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
263 : * @throws InvalidChangeOperationException if the file does not exist
264 : * @throws ResourceConflictException if the project state does not permit the operation
265 : */
266 : public void deleteFile(Repository repository, ChangeNotes notes, String file)
267 : throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
268 : PermissionBackendException, ResourceConflictException {
269 7 : modifyCommit(
270 : repository,
271 : notes,
272 : new ModificationIntention.LatestCommit(),
273 7 : CommitModification.builder().addTreeModification(new DeleteFileModification(file)).build());
274 6 : }
275 :
276 : /**
277 : * Renames a file of a change edit or moves it to another directory. If the change edit doesn't
278 : * exist, a new one will be created based on the current patch set.
279 : *
280 : * @param repository the affected Git repository
281 : * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
282 : * @param currentFilePath the current path/name of the file
283 : * @param newFilePath the desired path/name of the file
284 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
285 : * @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
286 : * @throws InvalidChangeOperationException if the file was already renamed to the specified new
287 : * name
288 : * @throws ResourceConflictException if the project state does not permit the operation
289 : */
290 : public void renameFile(
291 : Repository repository, ChangeNotes notes, String currentFilePath, String newFilePath)
292 : throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
293 : PermissionBackendException, ResourceConflictException {
294 3 : modifyCommit(
295 : repository,
296 : notes,
297 : new ModificationIntention.LatestCommit(),
298 3 : CommitModification.builder()
299 3 : .addTreeModification(new RenameFileModification(currentFilePath, newFilePath))
300 3 : .build());
301 3 : }
302 :
303 : /**
304 : * Restores a file of a change edit to the state it was in before the patch set on which the
305 : * change edit is based. If the change edit doesn't exist, a new one will be created based on the
306 : * current patch set.
307 : *
308 : * @param repository the affected Git repository
309 : * @param notes the {@link ChangeNotes} of the change whose change edit should be modified
310 : * @param file the path of the file which should be restored
311 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
312 : * @throws InvalidChangeOperationException if the file was already restored
313 : */
314 : public void restoreFile(Repository repository, ChangeNotes notes, String file)
315 : throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
316 : PermissionBackendException, ResourceConflictException {
317 1 : modifyCommit(
318 : repository,
319 : notes,
320 : new ModificationIntention.LatestCommit(),
321 1 : CommitModification.builder()
322 1 : .addTreeModification(new RestoreFileModification(file))
323 1 : .build());
324 1 : }
325 :
326 : /**
327 : * Applies the indicated modifications to the specified patch set. If a change edit exists and is
328 : * based on the same patch set, the modified patch set tree is merged with the change edit. If the
329 : * change edit doesn't exist, a new one will be created.
330 : *
331 : * @param repository the affected Git repository
332 : * @param notes the {@link ChangeNotes} of the change to which the patch set belongs
333 : * @param patchSet the {@code PatchSet} which should be modified
334 : * @param commitModification the modifications which should be applied
335 : * @return the resulting {@code ChangeEdit}
336 : * @throws AuthException if the user isn't authenticated or not allowed to use change edits
337 : * @throws InvalidChangeOperationException if the existing change edit is based on another patch
338 : * set or no change edit exists but the specified patch set isn't the current one
339 : * @throws MergeConflictException if the modified patch set tree can't be merged with an existing
340 : * change edit
341 : */
342 : public ChangeEdit combineWithModifiedPatchSetTree(
343 : Repository repository,
344 : ChangeNotes notes,
345 : PatchSet patchSet,
346 : CommitModification commitModification)
347 : throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
348 : PermissionBackendException, ResourceConflictException {
349 3 : return modifyCommit(
350 : repository, notes, new ModificationIntention.PatchsetCommit(patchSet), commitModification);
351 : }
352 :
353 : private ChangeEdit modifyCommit(
354 : Repository repository,
355 : ChangeNotes notes,
356 : ModificationIntention modificationIntention,
357 : CommitModification commitModification)
358 : throws AuthException, BadRequestException, IOException, InvalidChangeOperationException,
359 : PermissionBackendException, ResourceConflictException {
360 22 : assertCanEdit(notes);
361 :
362 22 : Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(notes);
363 22 : EditBehavior editBehavior =
364 : optionalChangeEdit
365 22 : .<EditBehavior>map(changeEdit -> new ExistingEditBehavior(changeEdit, noteDbEdits))
366 22 : .orElseGet(() -> new NewEditBehavior(noteDbEdits));
367 22 : ModificationTarget modificationTarget =
368 22 : editBehavior.getModificationTarget(notes, modificationIntention);
369 :
370 22 : RevCommit commitToModify = modificationTarget.getCommit(repository);
371 22 : ObjectId newTreeId =
372 21 : createNewTree(repository, commitToModify, commitModification.treeModifications());
373 21 : newTreeId = editBehavior.mergeTreesIfNecessary(repository, newTreeId, commitToModify);
374 :
375 21 : PatchSet basePatchset = modificationTarget.getBasePatchset();
376 21 : RevCommit basePatchsetCommit = NoteDbEdits.lookupCommit(repository, basePatchset.commitId());
377 :
378 21 : boolean changeIdRequired =
379 : projectCache
380 21 : .get(notes.getChange().getProject())
381 21 : .orElseThrow(illegalState(notes.getChange().getProject()))
382 21 : .is(BooleanProjectConfig.REQUIRE_CHANGE_ID);
383 21 : String currentChangeId = notes.getChange().getKey().get();
384 21 : String newCommitMessage =
385 21 : createNewCommitMessage(
386 : changeIdRequired, currentChangeId, editBehavior, commitModification, commitToModify);
387 21 : newCommitMessage = editBehavior.mergeCommitMessageIfNecessary(newCommitMessage, commitToModify);
388 :
389 21 : Optional<ChangeEdit> unmodifiedEdit =
390 21 : editBehavior.getEditIfNoModification(newTreeId, newCommitMessage);
391 21 : if (unmodifiedEdit.isPresent()) {
392 2 : return unmodifiedEdit.get();
393 : }
394 :
395 21 : Instant nowTimestamp = TimeUtil.now();
396 21 : ObjectId newEditCommit =
397 21 : createCommit(repository, basePatchsetCommit, newTreeId, newCommitMessage, nowTimestamp);
398 :
399 21 : return editBehavior.updateEditInStorage(
400 : repository, notes, basePatchset, newEditCommit, nowTimestamp);
401 : }
402 :
403 : private void assertCanEdit(ChangeNotes notes)
404 : throws AuthException, PermissionBackendException, ResourceConflictException {
405 24 : if (!currentUser.get().isIdentifiedUser()) {
406 0 : throw new AuthException("Authentication required");
407 : }
408 :
409 24 : Change c = notes.getChange();
410 24 : if (!c.isNew()) {
411 1 : throw new ResourceConflictException(
412 1 : String.format("change %s is %s", c.getChangeId(), ChangeUtil.status(c)));
413 : }
414 :
415 : // Not allowed to edit if the current patch set is locked.
416 24 : patchSetUtil.checkPatchSetNotLocked(notes);
417 24 : boolean canEdit =
418 24 : permissionBackend.currentUser().change(notes).test(ChangePermission.ADD_PATCH_SET);
419 24 : canEdit &=
420 : projectCache
421 24 : .get(notes.getProjectName())
422 24 : .orElseThrow(illegalState(notes.getProjectName()))
423 24 : .statePermitsWrite();
424 24 : if (!canEdit) {
425 2 : throw new AuthException("edit not permitted");
426 : }
427 24 : }
428 :
429 : private Optional<ChangeEdit> lookupChangeEdit(ChangeNotes notes)
430 : throws AuthException, IOException {
431 24 : return changeEditUtil.byChange(notes);
432 : }
433 :
434 : private PatchSet lookupCurrentPatchSet(ChangeNotes notes) {
435 18 : return patchSetUtil.current(notes);
436 : }
437 :
438 : private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
439 2 : PatchSet editBasePatchSet = changeEdit.getBasePatchSet();
440 2 : return editBasePatchSet.id().equals(patchSet.id());
441 : }
442 :
443 : private static ObjectId createNewTree(
444 : Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications)
445 : throws BadRequestException, IOException, InvalidChangeOperationException {
446 22 : if (treeModifications.isEmpty()) {
447 6 : return baseCommit.getTree();
448 : }
449 :
450 : ObjectId newTreeId;
451 : try {
452 21 : TreeCreator treeCreator = TreeCreator.basedOn(baseCommit);
453 21 : treeCreator.addTreeModifications(treeModifications);
454 21 : newTreeId = treeCreator.createNewTreeAndGetId(repository);
455 1 : } catch (InvalidPathException e) {
456 1 : throw new BadRequestException(e.getMessage());
457 21 : }
458 :
459 21 : if (ObjectId.isEqual(newTreeId, baseCommit.getTree())) {
460 2 : throw new InvalidChangeOperationException("no changes were made");
461 : }
462 20 : return newTreeId;
463 : }
464 :
465 : private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId)
466 : throws IOException, MergeConflictException {
467 3 : PatchSet basePatchSet = changeEdit.getBasePatchSet();
468 3 : ObjectId basePatchSetCommitId = basePatchSet.commitId();
469 3 : ObjectId editCommitId = changeEdit.getEditCommit();
470 :
471 3 : ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repository, true);
472 3 : threeWayMerger.setBase(basePatchSetCommitId);
473 3 : boolean successful = threeWayMerger.merge(newTreeId, editCommitId);
474 :
475 3 : if (!successful) {
476 2 : throw new MergeConflictException(
477 : "The existing change edit could not be merged with another tree.");
478 : }
479 3 : return threeWayMerger.getResultTreeId();
480 : }
481 :
482 : private String createNewCommitMessage(
483 : boolean requireChangeId,
484 : String currentChangeId,
485 : EditBehavior editBehavior,
486 : CommitModification commitModification,
487 : RevCommit commitToModify)
488 : throws InvalidChangeOperationException, BadRequestException, ResourceConflictException {
489 21 : if (!commitModification.newCommitMessage().isPresent()) {
490 20 : return editBehavior.getUnmodifiedCommitMessage(commitToModify);
491 : }
492 :
493 6 : String newCommitMessage =
494 6 : CommitMessageUtil.checkAndSanitizeCommitMessage(
495 6 : commitModification.newCommitMessage().get());
496 :
497 6 : if (newCommitMessage.equals(commitToModify.getFullMessage())) {
498 1 : throw new InvalidChangeOperationException(
499 : "New commit message cannot be same as existing commit message");
500 : }
501 :
502 6 : ChangeUtil.ensureChangeIdIsCorrect(requireChangeId, currentChangeId, newCommitMessage);
503 :
504 6 : return newCommitMessage;
505 : }
506 :
507 : private ObjectId createCommit(
508 : Repository repository,
509 : RevCommit basePatchsetCommit,
510 : ObjectId tree,
511 : String commitMessage,
512 : Instant timestamp)
513 : throws IOException {
514 21 : try (ObjectInserter objectInserter = repository.newObjectInserter()) {
515 21 : CommitBuilder builder = new CommitBuilder();
516 21 : builder.setTreeId(tree);
517 21 : builder.setParentIds(basePatchsetCommit.getParents());
518 21 : builder.setAuthor(basePatchsetCommit.getAuthorIdent());
519 21 : builder.setCommitter(getCommitterIdent(timestamp));
520 21 : builder.setMessage(commitMessage);
521 21 : ObjectId newCommitId = objectInserter.insert(builder);
522 21 : objectInserter.flush();
523 21 : return newCommitId;
524 : }
525 : }
526 :
527 : private PersonIdent getCommitterIdent(Instant commitTimestamp) {
528 21 : IdentifiedUser user = currentUser.get().asIdentifiedUser();
529 21 : return user.newCommitterIdent(commitTimestamp, zoneId);
530 : }
531 :
532 : /**
533 : * Strategy to apply depending on the current situation regarding change edits (e.g. creating a
534 : * new edit requires different storage modifications than updating an existing edit).
535 : */
536 : private interface EditBehavior {
537 :
538 : ModificationTarget getModificationTarget(
539 : ChangeNotes notes, ModificationIntention targetIntention)
540 : throws InvalidChangeOperationException;
541 :
542 : ObjectId mergeTreesIfNecessary(
543 : Repository repository, ObjectId newTreeId, ObjectId commitToModify)
544 : throws IOException, MergeConflictException;
545 :
546 : String getUnmodifiedCommitMessage(RevCommit commitToModify);
547 :
548 : String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify)
549 : throws MergeConflictException;
550 :
551 : Optional<ChangeEdit> getEditIfNoModification(ObjectId newTreeId, String newCommitMessage);
552 :
553 : ChangeEdit updateEditInStorage(
554 : Repository repository,
555 : ChangeNotes notes,
556 : PatchSet basePatchSet,
557 : ObjectId newEditCommitId,
558 : Instant timestamp)
559 : throws IOException;
560 : }
561 :
562 : private static class ExistingEditBehavior implements EditBehavior {
563 :
564 : private final ChangeEdit changeEdit;
565 : private final NoteDbEdits noteDbEdits;
566 :
567 13 : ExistingEditBehavior(ChangeEdit changeEdit, NoteDbEdits noteDbEdits) {
568 13 : this.changeEdit = changeEdit;
569 13 : this.noteDbEdits = noteDbEdits;
570 13 : }
571 :
572 : @Override
573 : public ModificationTarget getModificationTarget(
574 : ChangeNotes notes, ModificationIntention targetIntention)
575 : throws InvalidChangeOperationException {
576 13 : ModificationTarget modificationTarget = targetIntention.getTargetWhenEditExists(changeEdit);
577 : // It would be better to do this validation in the implementation of the REST endpoints
578 : // before calling any write actions on ChangeEditModifier.
579 13 : modificationTarget.ensureTargetMayBeModifiedDespiteExistingEdit(changeEdit);
580 13 : return modificationTarget;
581 : }
582 :
583 : @Override
584 : public ObjectId mergeTreesIfNecessary(
585 : Repository repository, ObjectId newTreeId, ObjectId commitToModify)
586 : throws IOException, MergeConflictException {
587 12 : if (ObjectId.isEqual(changeEdit.getEditCommit(), commitToModify)) {
588 12 : return newTreeId;
589 : }
590 2 : return merge(repository, changeEdit, newTreeId);
591 : }
592 :
593 : @Override
594 : public String getUnmodifiedCommitMessage(RevCommit commitToModify) {
595 11 : return changeEdit.getEditCommit().getFullMessage();
596 : }
597 :
598 : @Override
599 : public String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify)
600 : throws MergeConflictException {
601 12 : if (ObjectId.isEqual(changeEdit.getEditCommit(), commitToModify)) {
602 12 : return newCommitMessage;
603 : }
604 2 : String editCommitMessage = changeEdit.getEditCommit().getFullMessage();
605 2 : if (editCommitMessage.equals(newCommitMessage)) {
606 2 : return editCommitMessage;
607 : }
608 2 : return mergeCommitMessage(newCommitMessage, commitToModify, editCommitMessage);
609 : }
610 :
611 : private String mergeCommitMessage(
612 : String newCommitMessage, RevCommit commitToModify, String editCommitMessage)
613 : throws MergeConflictException {
614 2 : MergeAlgorithm mergeAlgorithm =
615 2 : new MergeAlgorithm(DiffAlgorithm.getAlgorithm(SupportedAlgorithm.MYERS));
616 2 : RawText baseMessage = new RawText(commitToModify.getFullMessage().getBytes(Charsets.UTF_8));
617 2 : RawText oldMessage = new RawText(editCommitMessage.getBytes(Charsets.UTF_8));
618 2 : RawText newMessage = new RawText(newCommitMessage.getBytes(Charsets.UTF_8));
619 2 : RawTextComparator textComparator = RawTextComparator.DEFAULT;
620 2 : MergeResult<RawText> mergeResult =
621 2 : mergeAlgorithm.merge(textComparator, baseMessage, oldMessage, newMessage);
622 2 : if (mergeResult.containsConflicts()) {
623 1 : throw new MergeConflictException(
624 : "The chosen modification adjusted the commit message. However, the new commit message"
625 : + " could not be merged with the commit message of the existing change edit."
626 : + " Please manually apply the desired changes to the commit message of the change"
627 : + " edit.");
628 : }
629 :
630 2 : StringBuilder resultingCommitMessage = new StringBuilder();
631 2 : for (MergeChunk mergeChunk : mergeResult) {
632 2 : RawText mergedMessagePart = mergeResult.getSequences().get(mergeChunk.getSequenceIndex());
633 2 : resultingCommitMessage.append(
634 2 : mergedMessagePart.getString(mergeChunk.getBegin(), mergeChunk.getEnd(), false));
635 2 : }
636 2 : return resultingCommitMessage.toString();
637 : }
638 :
639 : @Override
640 : public Optional<ChangeEdit> getEditIfNoModification(
641 : ObjectId newTreeId, String newCommitMessage) {
642 12 : if (!ObjectId.isEqual(newTreeId, changeEdit.getEditCommit().getTree())) {
643 11 : return Optional.empty();
644 : }
645 4 : if (!Objects.equals(newCommitMessage, changeEdit.getEditCommit().getFullMessage())) {
646 4 : return Optional.empty();
647 : }
648 : // Modifications are already contained in the change edit.
649 2 : return Optional.of(changeEdit);
650 : }
651 :
652 : @Override
653 : public ChangeEdit updateEditInStorage(
654 : Repository repository,
655 : ChangeNotes notes,
656 : PatchSet basePatchSet,
657 : ObjectId newEditCommitId,
658 : Instant timestamp)
659 : throws IOException {
660 12 : return noteDbEdits.updateEdit(
661 12 : notes.getProjectName(), repository, changeEdit, newEditCommitId, timestamp);
662 : }
663 : }
664 :
665 : private static class NewEditBehavior implements EditBehavior {
666 :
667 : private final NoteDbEdits noteDbEdits;
668 :
669 17 : NewEditBehavior(NoteDbEdits noteDbEdits) {
670 17 : this.noteDbEdits = noteDbEdits;
671 17 : }
672 :
673 : @Override
674 : public ModificationTarget getModificationTarget(
675 : ChangeNotes notes, ModificationIntention targetIntention)
676 : throws InvalidChangeOperationException {
677 17 : ModificationTarget modificationTarget = targetIntention.getTargetWhenNoEdit(notes);
678 : // It would be better to do this validation in the implementation of the REST endpoints
679 : // before calling any write actions on ChangeEditModifier.
680 17 : modificationTarget.ensureNewEditMayBeBasedOnTarget(notes.getChange());
681 17 : return modificationTarget;
682 : }
683 :
684 : @Override
685 : public ObjectId mergeTreesIfNecessary(
686 : Repository repository, ObjectId newTreeId, ObjectId commitToModify) {
687 17 : return newTreeId;
688 : }
689 :
690 : @Override
691 : public String getUnmodifiedCommitMessage(RevCommit commitToModify) {
692 17 : return commitToModify.getFullMessage();
693 : }
694 :
695 : @Override
696 : public String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify) {
697 17 : return newCommitMessage;
698 : }
699 :
700 : @Override
701 : public Optional<ChangeEdit> getEditIfNoModification(
702 : ObjectId newTreeId, String newCommitMessage) {
703 17 : return Optional.empty();
704 : }
705 :
706 : @Override
707 : public ChangeEdit updateEditInStorage(
708 : Repository repository,
709 : ChangeNotes notes,
710 : PatchSet basePatchSet,
711 : ObjectId newEditCommitId,
712 : Instant timestamp)
713 : throws IOException {
714 17 : return noteDbEdits.createEdit(repository, notes, basePatchSet, newEditCommitId, timestamp);
715 : }
716 : }
717 :
718 : private static class NoteDbEdits {
719 : private final ZoneId zoneId;
720 : private final ChangeIndexer indexer;
721 : private final Provider<CurrentUser> currentUser;
722 :
723 145 : NoteDbEdits(ZoneId zoneId, ChangeIndexer indexer, Provider<CurrentUser> currentUser) {
724 145 : this.zoneId = zoneId;
725 145 : this.indexer = indexer;
726 145 : this.currentUser = currentUser;
727 145 : }
728 :
729 : ChangeEdit createEdit(
730 : Repository repository,
731 : ChangeNotes notes,
732 : PatchSet basePatchset,
733 : ObjectId newEditCommitId,
734 : Instant timestamp)
735 : throws IOException {
736 24 : Change change = notes.getChange();
737 24 : String editRefName = getEditRefName(change, basePatchset);
738 24 : updateReference(
739 24 : notes.getProjectName(),
740 : repository,
741 : editRefName,
742 24 : ObjectId.zeroId(),
743 : newEditCommitId,
744 : timestamp);
745 24 : reindex(change);
746 :
747 24 : RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
748 24 : return new ChangeEdit(change, editRefName, newEditCommit, basePatchset);
749 : }
750 :
751 : private String getEditRefName(Change change, PatchSet basePatchset) {
752 24 : IdentifiedUser me = currentUser.get().asIdentifiedUser();
753 24 : return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchset.id());
754 : }
755 :
756 : ChangeEdit updateEdit(
757 : Project.NameKey projectName,
758 : Repository repository,
759 : ChangeEdit changeEdit,
760 : ObjectId newEditCommitId,
761 : Instant timestamp)
762 : throws IOException {
763 12 : String editRefName = changeEdit.getRefName();
764 12 : RevCommit currentEditCommit = changeEdit.getEditCommit();
765 12 : updateReference(
766 : projectName, repository, editRefName, currentEditCommit, newEditCommitId, timestamp);
767 12 : reindex(changeEdit.getChange());
768 :
769 12 : RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
770 12 : return new ChangeEdit(
771 12 : changeEdit.getChange(), editRefName, newEditCommit, changeEdit.getBasePatchSet());
772 : }
773 :
774 : private void updateReference(
775 : Project.NameKey projectName,
776 : Repository repository,
777 : String refName,
778 : ObjectId currentObjectId,
779 : ObjectId targetObjectId,
780 : Instant timestamp)
781 : throws IOException {
782 24 : RefUpdate ru = repository.updateRef(refName);
783 24 : ru.setExpectedOldObjectId(currentObjectId);
784 24 : ru.setNewObjectId(targetObjectId);
785 24 : ru.setRefLogIdent(getRefLogIdent(timestamp));
786 24 : ru.setRefLogMessage("inline edit (amend)", false);
787 24 : ru.setForceUpdate(true);
788 24 : try (RevWalk revWalk = new RevWalk(repository)) {
789 24 : RefUpdate.Result res = ru.update(revWalk);
790 24 : String message = "cannot update " + ru.getName() + " in " + projectName + ": " + res;
791 24 : if (res == RefUpdate.Result.LOCK_FAILURE) {
792 0 : throw new LockFailureException(message, ru);
793 : }
794 24 : if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
795 0 : throw new IOException(message);
796 : }
797 : }
798 24 : }
799 :
800 : void baseEditOnDifferentPatchset(
801 : Repository repository,
802 : ChangeEdit changeEdit,
803 : PatchSet currentPatchSet,
804 : ObjectId currentEditCommit,
805 : ObjectId newEditCommitId,
806 : Instant nowTimestamp)
807 : throws IOException {
808 1 : String newEditRefName = getEditRefName(changeEdit.getChange(), currentPatchSet);
809 1 : updateReferenceWithNameChange(
810 : repository,
811 1 : changeEdit.getRefName(),
812 : currentEditCommit,
813 : newEditRefName,
814 : newEditCommitId,
815 : nowTimestamp);
816 1 : reindex(changeEdit.getChange());
817 1 : }
818 :
819 : private void updateReferenceWithNameChange(
820 : Repository repository,
821 : String currentRefName,
822 : ObjectId currentObjectId,
823 : String newRefName,
824 : ObjectId targetObjectId,
825 : Instant timestamp)
826 : throws IOException {
827 1 : BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate();
828 1 : batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName));
829 1 : batchRefUpdate.addCommand(
830 1 : new ReceiveCommand(currentObjectId, ObjectId.zeroId(), currentRefName));
831 1 : batchRefUpdate.setRefLogMessage("rebase edit", false);
832 1 : batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp));
833 1 : try (RevWalk revWalk = new RevWalk(repository)) {
834 1 : batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
835 : }
836 1 : for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
837 1 : if (cmd.getResult() != ReceiveCommand.Result.OK) {
838 0 : throw new IOException("failed: " + cmd);
839 : }
840 1 : }
841 1 : }
842 :
843 : static RevCommit lookupCommit(Repository repository, ObjectId commitId) throws IOException {
844 24 : try (RevWalk revWalk = new RevWalk(repository)) {
845 24 : return revWalk.parseCommit(commitId);
846 : }
847 : }
848 :
849 : private PersonIdent getRefLogIdent(Instant timestamp) {
850 24 : IdentifiedUser user = currentUser.get().asIdentifiedUser();
851 24 : return user.newRefLogIdent(timestamp, zoneId);
852 : }
853 :
854 : private void reindex(Change change) {
855 24 : indexer.index(change);
856 24 : }
857 : }
858 : }
|