summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/patch/PatchScriptFactory.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/patch/PatchScriptFactory.java')
-rw-r--r--java/com/google/gerrit/server/patch/PatchScriptFactory.java417
1 files changed, 417 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
new file mode 100644
index 0000000000..b1e0e3cf68
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -0,0 +1,417 @@
+// 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 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.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+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.gwtorm.server.OrmException;
+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 ReviewDb db;
+ 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,
+ ReviewDb db,
+ 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.db = db;
+ 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.getParentKey();
+ }
+
+ @AssistedInject
+ PatchScriptFactory(
+ GitRepositoryManager grm,
+ PatchSetUtil psUtil,
+ Provider<PatchScriptBuilder> builderFactory,
+ PatchListCache patchListCache,
+ ReviewDb db,
+ 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.db = db;
+ 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.getParentKey();
+ 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 OrmException, LargeObjectException, AuthException, InvalidChangeOperationException,
+ IOException, PermissionBackendException {
+ if (parentNum < 0) {
+ validatePatchSetId(psa);
+ }
+ validatePatchSetId(psb);
+
+ PatchSet psEntityA = psa != null ? psUtil.get(db, notes, psa) : null;
+ PatchSet psEntityB = psb.get() == 0 ? new PatchSet(psb) : psUtil.get(db, notes, psb);
+ if (psEntityA != null || psEntityB != null) {
+ try {
+ permissionBackend.currentUser().change(notes).database(db).check(ChangePermission.READ);
+ } catch (AuthException e) {
+ throw new NoSuchChangeException(changeId);
+ }
+ }
+
+ if (!projectCache.checkedGet(notes.getProjectName()).statePermitsRead()) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ try (Repository git = repoManager.openRepository(notes.getProjectName())) {
+ bId = toObjectId(psEntityB);
+ if (parentNum < 0) {
+ aId = psEntityA != null ? toObjectId(psEntityA) : null;
+ }
+
+ 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 toObjectId(PatchSet ps) throws AuthException, IOException, OrmException {
+ if (ps.getId().get() == 0) {
+ return getEditRev();
+ }
+ if (ps.getRevision() == null || ps.getRevision().get() == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ try {
+ return ObjectId.fromString(ps.getRevision().get());
+ } catch (IllegalArgumentException e) {
+ logger.atSevere().log("Patch set %s has invalid revision", ps.getId());
+ throw new NoSuchChangeException(changeId, e);
+ }
+ }
+
+ private ObjectId getEditRev() throws AuthException, IOException, OrmException {
+ 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.getParentKey())) { // OK, same change;
+ } else {
+ throw new NoSuchChangeException(changeId);
+ }
+ }
+
+ private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName)
+ throws OrmException {
+ 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(db, notes)) {
+ String name = fileName;
+ if (psa != null) {
+ switch (changeType) {
+ case COPIED:
+ case RENAMED:
+ if (ps.getId().equals(psa)) {
+ name = oldName;
+ }
+ break;
+
+ case MODIFIED:
+ case DELETED:
+ case ADDED:
+ case REWRITE:
+ break;
+ }
+ }
+
+ Patch p = new Patch(new Patch.Key(ps.getId(), name));
+ history.add(p);
+ byKey.put(p.getKey(), p);
+ }
+ if (edit != null && edit.isPresent()) {
+ Patch p = new Patch(new Patch.Key(new PatchSet.Id(psb.getParentKey(), 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) throws OrmException {
+ for (Comment c : commentsUtil.publishedByChangeFile(db, notes, changeId, file)) {
+ comments.include(notes.getChangeId(), c);
+ PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
+ Patch.Key pKey = new 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)
+ throws OrmException {
+ for (Comment c : commentsUtil.draftByChangeFileAuthor(db, notes, file, me)) {
+ comments.include(notes.getChangeId(), c);
+ PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
+ Patch.Key pKey = new Patch.Key(psId, c.key.filename);
+ Patch p = byKey.get(pKey);
+ if (p != null) {
+ p.setDraftCount(p.getDraftCount() + 1);
+ }
+ }
+ }
+}