| // Copyright (C) 2009 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.patch; | |
| import static com.google.common.base.Preconditions.checkArgument; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.common.data.CommentDetail; | |
| import com.google.gerrit.common.data.PatchScript; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.Comment; | |
| import com.google.gerrit.entities.Patch; | |
| import com.google.gerrit.entities.Patch.ChangeType; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.extensions.client.DiffPreferencesInfo; | |
| import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; | |
| import com.google.gerrit.extensions.restapi.AuthException; | |
| import com.google.gerrit.server.CommentsUtil; | |
| import com.google.gerrit.server.CurrentUser; | |
| import com.google.gerrit.server.PatchSetUtil; | |
| import com.google.gerrit.server.edit.ChangeEdit; | |
| import com.google.gerrit.server.edit.ChangeEditUtil; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.git.LargeObjectException; | |
| import com.google.gerrit.server.notedb.ChangeNotes; | |
| import com.google.gerrit.server.permissions.ChangePermission; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.project.InvalidChangeOperationException; | |
| import com.google.gerrit.server.project.NoSuchChangeException; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.inject.Provider; | |
| import com.google.inject.assistedinject.Assisted; | |
| import com.google.inject.assistedinject.AssistedInject; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.concurrent.Callable; | |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.Repository; | |
| public class PatchScriptFactory implements Callable<PatchScript> { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| public interface Factory { | |
| PatchScriptFactory create( | |
| ChangeNotes notes, | |
| String fileName, | |
| @Assisted("patchSetA") PatchSet.Id patchSetA, | |
| @Assisted("patchSetB") PatchSet.Id patchSetB, | |
| DiffPreferencesInfo diffPrefs); | |
| PatchScriptFactory create( | |
| ChangeNotes notes, | |
| String fileName, | |
| int parentNum, | |
| PatchSet.Id patchSetB, | |
| DiffPreferencesInfo diffPrefs); | |
| } | |
| private final GitRepositoryManager repoManager; | |
| private final PatchSetUtil psUtil; | |
| private final Provider<PatchScriptBuilder> builderFactory; | |
| private final PatchListCache patchListCache; | |
| private final CommentsUtil commentsUtil; | |
| private final String fileName; | |
| @Nullable private final PatchSet.Id psa; | |
| private final int parentNum; | |
| private final PatchSet.Id psb; | |
| private final DiffPreferencesInfo diffPrefs; | |
| private final ChangeEditUtil editReader; | |
| private final Provider<CurrentUser> userProvider; | |
| private final PermissionBackend permissionBackend; | |
| private final ProjectCache projectCache; | |
| private Optional<ChangeEdit> edit; | |
| private final Change.Id changeId; | |
| private boolean loadHistory = true; | |
| private boolean loadComments = true; | |
| private ChangeNotes notes; | |
| private ObjectId aId; | |
| private ObjectId bId; | |
| private List<Patch> history; | |
| private CommentDetail comments; | |
| @AssistedInject | |
| PatchScriptFactory( | |
| GitRepositoryManager grm, | |
| PatchSetUtil psUtil, | |
| Provider<PatchScriptBuilder> builderFactory, | |
| PatchListCache patchListCache, | |
| CommentsUtil commentsUtil, | |
| ChangeEditUtil editReader, | |
| Provider<CurrentUser> userProvider, | |
| PermissionBackend permissionBackend, | |
| ProjectCache projectCache, | |
| @Assisted ChangeNotes notes, | |
| @Assisted String fileName, | |
| @Assisted("patchSetA") @Nullable PatchSet.Id patchSetA, | |
| @Assisted("patchSetB") PatchSet.Id patchSetB, | |
| @Assisted DiffPreferencesInfo diffPrefs) { | |
| this.repoManager = grm; | |
| this.psUtil = psUtil; | |
| this.builderFactory = builderFactory; | |
| this.patchListCache = patchListCache; | |
| this.notes = notes; | |
| this.commentsUtil = commentsUtil; | |
| this.editReader = editReader; | |
| this.userProvider = userProvider; | |
| this.permissionBackend = permissionBackend; | |
| this.projectCache = projectCache; | |
| this.fileName = fileName; | |
| this.psa = patchSetA; | |
| this.parentNum = -1; | |
| this.psb = patchSetB; | |
| this.diffPrefs = diffPrefs; | |
| changeId = patchSetB.changeId(); | |
| } | |
| @AssistedInject | |
| PatchScriptFactory( | |
| GitRepositoryManager grm, | |
| PatchSetUtil psUtil, | |
| Provider<PatchScriptBuilder> builderFactory, | |
| PatchListCache patchListCache, | |
| CommentsUtil commentsUtil, | |
| ChangeEditUtil editReader, | |
| Provider<CurrentUser> userProvider, | |
| PermissionBackend permissionBackend, | |
| ProjectCache projectCache, | |
| @Assisted ChangeNotes notes, | |
| @Assisted String fileName, | |
| @Assisted int parentNum, | |
| @Assisted PatchSet.Id patchSetB, | |
| @Assisted DiffPreferencesInfo diffPrefs) { | |
| this.repoManager = grm; | |
| this.psUtil = psUtil; | |
| this.builderFactory = builderFactory; | |
| this.patchListCache = patchListCache; | |
| this.notes = notes; | |
| this.commentsUtil = commentsUtil; | |
| this.editReader = editReader; | |
| this.userProvider = userProvider; | |
| this.permissionBackend = permissionBackend; | |
| this.projectCache = projectCache; | |
| this.fileName = fileName; | |
| this.psa = null; | |
| this.parentNum = parentNum; | |
| this.psb = patchSetB; | |
| this.diffPrefs = diffPrefs; | |
| changeId = patchSetB.changeId(); | |
| checkArgument(parentNum >= 0, "parentNum must be >= 0"); | |
| } | |
| public void setLoadHistory(boolean load) { | |
| loadHistory = load; | |
| } | |
| public void setLoadComments(boolean load) { | |
| loadComments = load; | |
| } | |
| @Override | |
| public PatchScript call() | |
| throws LargeObjectException, AuthException, InvalidChangeOperationException, IOException, | |
| PermissionBackendException { | |
| validatePatchSetId(psa); | |
| validatePatchSetId(psb); | |
| if (psa != null) { | |
| checkState(parentNum < 0, "expected no parentNum when psa is present"); | |
| checkArgument(psa.get() != 0, "edit not supported for left side"); | |
| aId = getCommitId(psa); | |
| } else { | |
| aId = null; | |
| } | |
| if (psb.get() != 0) { | |
| bId = getCommitId(psb); | |
| } else { | |
| // Change edit: create synthetic PatchSet corresponding to the edit. | |
| bId = getEditRev(); | |
| } | |
| try { | |
| permissionBackend.currentUser().change(notes).check(ChangePermission.READ); | |
| } catch (AuthException e) { | |
| throw new NoSuchChangeException(changeId, e); | |
| } | |
| if (!projectCache.checkedGet(notes.getProjectName()).statePermitsRead()) { | |
| throw new NoSuchChangeException(changeId); | |
| } | |
| try (Repository git = repoManager.openRepository(notes.getProjectName())) { | |
| try { | |
| final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace)); | |
| final PatchScriptBuilder b = newBuilder(list, git); | |
| final PatchListEntry content = list.get(fileName); | |
| loadCommentsAndHistory(content.getChangeType(), content.getOldName(), content.getNewName()); | |
| return b.toPatchScript(content, comments, history); | |
| } catch (PatchListNotAvailableException e) { | |
| throw new NoSuchChangeException(changeId, e); | |
| } catch (IOException e) { | |
| logger.atSevere().withCause(e).log("File content unavailable"); | |
| throw new NoSuchChangeException(changeId, e); | |
| } catch (org.eclipse.jgit.errors.LargeObjectException err) { | |
| throw new LargeObjectException("File content is too large", err); | |
| } | |
| } catch (RepositoryNotFoundException e) { | |
| logger.atSevere().withCause(e).log("Repository %s not found", notes.getProjectName()); | |
| throw new NoSuchChangeException(changeId, e); | |
| } catch (IOException e) { | |
| logger.atSevere().withCause(e).log("Cannot open repository %s", notes.getProjectName()); | |
| throw new NoSuchChangeException(changeId, e); | |
| } | |
| } | |
| private PatchListKey keyFor(Whitespace whitespace) { | |
| if (parentNum < 0) { | |
| return PatchListKey.againstCommit(aId, bId, whitespace); | |
| } | |
| return PatchListKey.againstParentNum(parentNum + 1, bId, whitespace); | |
| } | |
| private PatchList listFor(PatchListKey key) throws PatchListNotAvailableException { | |
| return patchListCache.get(key, notes.getProjectName()); | |
| } | |
| private PatchScriptBuilder newBuilder(PatchList list, Repository git) { | |
| final PatchScriptBuilder b = builderFactory.get(); | |
| b.setRepository(git, notes.getProjectName()); | |
| b.setChange(notes.getChange()); | |
| b.setDiffPrefs(diffPrefs); | |
| b.setTrees(list.getComparisonType(), list.getOldId(), list.getNewId()); | |
| return b; | |
| } | |
| private ObjectId getCommitId(PatchSet.Id psId) { | |
| PatchSet ps = psUtil.get(notes, psId); | |
| if (ps == null) { | |
| throw new NoSuchChangeException(psId.changeId()); | |
| } | |
| return ps.commitId(); | |
| } | |
| private ObjectId getEditRev() throws AuthException, IOException { | |
| edit = editReader.byChange(notes); | |
| if (edit.isPresent()) { | |
| return edit.get().getEditCommit(); | |
| } | |
| throw new NoSuchChangeException(notes.getChangeId()); | |
| } | |
| private void validatePatchSetId(PatchSet.Id psId) throws NoSuchChangeException { | |
| if (psId == null) { // OK, means use base; | |
| } else if (changeId.equals(psId.changeId())) { // OK, same change; | |
| } else { | |
| throw new NoSuchChangeException(changeId); | |
| } | |
| } | |
| private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName) { | |
| Map<Patch.Key, Patch> byKey = new HashMap<>(); | |
| if (loadHistory) { | |
| // This seems like a cheap trick. It doesn't properly account for a | |
| // file that gets renamed between patch set 1 and patch set 2. We | |
| // will wind up packing the wrong Patch object because we didn't do | |
| // proper rename detection between the patch sets. | |
| // | |
| history = new ArrayList<>(); | |
| for (PatchSet ps : psUtil.byChange(notes)) { | |
| String name = fileName; | |
| if (psa != null) { | |
| switch (changeType) { | |
| case COPIED: | |
| case RENAMED: | |
| if (ps.id().equals(psa)) { | |
| name = oldName; | |
| } | |
| break; | |
| case MODIFIED: | |
| case DELETED: | |
| case ADDED: | |
| case REWRITE: | |
| break; | |
| } | |
| } | |
| Patch p = new Patch(Patch.key(ps.id(), name)); | |
| history.add(p); | |
| byKey.put(p.getKey(), p); | |
| } | |
| if (edit != null && edit.isPresent()) { | |
| Patch p = new Patch(Patch.key(PatchSet.id(psb.changeId(), 0), fileName)); | |
| history.add(p); | |
| byKey.put(p.getKey(), p); | |
| } | |
| } | |
| if (loadComments && edit == null) { | |
| comments = new CommentDetail(psa, psb); | |
| switch (changeType) { | |
| case ADDED: | |
| case MODIFIED: | |
| loadPublished(byKey, newName); | |
| break; | |
| case DELETED: | |
| loadPublished(byKey, newName); | |
| break; | |
| case COPIED: | |
| case RENAMED: | |
| if (psa != null) { | |
| loadPublished(byKey, oldName); | |
| } | |
| loadPublished(byKey, newName); | |
| break; | |
| case REWRITE: | |
| break; | |
| } | |
| CurrentUser user = userProvider.get(); | |
| if (user.isIdentifiedUser()) { | |
| Account.Id me = user.getAccountId(); | |
| switch (changeType) { | |
| case ADDED: | |
| case MODIFIED: | |
| loadDrafts(byKey, me, newName); | |
| break; | |
| case DELETED: | |
| loadDrafts(byKey, me, newName); | |
| break; | |
| case COPIED: | |
| case RENAMED: | |
| if (psa != null) { | |
| loadDrafts(byKey, me, oldName); | |
| } | |
| loadDrafts(byKey, me, newName); | |
| break; | |
| case REWRITE: | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| private void loadPublished(Map<Patch.Key, Patch> byKey, String file) { | |
| for (Comment c : commentsUtil.publishedByChangeFile(notes, file)) { | |
| comments.include(notes.getChangeId(), c); | |
| PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId); | |
| Patch.Key pKey = Patch.key(psId, c.key.filename); | |
| Patch p = byKey.get(pKey); | |
| if (p != null) { | |
| p.setCommentCount(p.getCommentCount() + 1); | |
| } | |
| } | |
| } | |
| private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file) { | |
| for (Comment c : commentsUtil.draftByChangeFileAuthor(notes, file, me)) { | |
| comments.include(notes.getChangeId(), c); | |
| PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId); | |
| Patch.Key pKey = Patch.key(psId, c.key.filename); | |
| Patch p = byKey.get(pKey); | |
| if (p != null) { | |
| p.setDraftCount(p.getDraftCount() + 1); | |
| } | |
| } | |
| } | |
| } |