This field has been nullable since the very beginning (I74ef9f34), but honestly I have no idea why. Certainly the vast majority of callers today assume that it's not null. Force callers to provide a non-null commitId at construction time, and disallow mutating it later. There was one stubborn caller in PatchScriptFactory which I couldn't get rid of without disentangling the logic of the whole class, so I punted and left a deprecated ugly named static factory method just for this one case. Change-Id: If8253781c101cbe4ed18210ec9ebe78dee8f4add
406 lines
14 KiB
Java
406 lines
14 KiB
Java
// 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.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 {
|
|
if (parentNum < 0) {
|
|
validatePatchSetId(psa);
|
|
}
|
|
validatePatchSetId(psb);
|
|
|
|
PatchSet psEntityA = psa != null ? psUtil.get(notes, psa) : null;
|
|
|
|
// TODO(dborowitz): Shouldn't be creating a PatchSet with no commitId, but the logic depends on
|
|
// it somehow in a way that I don't follow, so old behavior is preserved for now.
|
|
@SuppressWarnings("deprecation")
|
|
PatchSet psEntityB =
|
|
psb.get() == 0 ? PatchSet.createWithNoCommitId(psb) : psUtil.get(notes, psb);
|
|
|
|
if (psEntityA != null || psEntityB != null) {
|
|
try {
|
|
permissionBackend.currentUser().change(notes).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 {
|
|
if (ps.getId().get() == 0) {
|
|
return getEditRev();
|
|
}
|
|
return ps.getCommitId();
|
|
}
|
|
|
|
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.getId().equals(psa)) {
|
|
name = oldName;
|
|
}
|
|
break;
|
|
|
|
case MODIFIED:
|
|
case DELETED:
|
|
case ADDED:
|
|
case REWRITE:
|
|
break;
|
|
}
|
|
}
|
|
|
|
Patch p = new Patch(Patch.key(ps.getId(), 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);
|
|
}
|
|
}
|
|
}
|
|
}
|