Introduce SubmitPreview REST API call
Before submitting a whole topic or submission including parents existed and the usage of Superproject subscriptions was low, it was easy to predict, what would happen if a change was submitted as it was just one change that was submitted. This is changing as the submission process gets bigger unknowns by having coupling of changes via their ancestors, topic submissions and superproject subscriptions. We would like to provide aid answering "What would happen if I submit this change?" even more than just the submitted together tab, as that would not show e.g. superproject subscriptions or the underlying Git DAG. In case of merging, this also doesn't show the exact tree afterwards. This introduces a REST API call that will show exactly what will happen by providing the exact Git DAG that would be produced if a change was submitted now. Requirements for such a call: * It has to be capable of dealing with multiple projects. (superproject subscriptions, submitting topics across projects) * easy to deal with on the client side, while transporting the whole Git DAG. This call returns a zip file that contains thin bundles which can be pulled from to see the exact state after a hypothetical submission. As projects can be nested (e.g. project and project/foo), we cannot just name the bundles as the projects as that may produce a directory/file conflict inside the zip file. Projects have limitations on how they can be named, which is enforced upon project creation in the LocalDiskRepository class. One of the rules is that no project name contains ".git/", which makes ".git" a suffix that will guarantee no directory/file conflicts, which is why the names are chosen to be "${projectname}.git" In case of an error, no zip file is returned, but just a plain message string. We considered having a "dry run" submission process, that allows submission of a change with a prefix to the target branch(es). As an example: You would call submit_with_prefix(change-id=234, prefix=refs/testing/) which would create a branch refs/testing/<change target branch> which is updated to what that branch would look like. But also a superproject would get a refs/testing/<superproject branch> which could be inspected to what actually happened. That has some disadvantages: * Ref updates are expensive in Gerrit on Googles infrastructure as ref updates are replicated across the globe. For such testing refs we do not need a strong replication as we can reproduce them any time. * The ACLs are unclear for these testing branches (infer them from the actual branches?) * We’d need to cleanup these testing branches regularly (time to live of e.g. 1 week?) * Making sure the prefix is unique for all projects that are involved before test submission Change-Id: I3c976257c2b20de32373cbade9b99811586e926c Signed-off-by: Stefan Beller <sbeller@google.com>
This commit is contained in:
parent
6ac85596f5
commit
dfa1ef377d
@ -3257,6 +3257,59 @@ Query parameter `download` (e.g. `/changes/.../patch?download`)
|
||||
will suggest the browser save the patch as `commitsha1.diff.base64`,
|
||||
for later processing by command line tools.
|
||||
|
||||
[[submit-preview]]
|
||||
===Submit Preview
|
||||
--
|
||||
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/preview_submit'
|
||||
--
|
||||
Gets a file containing thin bundles of all modified projects if this
|
||||
change was submitted. The bundles are named `${ProjectName}.git`.
|
||||
Each thin bundle contains enough to construct the state in which a project would
|
||||
be in if this change were submitted. The base of the thin bundles are the
|
||||
current target branches, so to make use of this call in a non-racy way, first
|
||||
get the bundles and then fetch all projects contained in the bundle.
|
||||
(This assumes no non-fastforward pushes).
|
||||
|
||||
You need to give a parameter '?format=zip' or '?format=tar' to specify the
|
||||
format for the outer container.
|
||||
|
||||
To make good use of this call, you would roughly need code as found at:
|
||||
----
|
||||
$ curl -Lo preview_submit_test.sh http://review.example.com:8080/tools/scripts/preview_submit_test.sh
|
||||
----
|
||||
.Request
|
||||
----
|
||||
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/preview_submit?zip HTTP/1.0
|
||||
----
|
||||
|
||||
.Response
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Date: Tue, 13 Sep 2016 19:13:46 GMT
|
||||
Content-Disposition: attachment; filename="submit-preview-147.zip"
|
||||
X-Content-Type-Options: nosniff
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: Mon, 01 Jan 1990 00:00:00 GMT
|
||||
Content-Type: application/x-zip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
[binary stuff]
|
||||
----
|
||||
|
||||
In case of an error, the response is not a zip file but a regular json response,
|
||||
containing only the error message:
|
||||
|
||||
.Response
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Content-Disposition: attachment
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
|
||||
)]}'
|
||||
"Anonymous users cannot submit"
|
||||
----
|
||||
|
||||
[[get-mergeable]]
|
||||
=== Get Mergeable
|
||||
--
|
||||
|
@ -26,6 +26,7 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Iterators;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.primitives.Chars;
|
||||
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
|
||||
@ -49,6 +50,7 @@ import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.common.ActionInfo;
|
||||
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||
import com.google.gerrit.extensions.common.EditInfo;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.IdString;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||
@ -103,12 +105,19 @@ import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
||||
import org.eclipse.jgit.junit.TestRepository;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.NullProgressMonitor;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevTree;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.transport.FetchResult;
|
||||
import org.eclipse.jgit.transport.RefSpec;
|
||||
import org.eclipse.jgit.transport.Transport;
|
||||
import org.eclipse.jgit.transport.TransportBundleStream;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
@ -120,14 +129,20 @@ import org.junit.runner.Description;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
@RunWith(ConfigSuite.class)
|
||||
public abstract class AbstractDaemonTest {
|
||||
@ -949,7 +964,8 @@ public abstract class AbstractDaemonTest {
|
||||
|
||||
protected RevCommit getHead(Repository repo, String name) throws Exception {
|
||||
try (RevWalk rw = new RevWalk(repo)) {
|
||||
return rw.parseCommit(repo.exactRef(name).getObjectId());
|
||||
Ref r = repo.exactRef(name);
|
||||
return r != null ? rw.parseCommit(r.getObjectId()) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1008,4 +1024,73 @@ public abstract class AbstractDaemonTest {
|
||||
saveProjectConfig(allProjects, cfg);
|
||||
return ca;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches each bundle into a newly cloned repository, then it applies
|
||||
* the bundle, and returns the resulting tree id.
|
||||
*/
|
||||
protected Map<Branch.NameKey, RevTree>
|
||||
fetchFromBundles(BinaryResult bundles) throws Exception {
|
||||
|
||||
assertThat(bundles.getContentType()).isEqualTo("application/x-zip");
|
||||
|
||||
File tempfile = File.createTempFile("test", null);
|
||||
bundles.writeTo(new FileOutputStream(tempfile));
|
||||
|
||||
Map<Branch.NameKey, RevTree> ret = new HashMap<>();
|
||||
try (ZipFile readback = new ZipFile(tempfile);) {
|
||||
for (ZipEntry entry : ImmutableList.copyOf(
|
||||
Iterators.forEnumeration(readback.entries()))) {
|
||||
String bundleName = entry.getName();
|
||||
InputStream bundleStream = readback.getInputStream(entry);
|
||||
|
||||
int len = bundleName.length();
|
||||
assertThat(bundleName).endsWith(".git");
|
||||
String repoName = bundleName.substring(0, len - 4);
|
||||
Project.NameKey proj = new Project.NameKey(repoName);
|
||||
TestRepository<?> localRepo = cloneProject(proj);
|
||||
|
||||
try (TransportBundleStream tbs = new TransportBundleStream(
|
||||
localRepo.getRepository(), new URIish(bundleName), bundleStream);) {
|
||||
|
||||
FetchResult fr = tbs.fetch(NullProgressMonitor.INSTANCE,
|
||||
Arrays.asList(new RefSpec("refs/*:refs/preview/*")));
|
||||
for (Ref r : fr.getAdvertisedRefs()) {
|
||||
String branchName = r.getName();
|
||||
Branch.NameKey n = new Branch.NameKey(proj, branchName);
|
||||
|
||||
RevCommit c = localRepo.getRevWalk().parseCommit(r.getObjectId());
|
||||
ret.put(n, c.getTree());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given branches have the given tree ids.
|
||||
*/
|
||||
protected void assertRevTrees(Project.NameKey proj,
|
||||
Map<Branch.NameKey, RevTree> trees) throws Exception {
|
||||
TestRepository<?> localRepo = cloneProject(proj);
|
||||
GitUtil.fetch(localRepo, "refs/*:refs/*");
|
||||
Map<String, Ref> refs = localRepo.getRepository().getAllRefs();
|
||||
Map<Branch.NameKey, RevTree> refValues = new HashMap<>();
|
||||
|
||||
for (Branch.NameKey b : trees.keySet()) {
|
||||
if (!b.getParentKey().equals(proj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Ref r = refs.get(b.get());
|
||||
assertThat(r).isNotNull();
|
||||
RevWalk rw = localRepo.getRevWalk();
|
||||
RevCommit c = rw.parseCommit(r.getObjectId());
|
||||
refValues.put(b, c.getTree());
|
||||
|
||||
assertThat(trees.get(b)).isEqualTo(refValues.get(b));
|
||||
}
|
||||
assertThat(refValues.keySet()).containsAnyIn(trees.keySet());
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ acceptance_tests(
|
||||
deps = [
|
||||
':submodule_util',
|
||||
':push_for_review',
|
||||
'//gerrit-extension-api:api',
|
||||
],
|
||||
labels = ['git'],
|
||||
)
|
||||
|
@ -21,15 +21,22 @@ import static com.google.gerrit.acceptance.GitUtil.getChangeId;
|
||||
import com.google.gerrit.acceptance.NoHttpd;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||
import com.google.gerrit.extensions.client.ChangeStatus;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.testutil.ConfigSuite;
|
||||
|
||||
import org.eclipse.jgit.junit.TestRepository;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevTree;
|
||||
import org.eclipse.jgit.transport.RefSpec;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@NoHttpd
|
||||
public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
extends AbstractSubmoduleSubscription {
|
||||
@ -101,22 +108,50 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
gApi.changes().id(id2).current().review(ReviewInput.approve());
|
||||
gApi.changes().id(id3).current().review(ReviewInput.approve());
|
||||
|
||||
BinaryResult request = gApi.changes().id(id1).current().submitPreview();
|
||||
Map<Branch.NameKey, RevTree> preview =
|
||||
fetchFromBundles(request);
|
||||
|
||||
gApi.changes().id(id1).current().submit();
|
||||
ObjectId subRepoId = subRepo.git().fetch().setRemote("origin").call()
|
||||
.getAdvertisedRef("refs/heads/master").getObjectId();
|
||||
|
||||
expectToHaveSubmoduleState(superRepo, "master",
|
||||
"subscribed-to-project", subRepoId);
|
||||
|
||||
// As the submodules have changed commits, the superproject tree will be
|
||||
// different, so we cannot directly compare the trees here, so make
|
||||
// assumptions only about the changed branches:
|
||||
Project.NameKey p1 = new Project.NameKey(name("super-project"));
|
||||
Project.NameKey p2 = new Project.NameKey(name("subscribed-to-project"));
|
||||
assertThat(preview).containsKey(
|
||||
new Branch.NameKey(p1, "refs/heads/master"));
|
||||
assertThat(preview).containsKey(
|
||||
new Branch.NameKey(p2, "refs/heads/master"));
|
||||
|
||||
if (getSubmitType() == SubmitType.CHERRY_PICK) {
|
||||
// each change is updated and the respective target branch is updated:
|
||||
assertThat(preview).hasSize(5);
|
||||
} else if (getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
|
||||
// Either the first is used first as is, then the second and third need
|
||||
// rebasing, or those two stay as is and the first is rebased.
|
||||
// add in 2 master branches, expect 3 or 4:
|
||||
assertThat(preview.size()).isAnyOf(3, 4);
|
||||
} else {
|
||||
assertThat(preview).hasSize(2);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubscriptionUpdateIncludingChangeInSuperproject() throws Exception {
|
||||
public void testSubscriptionUpdateIncludingChangeInSuperproject()
|
||||
throws Exception {
|
||||
TestRepository<?> superRepo = createProjectWithPush("super-project");
|
||||
TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
|
||||
allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
|
||||
"super-project", "refs/heads/master");
|
||||
allowMatchingSubmoduleSubscription("subscribed-to-project",
|
||||
"refs/heads/master", "super-project", "refs/heads/master");
|
||||
|
||||
createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
|
||||
createSubmoduleSubscription(superRepo, "master",
|
||||
"subscribed-to-project", "master");
|
||||
|
||||
ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
|
||||
.message("some change")
|
||||
@ -310,7 +345,8 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
|
||||
|
||||
ObjectId bottomHead =
|
||||
pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
|
||||
pushChangeTo(bottomRepo, "refs/for/master",
|
||||
"some message", "same-topic");
|
||||
ObjectId topHead =
|
||||
pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
|
||||
|
||||
@ -322,8 +358,10 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
|
||||
gApi.changes().id(id1).current().submit();
|
||||
|
||||
expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
|
||||
expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
|
||||
expectToHaveSubmoduleState(midRepo, "master", "bottom-project",
|
||||
bottomRepo, "master");
|
||||
expectToHaveSubmoduleState(topRepo, "master", "mid-project",
|
||||
midRepo, "master");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -346,7 +384,8 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
pushSubmoduleConfig(topRepo, "master", config);
|
||||
|
||||
ObjectId bottomHead =
|
||||
pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
|
||||
pushChangeTo(bottomRepo, "refs/for/master",
|
||||
"some message", "same-topic");
|
||||
ObjectId topHead =
|
||||
pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
|
||||
|
||||
@ -358,13 +397,16 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
|
||||
gApi.changes().id(id1).current().submit();
|
||||
|
||||
expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
|
||||
expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
|
||||
expectToHaveSubmoduleState(topRepo, "master", "bottom-project", bottomRepo, "master");
|
||||
expectToHaveSubmoduleState(midRepo, "master",
|
||||
"bottom-project", bottomRepo, "master");
|
||||
expectToHaveSubmoduleState(topRepo, "master",
|
||||
"mid-project", midRepo, "master");
|
||||
expectToHaveSubmoduleState(topRepo, "master",
|
||||
"bottom-project", bottomRepo, "master");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBranchCircularSubscription() throws Exception {
|
||||
|
||||
private String prepareBranchCircularSubscription() throws Exception {
|
||||
TestRepository<?> topRepo = createProjectWithPush("top-project");
|
||||
TestRepository<?> midRepo = createProjectWithPush("mid-project");
|
||||
TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
|
||||
@ -385,15 +427,23 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
|
||||
|
||||
approve(changeId);
|
||||
|
||||
exception.expectMessage("Branch level circular subscriptions detected");
|
||||
exception.expectMessage("top-project,refs/heads/master");
|
||||
exception.expectMessage("mid-project,refs/heads/master");
|
||||
exception.expectMessage("bottom-project,refs/heads/master");
|
||||
gApi.changes().id(changeId).current().submit();
|
||||
return changeId;
|
||||
}
|
||||
|
||||
assertThat(hasSubmodule(midRepo, "master", "bottom-project")).isFalse();
|
||||
assertThat(hasSubmodule(topRepo, "master", "mid-project")).isFalse();
|
||||
@Test
|
||||
public void testBranchCircularSubscription() throws Exception {
|
||||
String changeId = prepareBranchCircularSubscription();
|
||||
gApi.changes().id(changeId).current().submit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBranchCircularSubscriptionPreview() throws Exception {
|
||||
String changeId = prepareBranchCircularSubscription();
|
||||
gApi.changes().id(changeId).current().submitPreview();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -401,8 +451,8 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
|
||||
TestRepository<?> superRepo = createProjectWithPush("super-project");
|
||||
TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
|
||||
|
||||
allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
|
||||
"super-project", "refs/heads/master");
|
||||
allowMatchingSubmoduleSubscription("subscribed-to-project",
|
||||
"refs/heads/master", "super-project", "refs/heads/master");
|
||||
allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev",
|
||||
"subscribed-to-project", "refs/heads/dev");
|
||||
|
||||
|
@ -43,6 +43,7 @@ import com.google.gerrit.extensions.common.ChangeInfo;
|
||||
import com.google.gerrit.extensions.common.ChangeMessageInfo;
|
||||
import com.google.gerrit.extensions.common.LabelInfo;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.extensions.webui.UiAction;
|
||||
@ -71,13 +72,16 @@ import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevTree;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@NoHttpd
|
||||
public abstract class AbstractSubmit extends AbstractDaemonTest {
|
||||
@ -116,9 +120,155 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
|
||||
@Test
|
||||
@TestProjectInput(createEmptyCommit = false)
|
||||
public void submitToEmptyRepo() throws Exception {
|
||||
RevCommit initialHead = getRemoteHead();
|
||||
PushOneCommit.Result change = createChange();
|
||||
BinaryResult request = submitPreview(change.getChangeId());
|
||||
RevCommit headAfterSubmitPreview = getRemoteHead();
|
||||
assertThat(headAfterSubmitPreview).isEqualTo(initialHead);
|
||||
Map<Branch.NameKey, RevTree> actual =
|
||||
fetchFromBundles(request);
|
||||
assertThat(actual).hasSize(1);
|
||||
|
||||
submit(change.getChangeId());
|
||||
assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
|
||||
assertRevTrees(project, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submitSingleChange() throws Exception {
|
||||
RevCommit initialHead = getRemoteHead();
|
||||
PushOneCommit.Result change = createChange();
|
||||
BinaryResult request = submitPreview(change.getChangeId());
|
||||
RevCommit headAfterSubmit = getRemoteHead();
|
||||
assertThat(headAfterSubmit).isEqualTo(initialHead);
|
||||
assertRefUpdatedEvents();
|
||||
assertChangeMergedEvents();
|
||||
|
||||
Map<Branch.NameKey, RevTree> actual =
|
||||
fetchFromBundles(request);
|
||||
|
||||
if (getSubmitType() == SubmitType.CHERRY_PICK) {
|
||||
// The change is updated as well:
|
||||
assertThat(actual).hasSize(2);
|
||||
} else {
|
||||
assertThat(actual).hasSize(1);
|
||||
}
|
||||
|
||||
submit(change.getChangeId());
|
||||
assertRevTrees(project, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submitMultipleChangesOtherMergeConflictPreview()
|
||||
throws Exception {
|
||||
RevCommit initialHead = getRemoteHead();
|
||||
|
||||
PushOneCommit.Result change =
|
||||
createChange("Change 1", "a.txt", "content");
|
||||
submit(change.getChangeId());
|
||||
|
||||
RevCommit headAfterFirstSubmit = getRemoteHead();
|
||||
testRepo.reset(initialHead);
|
||||
PushOneCommit.Result change2 = createChange("Change 2",
|
||||
"a.txt", "other content");
|
||||
PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
|
||||
PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
|
||||
// change 2 is not approved, but we ignore labels
|
||||
approve(change3.getChangeId());
|
||||
BinaryResult request = null;
|
||||
String msg = null;
|
||||
try {
|
||||
request = submitPreview(change4.getChangeId());
|
||||
} catch (Exception e) {
|
||||
msg = e.getMessage();
|
||||
}
|
||||
|
||||
if (getSubmitType() == SubmitType.CHERRY_PICK) {
|
||||
Map<Branch.NameKey, RevTree> s =
|
||||
fetchFromBundles(request);
|
||||
submit(change4.getChangeId());
|
||||
assertRevTrees(project, s);
|
||||
} else if (getSubmitType() == SubmitType.FAST_FORWARD_ONLY) {
|
||||
assertThat(msg).isEqualTo(
|
||||
"Failed to submit 3 changes due to the following problems:\n" +
|
||||
"Change " + change2.getChange().getId() + ": internal error: " +
|
||||
"change not processed by merge strategy\n" +
|
||||
"Change " + change3.getChange().getId() + ": internal error: " +
|
||||
"change not processed by merge strategy\n" +
|
||||
"Change " + change4.getChange().getId() + ": Project policy " +
|
||||
"requires all submissions to be a fast-forward. Please " +
|
||||
"rebase the change locally and upload again for review.");
|
||||
RevCommit headAfterSubmit = getRemoteHead();
|
||||
assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
|
||||
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
|
||||
assertChangeMergedEvents(change.getChangeId(),
|
||||
headAfterFirstSubmit.name());
|
||||
} else if(getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
|
||||
String change2hash = change2.getChange().currentPatchSet()
|
||||
.getRevision().get();
|
||||
assertThat(msg).isEqualTo(
|
||||
"Cannot rebase " + change2hash + ": The change could " +
|
||||
"not be rebased due to a conflict during merge.");
|
||||
RevCommit headAfterSubmit = getRemoteHead();
|
||||
assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
|
||||
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
|
||||
assertChangeMergedEvents(change.getChangeId(),
|
||||
headAfterFirstSubmit.name());
|
||||
} else {
|
||||
assertThat(msg).isEqualTo(
|
||||
"Failed to submit 3 changes due to the following problems:\n" +
|
||||
"Change " + change2.getChange().getId() + ": Change could not be " +
|
||||
"merged due to a path conflict. Please rebase the change " +
|
||||
"locally and upload the rebased commit for review.\n" +
|
||||
"Change " + change3.getChange().getId() + ": Change could not be " +
|
||||
"merged due to a path conflict. Please rebase the change " +
|
||||
"locally and upload the rebased commit for review.\n" +
|
||||
"Change " + change4.getChange().getId() + ": Change could not be " +
|
||||
"merged due to a path conflict. Please rebase the change " +
|
||||
"locally and upload the rebased commit for review.");
|
||||
RevCommit headAfterSubmit = getRemoteHead();
|
||||
assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
|
||||
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
|
||||
assertChangeMergedEvents(change.getChangeId(),
|
||||
headAfterFirstSubmit.name());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submitMultipleChangesPreview() throws Exception {
|
||||
RevCommit initialHead = getRemoteHead();
|
||||
PushOneCommit.Result change2 = createChange("Change 2",
|
||||
"a.txt", "other content");
|
||||
PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
|
||||
PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
|
||||
// change 2 is not approved, but we ignore labels
|
||||
approve(change3.getChangeId());
|
||||
BinaryResult request = submitPreview(change4.getChangeId());
|
||||
|
||||
Map<String, Map<String, Integer>> expected = new HashMap<>();
|
||||
expected.put(project.get(), new HashMap<String, Integer>());
|
||||
expected.get(project.get()).put("refs/heads/master", 3);
|
||||
Map<Branch.NameKey, RevTree> actual =
|
||||
fetchFromBundles(request);
|
||||
|
||||
assertThat(actual).containsKey(
|
||||
new Branch.NameKey(project, "refs/heads/master"));
|
||||
if (getSubmitType() == SubmitType.CHERRY_PICK) {
|
||||
assertThat(actual).hasSize(2);
|
||||
} else {
|
||||
assertThat(actual).hasSize(1);
|
||||
}
|
||||
|
||||
// check that the submit preview did not actually submit
|
||||
RevCommit headAfterSubmit = getRemoteHead();
|
||||
assertThat(headAfterSubmit).isEqualTo(initialHead);
|
||||
assertRefUpdatedEvents();
|
||||
assertChangeMergedEvents();
|
||||
|
||||
// now check we actually have the same content:
|
||||
approve(change2.getChangeId());
|
||||
submit(change4.getChangeId());
|
||||
assertRevTrees(project, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -326,6 +476,10 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
|
||||
assertMerged(change.changeId);
|
||||
}
|
||||
|
||||
protected BinaryResult submitPreview(String changeId) throws Exception {
|
||||
return gApi.changes().id(changeId).current().submitPreview();
|
||||
}
|
||||
|
||||
protected void assertSubmittable(String changeId) throws Exception {
|
||||
assertThat(gApi.changes().id(changeId).info().submittable)
|
||||
.named("submit bit on ChangeInfo")
|
||||
|
@ -15,6 +15,7 @@
|
||||
package com.google.gerrit.acceptance.rest.change;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.google.gerrit.acceptance.GitUtil;
|
||||
import com.google.gerrit.acceptance.PushOneCommit;
|
||||
@ -24,14 +25,19 @@ import com.google.gerrit.extensions.api.changes.CherryPickInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||
import com.google.gerrit.extensions.api.projects.BranchInput;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
|
||||
import org.eclipse.jgit.junit.TestRepository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevTree;
|
||||
import org.eclipse.jgit.transport.RefSpec;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
|
||||
|
||||
@ -144,6 +150,12 @@ public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
|
||||
approve(change2a.getChangeId());
|
||||
approve(change2b.getChangeId());
|
||||
approve(change3.getChangeId());
|
||||
|
||||
// get a preview before submitting:
|
||||
BinaryResult request = submitPreview(change1b.getChangeId());
|
||||
Map<Branch.NameKey, RevTree> preview =
|
||||
fetchFromBundles(request);
|
||||
|
||||
submit(change1b.getChangeId());
|
||||
|
||||
RevCommit tip1 = getRemoteLog(p1, "master").get(0);
|
||||
@ -158,11 +170,28 @@ public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
|
||||
change2b.getCommit().getShortMessage());
|
||||
assertThat(tip3.getShortMessage()).isEqualTo(
|
||||
change3.getCommit().getShortMessage());
|
||||
|
||||
// check that the preview matched what happened:
|
||||
assertThat(preview).hasSize(3);
|
||||
|
||||
assertThat(preview).containsKey(
|
||||
new Branch.NameKey(p1, "refs/heads/master"));
|
||||
assertRevTrees(p1, preview);
|
||||
|
||||
assertThat(preview).containsKey(
|
||||
new Branch.NameKey(p2, "refs/heads/master"));
|
||||
assertRevTrees(p2, preview);
|
||||
|
||||
assertThat(preview).containsKey(
|
||||
new Branch.NameKey(p3, "refs/heads/master"));
|
||||
assertRevTrees(p3, preview);
|
||||
} else {
|
||||
assertThat(tip2.getShortMessage()).isEqualTo(
|
||||
initialHead2.getShortMessage());
|
||||
assertThat(tip3.getShortMessage()).isEqualTo(
|
||||
initialHead3.getShortMessage());
|
||||
assertThat(preview).hasSize(1);
|
||||
assertThat(preview.get(new Branch.NameKey(p1, "refs/heads/master"))).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,11 +244,23 @@ public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
|
||||
approve(change3.getChangeId());
|
||||
|
||||
if (isSubmitWholeTopicEnabled()) {
|
||||
submitWithConflict(change1b.getChangeId(),
|
||||
String msg =
|
||||
"Failed to submit 5 changes due to the following problems:\n" +
|
||||
"Change " + change3.getChange().getId() + ": Change could not be " +
|
||||
"merged due to a path conflict. Please rebase the change locally " +
|
||||
"and upload the rebased commit for review.");
|
||||
"and upload the rebased commit for review.";
|
||||
|
||||
// Get a preview before submitting:
|
||||
try {
|
||||
// We cannot just use the ExpectedException infrastructure as provided
|
||||
// by AbstractDaemonTest, as then we'd stop early and not test the
|
||||
// actual submit.
|
||||
submitPreview(change1b.getChangeId());
|
||||
fail("expected failure");
|
||||
} catch (RestApiException e) {
|
||||
assertThat(e.getMessage()).isEqualTo(msg);
|
||||
}
|
||||
submitWithConflict(change1b.getChangeId(), msg);
|
||||
} else {
|
||||
submit(change1b.getChangeId());
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ public interface RevisionApi {
|
||||
|
||||
void submit() throws RestApiException;
|
||||
void submit(SubmitInput in) throws RestApiException;
|
||||
BinaryResult submitPreview() throws RestApiException;
|
||||
void publish() throws RestApiException;
|
||||
ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
|
||||
ChangeApi rebase() throws RestApiException;
|
||||
@ -240,6 +241,11 @@ public interface RevisionApi {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryResult submitPreview() throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubmitType testSubmitType(TestSubmitRuleInput in)
|
||||
throws RestApiException {
|
||||
|
@ -58,6 +58,7 @@ import com.google.gerrit.server.change.RebaseUtil;
|
||||
import com.google.gerrit.server.change.Reviewed;
|
||||
import com.google.gerrit.server.change.RevisionResource;
|
||||
import com.google.gerrit.server.change.Submit;
|
||||
import com.google.gerrit.server.change.PreviewSubmit;
|
||||
import com.google.gerrit.server.change.TestSubmitType;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
@ -88,6 +89,7 @@ class RevisionApiImpl implements RevisionApi {
|
||||
private final Rebase rebase;
|
||||
private final RebaseUtil rebaseUtil;
|
||||
private final Submit submit;
|
||||
private final PreviewSubmit submitPreview;
|
||||
private final PublishDraftPatchSet publish;
|
||||
private final Reviewed.PutReviewed putReviewed;
|
||||
private final Reviewed.DeleteReviewed deleteReviewed;
|
||||
@ -118,6 +120,7 @@ class RevisionApiImpl implements RevisionApi {
|
||||
Rebase rebase,
|
||||
RebaseUtil rebaseUtil,
|
||||
Submit submit,
|
||||
PreviewSubmit submitPreview,
|
||||
PublishDraftPatchSet publish,
|
||||
Reviewed.PutReviewed putReviewed,
|
||||
Reviewed.DeleteReviewed deleteReviewed,
|
||||
@ -147,6 +150,7 @@ class RevisionApiImpl implements RevisionApi {
|
||||
this.rebaseUtil = rebaseUtil;
|
||||
this.review = review;
|
||||
this.submit = submit;
|
||||
this.submitPreview = submitPreview;
|
||||
this.publish = publish;
|
||||
this.files = files;
|
||||
this.putReviewed = putReviewed;
|
||||
@ -193,6 +197,12 @@ class RevisionApiImpl implements RevisionApi {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryResult submitPreview() throws RestApiException {
|
||||
submitPreview.setFormat("zip");
|
||||
return submitPreview.apply(revision);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish() throws RestApiException {
|
||||
try {
|
||||
|
@ -14,6 +14,10 @@
|
||||
|
||||
package com.google.gerrit.server.change;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.ArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.eclipse.jgit.api.ArchiveCommand;
|
||||
import org.eclipse.jgit.archive.TarFormat;
|
||||
import org.eclipse.jgit.archive.Tbz2Format;
|
||||
@ -21,12 +25,40 @@ import org.eclipse.jgit.archive.TgzFormat;
|
||||
import org.eclipse.jgit.archive.TxzFormat;
|
||||
import org.eclipse.jgit.archive.ZipFormat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public enum ArchiveFormat {
|
||||
TGZ("application/x-gzip", new TgzFormat()),
|
||||
TAR("application/x-tar", new TarFormat()),
|
||||
TBZ2("application/x-bzip2", new Tbz2Format()),
|
||||
TXZ("application/x-xz", new TxzFormat()),
|
||||
ZIP("application/x-zip", new ZipFormat());
|
||||
TGZ("application/x-gzip", new TgzFormat()) {
|
||||
@Override
|
||||
public ArchiveEntry prepareArchiveEntry(String fileName) {
|
||||
return new TarArchiveEntry(fileName);
|
||||
}
|
||||
},
|
||||
TAR("application/x-tar", new TarFormat()) {
|
||||
@Override
|
||||
public ArchiveEntry prepareArchiveEntry(String fileName) {
|
||||
return new TarArchiveEntry(fileName);
|
||||
}
|
||||
},
|
||||
TBZ2("application/x-bzip2", new Tbz2Format()) {
|
||||
@Override
|
||||
public ArchiveEntry prepareArchiveEntry(String fileName) {
|
||||
return new TarArchiveEntry(fileName);
|
||||
}
|
||||
},
|
||||
TXZ("application/x-xz", new TxzFormat()) {
|
||||
@Override
|
||||
public ArchiveEntry prepareArchiveEntry(String fileName) {
|
||||
return new TarArchiveEntry(fileName);
|
||||
}
|
||||
},
|
||||
ZIP("application/x-zip", new ZipFormat()) {
|
||||
@Override
|
||||
public ArchiveEntry prepareArchiveEntry(String fileName) {
|
||||
return new ZipArchiveEntry(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
private final ArchiveCommand.Format<?> format;
|
||||
private final String mimeType;
|
||||
@ -52,4 +84,11 @@ public enum ArchiveFormat {
|
||||
Iterable<String> getSuffixes() {
|
||||
return format.suffixes();
|
||||
}
|
||||
|
||||
public ArchiveOutputStream createArchiveOutputStream(OutputStream o)
|
||||
throws IOException {
|
||||
return (ArchiveOutputStream)this.format.createArchiveOutputStream(o);
|
||||
}
|
||||
|
||||
public abstract ArchiveEntry prepareArchiveEntry(final String fileName);
|
||||
}
|
@ -93,6 +93,7 @@ public class Module extends RestApiModule {
|
||||
get(REVISION_KIND, "related").to(GetRelated.class);
|
||||
get(REVISION_KIND, "review").to(GetReview.class);
|
||||
post(REVISION_KIND, "review").to(PostReview.class);
|
||||
get(REVISION_KIND, "preview_submit").to(PreviewSubmit.class);
|
||||
post(REVISION_KIND, "submit").to(Submit.class);
|
||||
post(REVISION_KIND, "rebase").to(Rebase.class);
|
||||
get(REVISION_KIND, "patch").to(GetPatch.class);
|
||||
|
@ -0,0 +1,143 @@
|
||||
// Copyright (C) 2016 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.change;
|
||||
|
||||
import org.eclipse.jgit.lib.NullProgressMonitor;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.gerrit.extensions.api.changes.SubmitInput;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
||||
import com.google.gerrit.extensions.restapi.PreconditionFailedException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.git.MergeOp;
|
||||
import com.google.gerrit.server.git.MergeOpRepoManager;
|
||||
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.eclipse.jgit.transport.BundleWriter;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
import org.kohsuke.args4j.Option;
|
||||
import org.apache.commons.compress.archivers.ArchiveOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
@Singleton
|
||||
public class PreviewSubmit implements RestReadView<RevisionResource> {
|
||||
private final Provider<ReviewDb> dbProvider;
|
||||
private final Provider<MergeOp> mergeOpProvider;
|
||||
private final AllowedFormats allowedFormats;
|
||||
|
||||
private String format;
|
||||
|
||||
@Option(name = "--format")
|
||||
public void setFormat(String f) {
|
||||
this.format = f;
|
||||
}
|
||||
|
||||
@Inject
|
||||
PreviewSubmit(Provider<ReviewDb> dbProvider,
|
||||
Provider<MergeOp> mergeOpProvider,
|
||||
AllowedFormats allowedFormats) {
|
||||
this.dbProvider = dbProvider;
|
||||
this.mergeOpProvider = mergeOpProvider;
|
||||
this.allowedFormats = allowedFormats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryResult apply(RevisionResource rsrc) throws RestApiException {
|
||||
if (Strings.isNullOrEmpty(format)) {
|
||||
throw new BadRequestException("format is not specified");
|
||||
}
|
||||
ArchiveFormat f = allowedFormats.extensions.get("." + format);
|
||||
if (f == null) {
|
||||
throw new BadRequestException("unknown archive format");
|
||||
}
|
||||
|
||||
Change change = rsrc.getChange();
|
||||
if (!change.getStatus().isOpen()) {
|
||||
throw new PreconditionFailedException("change is " + Submit.status(change));
|
||||
}
|
||||
ChangeControl control = rsrc.getControl();
|
||||
if (!control.getUser().isIdentifiedUser()) {
|
||||
throw new MethodNotAllowedException("Anonymous users cannot submit");
|
||||
}
|
||||
try (BinaryResult b = getBundles(rsrc, f)) {
|
||||
b.disableGzip()
|
||||
.setContentType(f.getMimeType())
|
||||
.setAttachmentName("submit-preview-"
|
||||
+ change.getChangeId() + "." + format);
|
||||
return b;
|
||||
} catch (OrmException | IOException e) {
|
||||
throw new RestApiException("Error generating submit preview");
|
||||
}
|
||||
}
|
||||
|
||||
private BinaryResult getBundles(RevisionResource rsrc, final ArchiveFormat f)
|
||||
throws OrmException, RestApiException {
|
||||
ReviewDb db = dbProvider.get();
|
||||
ChangeControl control = rsrc.getControl();
|
||||
IdentifiedUser caller = control.getUser().asIdentifiedUser();
|
||||
Change change = rsrc.getChange();
|
||||
|
||||
BinaryResult bin;
|
||||
try (MergeOp op = mergeOpProvider.get()) {
|
||||
op.merge(db, change, caller, false, new SubmitInput(), true);
|
||||
final MergeOpRepoManager orm = op.getMergeOpRepoManager();
|
||||
final Set<Project.NameKey> projects = op.getAllProjects();
|
||||
|
||||
bin = new BinaryResult() {
|
||||
@Override
|
||||
public void writeTo(OutputStream out) throws IOException {
|
||||
ArchiveOutputStream aos = f.createArchiveOutputStream(out);
|
||||
|
||||
for (Project.NameKey p : projects) {
|
||||
OpenRepo or = orm.getRepo(p);
|
||||
BundleWriter bw = new BundleWriter(or.getRepo());
|
||||
bw.setObjectCountCallback(null);
|
||||
bw.setPackConfig(null);
|
||||
Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates();
|
||||
for (ReceiveCommand r : refs) {
|
||||
bw.include(r.getRefName(), r.getNewId());
|
||||
if (!r.getOldId().equals(ObjectId.zeroId())) {
|
||||
bw.assume(or.getCodeReviewRevWalk().parseCommit(r.getOldId()));
|
||||
}
|
||||
}
|
||||
// This naming scheme cannot produce directory/file conflicts
|
||||
// as no projects contains ".git/":
|
||||
aos.putArchiveEntry(f.prepareArchiveEntry(p.get() + ".git"));
|
||||
bw.writeBundle(NullProgressMonitor.INSTANCE, aos);
|
||||
aos.closeArchiveEntry();
|
||||
}
|
||||
aos.finish();
|
||||
}
|
||||
};
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
}
|
@ -90,11 +90,19 @@ public class MergeOpRepoManager implements AutoCloseable {
|
||||
return ob;
|
||||
}
|
||||
|
||||
public Repository getRepo() {
|
||||
return repo;
|
||||
}
|
||||
|
||||
Project.NameKey getProjectName() {
|
||||
return project.getProject().getNameKey();
|
||||
}
|
||||
|
||||
BatchUpdate getUpdate() {
|
||||
public CodeReviewRevWalk getCodeReviewRevWalk() {
|
||||
return rw;
|
||||
}
|
||||
|
||||
public BatchUpdate getUpdate() {
|
||||
checkState(db != null, "call setContext before getUpdate");
|
||||
if (update == null) {
|
||||
update = batchUpdateFactory.create(db, getProjectName(), caller, ts)
|
||||
|
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright (C) 2016 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.
|
||||
#
|
||||
# A sceleton script to demonstrate how to use the preview_submit REST API call.
|
||||
#
|
||||
#
|
||||
|
||||
if test -z $server
|
||||
then
|
||||
echo "The variable 'server' needs to point to your Gerrit instance"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test -z $changeId
|
||||
then
|
||||
echo "The variable 'changeId' must contain a valid change Id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test -z $gerrituser
|
||||
then
|
||||
echo "The variable 'gerrituser' must contain a user/password"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl --digest -u $gerrituser -w '%{http_code}' -o preview \
|
||||
$server/a/changes/$changeId/revisions/current/preview_submit?format=zip >http_code
|
||||
if ! grep 200 http_code >/dev/null
|
||||
then
|
||||
# error out:
|
||||
echo "Error previewing submit $changeId due to:"
|
||||
cat preview
|
||||
echo
|
||||
else
|
||||
# valid zip file, extract and obtain a bundle for each project
|
||||
mkdir tmp-bundles
|
||||
unzip preview -d tmp-bundles
|
||||
for project in $(cd tmp-bundles && find -type f)
|
||||
do
|
||||
# Projects may contain slashes, so create the required
|
||||
# directory structure
|
||||
mkdir -p $(dirname $project)
|
||||
# $project is in the format of "./path/name/project.git"
|
||||
# remove the leading ./
|
||||
proj=${project:-./}
|
||||
git clone $server/$proj $proj
|
||||
|
||||
# First some nice output:
|
||||
echo "Verify that the bundle is good:"
|
||||
GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
|
||||
git bundle verify tmp-bundles/$proj
|
||||
echo "Checking that the bundle only contains one branch..."
|
||||
if test \
|
||||
"$(GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
|
||||
git bundle list-heads tmp-bundles/$proj |wc -l)" != 1
|
||||
then
|
||||
echo "Submitting $changeId would affect the project"
|
||||
echo "$proj"
|
||||
echo "on multiple branches:"
|
||||
git bundle list-heads
|
||||
echo "This script does not demonstrate this use case."
|
||||
exit 1
|
||||
fi
|
||||
# find the target branch:
|
||||
branch=$(GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
|
||||
git bundle list-heads tmp-bundles/$proj | awk '{print $2}')
|
||||
echo "found branch $branch"
|
||||
echo "fetch the bundle into the repository"
|
||||
GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
|
||||
git fetch tmp-bundles/$proj $branch
|
||||
echo "and checkout the state"
|
||||
git -C $proj checkout FETCH_HEAD
|
||||
done
|
||||
echo "Now run a test for all of: $(cd tmp-bundles && find -type f)"
|
||||
fi
|
Loading…
Reference in New Issue
Block a user