// Copyright (C) 2013 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.acceptance.git; import static com.google.gerrit.acceptance.GitUtil.cloneProject; import static com.google.gerrit.acceptance.GitUtil.createProject; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.SshSession; import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRule; 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.PatchSetApproval; 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.GerritPersonIdent; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.git.CommitMergeStatus; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.project.ProjectCache; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.RefSpec; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; @NoHttpd public class SubmitOnPushIT extends AbstractDaemonTest { @Inject private SchemaFactory<ReviewDb> reviewDbProvider; @Inject private GitRepositoryManager repoManager; @Inject private ApprovalsUtil approvalsUtil; @Inject private MetaDataUpdate.Server metaDataUpdateFactory; @Inject private ProjectCache projectCache; @Inject private GroupCache groupCache; @Inject private ChangeNotes.Factory changeNotesFactory; @Inject private @GerritPersonIdent PersonIdent serverIdent; @Inject private PushOneCommit.Factory pushFactory; private Project.NameKey project; private Git git; private ReviewDb db; @Before public void setUp() throws Exception { project = new Project.NameKey("p"); SshSession sshSession = new SshSession(server, admin); createProject(sshSession, project.get()); git = cloneProject(sshSession.getUrl() + "/" + project.get()); sshSession.close(); db = reviewDbProvider.open(); } @After public void cleanup() { db.close(); } @Test public void submitOnPush() throws GitAPIException, OrmException, IOException, ConfigInvalidException { grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); PushOneCommit.Result r = pushTo("refs/for/master%submit"); r.assertOkStatus(); r.assertChange(Change.Status.MERGED, null, admin); assertSubmitApproval(r.getPatchSetId()); assertCommit(project, "refs/heads/master"); } @Test public void submitOnPushWithTag() throws GitAPIException, OrmException, IOException, ConfigInvalidException { grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); grant(Permission.CREATE, project, "refs/tags/*"); final String tag = "v1.0"; PushOneCommit push = pushFactory.create(db, admin.getIdent()); push.setTag(tag); PushOneCommit.Result r = push.to(git, "refs/for/master%submit"); r.assertOkStatus(); r.assertChange(Change.Status.MERGED, null, admin); assertSubmitApproval(r.getPatchSetId()); assertCommit(project, "refs/heads/master"); assertTag(project, "refs/heads/master", tag); } @Test public void submitOnPushToRefsMetaConfig() throws GitAPIException, OrmException, IOException, ConfigInvalidException { grant(Permission.SUBMIT, project, "refs/for/refs/meta/config"); git.fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call(); ObjectId objectId = git.getRepository().getRef("refs/meta/config").getObjectId(); git.checkout().setName(objectId.getName()).call(); PushOneCommit.Result r = pushTo("refs/for/refs/meta/config%submit"); r.assertOkStatus(); r.assertChange(Change.Status.MERGED, null, admin); assertSubmitApproval(r.getPatchSetId()); assertCommit(project, "refs/meta/config"); } @Test public void submitOnPushMergeConflict() throws GitAPIException, OrmException, IOException, ConfigInvalidException { String master = "refs/heads/master"; ObjectId objectId = git.getRepository().getRef(master).getObjectId(); push(master, "one change", "a.txt", "some content"); git.checkout().setName(objectId.getName()).call(); grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); PushOneCommit.Result r = push("refs/for/master%submit", "other change", "a.txt", "other content"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null, admin); r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage()); } @Test public void submitOnPushSuccessfulMerge() throws GitAPIException, OrmException, IOException, ConfigInvalidException { String master = "refs/heads/master"; ObjectId objectId = git.getRepository().getRef(master).getObjectId(); push(master, "one change", "a.txt", "some content"); git.checkout().setName(objectId.getName()).call(); grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); PushOneCommit.Result r = push("refs/for/master%submit", "other change", "b.txt", "other content"); r.assertOkStatus(); r.assertChange(Change.Status.MERGED, null, admin); assertMergeCommit(master, "other change"); } @Test public void submitOnPushNewPatchSet() throws GitAPIException, OrmException, IOException, ConfigInvalidException { PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content"); grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt", "other content", r.getChangeId()); r.assertOkStatus(); r.assertChange(Change.Status.MERGED, null, admin); Change c = Iterables.getOnlyElement(db.changes().byKey( new Change.Key(r.getChangeId())).toList()); assertEquals(2, db.patchSets().byChange(c.getId()).toList().size()); assertSubmitApproval(r.getPatchSetId()); assertCommit(project, "refs/heads/master"); } @Test public void submitOnPushNotAllowed_Error() throws GitAPIException, OrmException, IOException { PushOneCommit.Result r = pushTo("refs/for/master%submit"); r.assertErrorStatus("submit not allowed"); } @Test public void submitOnPushNewPatchSetNotAllowed_Error() throws GitAPIException, OrmException, IOException, ConfigInvalidException { PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content"); r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt", "other content", r.getChangeId()); r.assertErrorStatus("submit not allowed"); } @Test public void submitOnPushingDraft_Error() throws GitAPIException, OrmException, IOException { PushOneCommit.Result r = pushTo("refs/for/master%draft,submit"); r.assertErrorStatus("cannot submit draft"); } @Test public void submitOnPushToNonExistingBranch_Error() throws GitAPIException, OrmException, IOException { String branchName = "non-existing"; PushOneCommit.Result r = pushTo("refs/for/" + branchName + "%submit"); r.assertErrorStatus("branch " + branchName + " not found"); } @Test public void mergeOnPushToBranch() throws Exception { grant(Permission.PUSH, project, "refs/heads/master"); PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content"); r.assertOkStatus(); git.push() .setRefSpecs(new RefSpec(r.getCommitId().name() + ":refs/heads/master")) .call(); assertCommit(project, "refs/heads/master"); assertNull(getSubmitter(r.getPatchSetId())); Change c = db.changes().get(r.getPatchSetId().getParentKey()); assertEquals(Change.Status.MERGED, c.getStatus()); } private void grant(String permission, Project.NameKey project, String ref) throws RepositoryNotFoundException, IOException, ConfigInvalidException { MetaDataUpdate md = metaDataUpdateFactory.create(project); md.setMessage(String.format("Grant %s on %s", permission, ref)); ProjectConfig config = ProjectConfig.read(md); AccessSection s = config.getAccessSection(ref, true); Permission p = s.getPermission(permission, true); AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators")); p.add(new PermissionRule(config.resolve(adminGroup))); config.commit(md); projectCache.evict(config.getProject()); } private PatchSetApproval getSubmitter(PatchSet.Id patchSetId) throws OrmException { Change c = db.changes().get(patchSetId.getParentKey()); ChangeNotes notes = changeNotesFactory.create(c).load(); return approvalsUtil.getSubmitter(db, notes, patchSetId); } private void assertSubmitApproval(PatchSet.Id patchSetId) throws OrmException { PatchSetApproval a = getSubmitter(patchSetId); assertTrue(a.isSubmit()); assertEquals(1, a.getValue()); assertEquals(admin.id, a.getAccountId()); } private void assertCommit(Project.NameKey project, String branch) throws IOException { Repository r = repoManager.openRepository(project); try { RevWalk rw = new RevWalk(r); try { RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId()); assertEquals(PushOneCommit.SUBJECT, c.getShortMessage()); assertEquals(admin.email, c.getAuthorIdent().getEmailAddress()); assertEquals(admin.email, c.getCommitterIdent().getEmailAddress()); } finally { rw.release(); } } finally { r.close(); } } private void assertMergeCommit(String branch, String subject) throws IOException { Repository r = repoManager.openRepository(project); try { RevWalk rw = new RevWalk(r); try { RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId()); assertEquals(2, c.getParentCount()); assertEquals("Merge \"" + subject + "\"", c.getShortMessage()); assertEquals(admin.email, c.getAuthorIdent().getEmailAddress()); assertEquals(serverIdent.getEmailAddress(), c.getCommitterIdent().getEmailAddress()); } finally { rw.release(); } } finally { r.close(); } } private void assertTag(Project.NameKey project, String branch, String tagName) throws IOException { Repository r = repoManager.openRepository(project); try { ObjectId headCommit = r.getRef(branch).getObjectId(); ObjectId taggedCommit = r.getRef(tagName).getObjectId(); assertEquals(headCommit, taggedCommit); } finally { r.close(); } } private PushOneCommit.Result pushTo(String ref) throws GitAPIException, IOException { PushOneCommit push = pushFactory.create(db, admin.getIdent()); return push.to(git, ref); } private PushOneCommit.Result push(String ref, String subject, String fileName, String content) throws GitAPIException, IOException { PushOneCommit push = pushFactory.create(db, admin.getIdent(), subject, fileName, content); return push.to(git, ref); } private PushOneCommit.Result push(String ref, String subject, String fileName, String content, String changeId) throws GitAPIException, IOException { PushOneCommit push = pushFactory.create(db, admin.getIdent(), subject, fileName, content, changeId); return push.to(git, ref); } }