Merge "Prevent pushing of patch sets onto invisible patch sets"
This commit is contained in:
		| @@ -520,10 +520,16 @@ public abstract class AbstractDaemonTest { | ||||
|  | ||||
|   protected PushOneCommit.Result amendChange(String changeId, String ref) | ||||
|       throws Exception { | ||||
|     return amendChange(changeId, ref, admin, testRepo); | ||||
|   } | ||||
|  | ||||
|   protected PushOneCommit.Result amendChange(String changeId, String ref, | ||||
|       TestAccount testAccount, TestRepository<?> repo) throws Exception { | ||||
|     Collections.shuffle(RANDOM); | ||||
|     PushOneCommit push = | ||||
|         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, | ||||
|             PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId); | ||||
|         pushFactory.create(db, testAccount.getIdent(), repo, | ||||
|             PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, | ||||
|             new String(Chars.toArray(RANDOM)), changeId); | ||||
|     return push.to(ref); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableSet; | ||||
| import com.google.common.collect.Iterables; | ||||
| import com.google.gerrit.acceptance.AbstractDaemonTest; | ||||
| import com.google.gerrit.acceptance.AcceptanceTestRequestScope; | ||||
| import com.google.gerrit.acceptance.GitUtil; | ||||
| import com.google.gerrit.acceptance.NoHttpd; | ||||
| import com.google.gerrit.acceptance.PushOneCommit; | ||||
| import com.google.gerrit.acceptance.TestProjectInput; | ||||
| @@ -64,6 +65,7 @@ import com.google.gerrit.reviewdb.client.Account; | ||||
| import com.google.gerrit.reviewdb.client.AccountGroup; | ||||
| import com.google.gerrit.reviewdb.client.Change; | ||||
| import com.google.gerrit.reviewdb.client.PatchSet; | ||||
| import com.google.gerrit.reviewdb.client.Project; | ||||
| import com.google.gerrit.server.change.ChangeResource; | ||||
| import com.google.gerrit.server.config.AnonymousCowardNameProvider; | ||||
| import com.google.gerrit.server.git.ProjectConfig; | ||||
| @@ -74,6 +76,8 @@ import com.google.gerrit.testutil.FakeEmailSender.Message; | ||||
| import com.google.gerrit.testutil.NoteDbMode; | ||||
| import com.google.gerrit.testutil.TestTimeUtil; | ||||
|  | ||||
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; | ||||
| import org.eclipse.jgit.junit.TestRepository; | ||||
| import org.eclipse.jgit.lib.Constants; | ||||
| import org.eclipse.jgit.lib.PersonIdent; | ||||
| import org.eclipse.jgit.lib.Repository; | ||||
| @@ -1246,6 +1250,71 @@ public class ChangeIT extends AbstractDaemonTest { | ||||
|         .get(); | ||||
|   } | ||||
|  | ||||
|   @Test | ||||
|   public void createNewPatchSetOnVisibleDraftPatchSet() throws Exception { | ||||
|     // Clone separate repositories of the same project as admin and as user | ||||
|     TestRepository<InMemoryRepository> adminTestRepo = | ||||
|         cloneProject(project, admin); | ||||
|     TestRepository<InMemoryRepository> userTestRepo = | ||||
|         cloneProject(project, user); | ||||
|  | ||||
|     // Create change as admin | ||||
|     PushOneCommit push = pushFactory.create( | ||||
|         db, admin.getIdent(), adminTestRepo); | ||||
|     PushOneCommit.Result r1 = push.to("refs/for/master"); | ||||
|     r1.assertOkStatus(); | ||||
|  | ||||
|     // Amend draft as admin | ||||
|     PushOneCommit.Result r2 = amendChange( | ||||
|         r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo); | ||||
|     r2.assertOkStatus(); | ||||
|  | ||||
|     // Add user as reviewer to make this patch set visible | ||||
|     AddReviewerInput in = new AddReviewerInput(); | ||||
|     in.reviewer = user.email; | ||||
|     gApi.changes() | ||||
|         .id(r1.getChangeId()) | ||||
|         .addReviewer(in); | ||||
|  | ||||
|     // Fetch change | ||||
|     GitUtil.fetch(userTestRepo, r2.getPatchSet().getRefName() + ":ps"); | ||||
|     userTestRepo.reset("ps"); | ||||
|  | ||||
|     // Amend change as user | ||||
|     PushOneCommit.Result r3 = amendChange( | ||||
|         r2.getChangeId(), "refs/drafts/master", user, userTestRepo); | ||||
|     r3.assertOkStatus(); | ||||
|   } | ||||
|  | ||||
|   @Test | ||||
|   public void createNewPatchSetOnInvisibleDraftPatchSet() throws Exception { | ||||
|     // Clone separate repositories of the same project as admin and as user | ||||
|     TestRepository<InMemoryRepository> adminTestRepo = | ||||
|         cloneProject(project, admin); | ||||
|     TestRepository<InMemoryRepository> userTestRepo = | ||||
|         cloneProject(project, user); | ||||
|  | ||||
|     // Create change as admin | ||||
|     PushOneCommit push = pushFactory.create( | ||||
|         db, admin.getIdent(), adminTestRepo); | ||||
|     PushOneCommit.Result r1 = push.to("refs/for/master"); | ||||
|     r1.assertOkStatus(); | ||||
|     // Amend draft as admin | ||||
|     PushOneCommit.Result r2 = amendChange( | ||||
|         r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo); | ||||
|     r2.assertOkStatus(); | ||||
|  | ||||
|     // Fetch change | ||||
|     GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps"); | ||||
|     userTestRepo.reset("ps"); | ||||
|  | ||||
|     // Amend change as user | ||||
|     PushOneCommit.Result r3 = amendChange( | ||||
|         r1.getChangeId(), "refs/for/master", user, userTestRepo); | ||||
|     r3.assertErrorStatus("cannot replace " | ||||
|         + r3.getChange().change().getChangeId() + "."); | ||||
|   } | ||||
|  | ||||
|   private static Iterable<Account.Id> getReviewers( | ||||
|       Collection<AccountInfo> r) { | ||||
|     return Iterables.transform(r, new Function<AccountInfo, Account.Id>() { | ||||
|   | ||||
| @@ -434,7 +434,8 @@ public class ReceiveCommits { | ||||
|     rp.setRefFilter(new RefFilter() { | ||||
|       @Override | ||||
|       public Map<String, Ref> filter(Map<String, Ref> refs) { | ||||
|         Map<String, Ref> filteredRefs = Maps.newHashMapWithExpectedSize(refs.size()); | ||||
|         Map<String, Ref> filteredRefs = | ||||
|             Maps.newHashMapWithExpectedSize(refs.size()); | ||||
|         for (Map.Entry<String, Ref> e : refs.entrySet()) { | ||||
|           String name = e.getKey(); | ||||
|           if (!name.startsWith(REFS_CHANGES) | ||||
| @@ -464,7 +465,8 @@ public class ReceiveCommits { | ||||
|           } catch (ServiceMayNotContinueException e) { | ||||
|             throw e; | ||||
|           } catch (IOException e) { | ||||
|             ServiceMayNotContinueException ex = new ServiceMayNotContinueException(); | ||||
|             ServiceMayNotContinueException ex = | ||||
|                 new ServiceMayNotContinueException(); | ||||
|             ex.initCause(e); | ||||
|             throw ex; | ||||
|           } | ||||
| @@ -689,7 +691,7 @@ public class ReceiveCommits { | ||||
|             new Function<ReplaceRequest, Integer>() { | ||||
|               @Override | ||||
|               public Integer apply(ReplaceRequest in) { | ||||
|                 return in.change.getId().get(); | ||||
|                 return in.notes.getChangeId().get(); | ||||
|               } | ||||
|             })); | ||||
|     if (!updated.isEmpty()) { | ||||
| @@ -697,7 +699,7 @@ public class ReceiveCommits { | ||||
|       addMessage("Updated Changes:"); | ||||
|       boolean edit = magicBranch != null && magicBranch.edit; | ||||
|       for (ReplaceRequest u : updated) { | ||||
|         addMessage(formatChangeUrl(canonicalWebUrl, u.change, | ||||
|         addMessage(formatChangeUrl(canonicalWebUrl, u.notes.getChange(), | ||||
|             u.info.getSubject(), edit)); | ||||
|       } | ||||
|       addMessage(""); | ||||
| @@ -1823,7 +1825,7 @@ public class ReceiveCommits { | ||||
|       bySha.put(r.commitId, r.change); | ||||
|     } | ||||
|     for (ReplaceRequest r : replace) { | ||||
|       bySha.put(r.newCommitId, r.change); | ||||
|       bySha.put(r.newCommitId, r.notes.getChange()); | ||||
|     } | ||||
|     Change tipChange = bySha.get(magicBranch.cmd.getNewId()); | ||||
|     checkState(tipChange != null, | ||||
| @@ -1899,7 +1901,7 @@ public class ReceiveCommits { | ||||
|     for (CheckedFuture<ChangeNotes, OrmException> f : futures) { | ||||
|       ChangeNotes notes = f.checkedGet(); | ||||
|       if (notes.getChange() != null) { | ||||
|         replaceByChange.get(notes.getChangeId()).change = notes.getChange(); | ||||
|         replaceByChange.get(notes.getChangeId()).notes = notes; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -1909,7 +1911,7 @@ public class ReceiveCommits { | ||||
|     final ObjectId newCommitId; | ||||
|     final ReceiveCommand inputCommand; | ||||
|     final boolean checkMergedInto; | ||||
|     Change change; | ||||
|     ChangeNotes notes; | ||||
|     ChangeControl changeCtl; | ||||
|     BiMap<RevCommit, PatchSet.Id> revisions; | ||||
|     PatchSet.Id psId; | ||||
| @@ -1945,12 +1947,12 @@ public class ReceiveCommits { | ||||
|     boolean validate(boolean autoClose) throws IOException, OrmException { | ||||
|       if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) { | ||||
|         return false; | ||||
|       } else if (change == null) { | ||||
|       } else if (notes == null) { | ||||
|         reject(inputCommand, "change " + ontoChange + " not found"); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       priorPatchSet = change.currentPatchSetId(); | ||||
|       priorPatchSet = notes.getChange().currentPatchSetId(); | ||||
|       if (!revisions.containsValue(priorPatchSet)) { | ||||
|         reject(inputCommand, "change " + ontoChange + " missing revisions"); | ||||
|         return false; | ||||
| @@ -1965,7 +1967,7 @@ public class ReceiveCommits { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       changeCtl = projectControl.controlFor(db, change); | ||||
|       changeCtl = projectControl.controlFor(notes); | ||||
|       if (!changeCtl.canAddPatchSet(db)) { | ||||
|         String locked = "."; | ||||
|         if (changeCtl.isPatchSetLocked(db)) { | ||||
| @@ -1973,7 +1975,7 @@ public class ReceiveCommits { | ||||
|         } | ||||
|         reject(inputCommand, "cannot replace " + ontoChange + locked); | ||||
|         return false; | ||||
|       } else if (change.getStatus().isClosed()) { | ||||
|       } else if (notes.getChange().getStatus().isClosed()) { | ||||
|         reject(inputCommand, "change " + ontoChange + " closed"); | ||||
|         return false; | ||||
|       } else if (revisions.containsKey(newCommit)) { | ||||
| @@ -2048,7 +2050,7 @@ public class ReceiveCommits { | ||||
|     } | ||||
|  | ||||
|     private boolean newEdit() { | ||||
|       psId = change.currentPatchSetId(); | ||||
|       psId = notes.getChange().currentPatchSetId(); | ||||
|       Optional<ChangeEdit> edit = null; | ||||
|  | ||||
|       try { | ||||
| @@ -2087,13 +2089,14 @@ public class ReceiveCommits { | ||||
|           newCommitId, | ||||
|           RefNames.refsEdit( | ||||
|               user.getAccountId(), | ||||
|               change.getId(), | ||||
|               notes.getChangeId(), | ||||
|               psId)); | ||||
|     } | ||||
|  | ||||
|     private void newPatchSet() throws IOException { | ||||
|       RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId); | ||||
|       psId = ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId()); | ||||
|       psId = ChangeUtil.nextPatchSetId( | ||||
|           allRefs, notes.getChange().currentPatchSetId()); | ||||
|       info = patchSetInfoFactory.get( | ||||
|           rp.getRevWalk(), newCommit, psId); | ||||
|       cmd = new ReceiveCommand( | ||||
| @@ -2119,12 +2122,12 @@ public class ReceiveCommits { | ||||
|  | ||||
|       RevCommit priorCommit = revisions.inverse().get(priorPatchSet); | ||||
|       replaceOp = replaceOpFactory.create(requestScopePropagator, | ||||
|           projectControl, change.getDest(), checkMergedInto, priorPatchSet, | ||||
|           priorCommit, psId, newCommit, info, groups, magicBranch, | ||||
|           rp.getPushCertificate()); | ||||
|       bu.addOp(change.getId(), replaceOp); | ||||
|           projectControl, notes.getChange().getDest(), checkMergedInto, | ||||
|           priorPatchSet, priorCommit, psId, newCommit, info, groups, | ||||
|           magicBranch, rp.getPushCertificate()); | ||||
|       bu.addOp(notes.getChangeId(), replaceOp); | ||||
|       if (progress != null) { | ||||
|         bu.addOp(change.getId(), new ChangeProgressOp(progress)); | ||||
|         bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -2261,7 +2264,8 @@ public class ReceiveCommits { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName()); | ||||
|     boolean defaultName = | ||||
|         Strings.isNullOrEmpty(user.getAccount().getFullName()); | ||||
|     RevWalk walk = rp.getRevWalk(); | ||||
|     walk.reset(); | ||||
|     walk.sort(RevSort.NONE); | ||||
| @@ -2354,7 +2358,7 @@ public class ReceiveCommits { | ||||
|       } | ||||
|  | ||||
|       SetMultimap<ObjectId, Ref> byCommit = changeRefsById(); | ||||
|       Map<Change.Key, Change> byKey = null; | ||||
|       Map<Change.Key, ChangeNotes> byKey = null; | ||||
|  | ||||
|       COMMIT: for (RevCommit c; (c = rw.next()) != null;) { | ||||
|         rw.parseBody(c); | ||||
| @@ -2373,11 +2377,11 @@ public class ReceiveCommits { | ||||
|             byKey = openChangesByBranch(branch); | ||||
|           } | ||||
|  | ||||
|           Change onto = byKey.get(new Change.Key(changeId.trim())); | ||||
|           ChangeNotes onto = byKey.get(new Change.Key(changeId.trim())); | ||||
|           if (onto != null) { | ||||
|             Change.Id id = onto.getId(); | ||||
|             Change.Id id = onto.getChangeId(); | ||||
|             final ReplaceRequest req = new ReplaceRequest(id, c, cmd, false); | ||||
|             req.change = onto; | ||||
|             req.notes = onto; | ||||
|             if (req.validate(true)) { | ||||
|               req.addOps(bu, null); | ||||
|               bu.addOp( | ||||
| @@ -2405,11 +2409,11 @@ public class ReceiveCommits { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private Map<Change.Key, Change> openChangesByBranch(Branch.NameKey branch) | ||||
|       throws OrmException { | ||||
|     Map<Change.Key, Change> r = new HashMap<>(); | ||||
|   private Map<Change.Key, ChangeNotes> openChangesByBranch( | ||||
|       Branch.NameKey branch) throws OrmException { | ||||
|     Map<Change.Key, ChangeNotes> r = new HashMap<>(); | ||||
|     for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) { | ||||
|       r.put(cd.change().getKey(), cd.change()); | ||||
|       r.put(cd.change().getKey(), cd.notes()); | ||||
|     } | ||||
|     return r; | ||||
|   } | ||||
|   | ||||
| @@ -30,6 +30,7 @@ import com.google.gerrit.reviewdb.client.Project; | ||||
| import com.google.gerrit.reviewdb.server.ReviewDb; | ||||
| import com.google.gerrit.server.ApprovalsUtil; | ||||
| import com.google.gerrit.server.CurrentUser; | ||||
| import com.google.gerrit.server.PatchSetUtil; | ||||
| import com.google.gerrit.server.notedb.ChangeNotes; | ||||
| import com.google.gerrit.server.query.change.ChangeData; | ||||
| import com.google.gwtorm.server.OrmException; | ||||
| @@ -106,14 +107,17 @@ public class ChangeControl { | ||||
|     private final ChangeData.Factory changeDataFactory; | ||||
|     private final ChangeNotes.Factory notesFactory; | ||||
|     private final ApprovalsUtil approvalsUtil; | ||||
|     private final PatchSetUtil patchSetUtil; | ||||
|  | ||||
|     @Inject | ||||
|     Factory(ChangeData.Factory changeDataFactory, | ||||
|         ChangeNotes.Factory notesFactory, | ||||
|         ApprovalsUtil approvalsUtil) { | ||||
|         ApprovalsUtil approvalsUtil, | ||||
|         PatchSetUtil patchSetUtil) { | ||||
|       this.changeDataFactory = changeDataFactory; | ||||
|       this.notesFactory = notesFactory; | ||||
|       this.approvalsUtil = approvalsUtil; | ||||
|       this.patchSetUtil = patchSetUtil; | ||||
|     } | ||||
|  | ||||
|     ChangeControl create(RefControl refControl, ReviewDb db, Project.NameKey | ||||
| @@ -137,7 +141,7 @@ public class ChangeControl { | ||||
|  | ||||
|     ChangeControl create(RefControl refControl, ChangeNotes notes) { | ||||
|       return new ChangeControl(changeDataFactory, approvalsUtil, refControl, | ||||
|           notes); | ||||
|           notes, patchSetUtil); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -145,16 +149,19 @@ public class ChangeControl { | ||||
|   private final ApprovalsUtil approvalsUtil; | ||||
|   private final RefControl refControl; | ||||
|   private final ChangeNotes notes; | ||||
|   private final PatchSetUtil patchSetUtil; | ||||
|  | ||||
|   ChangeControl( | ||||
|       ChangeData.Factory changeDataFactory, | ||||
|       ApprovalsUtil approvalsUtil, | ||||
|       RefControl refControl, | ||||
|       ChangeNotes notes) { | ||||
|       ChangeNotes notes, | ||||
|       PatchSetUtil patchSetUtil) { | ||||
|     this.changeDataFactory = changeDataFactory; | ||||
|     this.approvalsUtil = approvalsUtil; | ||||
|     this.refControl = refControl; | ||||
|     this.notes = notes; | ||||
|     this.patchSetUtil = patchSetUtil; | ||||
|   } | ||||
|  | ||||
|   public ChangeControl forUser(final CurrentUser who) { | ||||
| @@ -162,7 +169,7 @@ public class ChangeControl { | ||||
|       return this; | ||||
|     } | ||||
|     return new ChangeControl(changeDataFactory, approvalsUtil, | ||||
|         getRefControl().forUser(who), notes); | ||||
|         getRefControl().forUser(who), notes, patchSetUtil); | ||||
|   } | ||||
|  | ||||
|   public RefControl getRefControl() { | ||||
| @@ -307,8 +314,11 @@ public class ChangeControl { | ||||
|   } | ||||
|  | ||||
|   /** Can this user add a patch set to this change? */ | ||||
|   public boolean canAddPatchSet(ReviewDb db) throws OrmException { | ||||
|     return getRefControl().canUpload() && !isPatchSetLocked(db); | ||||
|   public boolean canAddPatchSet(ReviewDb db) | ||||
|       throws OrmException { | ||||
|     return getRefControl().canUpload() | ||||
|         && !isPatchSetLocked(db) | ||||
|         && isPatchVisible(patchSetUtil.current(db, notes), db); | ||||
|   } | ||||
|  | ||||
|   /** Is the current patch set locked against state changes? */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Edwin Kempin
					Edwin Kempin