Merge "Allows slave to use authenticated Git/HTTP protocol"
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 40873fe..ef2cdd9 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1121,6 +1121,38 @@
HTTP/1.1 204 No Content
----
+[[delete-branches]]
+=== Delete Branches
+--
+'POST /projects/link:#project-name[\{project-name\}]/branches:delete'
+--
+
+Delete one or more branches.
+
+The branches to be deleted must be provided in the request body as a
+link:#delete-branches-input[DeleteBranchesInput] entity.
+
+.Request
+----
+ POST /projects/MyProject/branches:delete HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "branches": [
+ "stable-1.0",
+ "stable-2.0"
+ ]
+ }
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+If some branches could not be deleted, the response is "`409 Conflict`" and the
+error message is contained in the response body.
+
[[get-content]]
=== Get Content
--
@@ -2060,6 +2092,18 @@
Tokens such as `${project}` are not resolved.
|===========================
+[[delete-branches-input]]
+=== DeleteBranchesInput
+The `DeleteBranchesInput` entity contains information about branches that should
+be deleted.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name |Description
+|`branches` |A list of branch names that identify the branches that should be
+deleted.
+|==========================
+
[[gc-input]]
=== GCInput
The `GCInput` entity contains information to run the Git garbage
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index ff2944f..a177d00 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -36,6 +36,7 @@
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
@@ -55,6 +56,7 @@
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -157,6 +159,21 @@
.cherryPick(in);
assertThat((Iterable<?>)orig.get().messages).hasSize(2);
+ String cherryPickedRevision = cherry.get().currentRevision;
+ String expectedMessage = String.format(
+ "Patch Set 1: Cherry Picked\n\n" +
+ "This patchset was cherry picked to branch %s as commit %s",
+ in.destination, cherryPickedRevision);
+
+ Iterator<ChangeMessageInfo> origIt = orig.get().messages.iterator();
+ origIt.next();
+ assertThat(origIt.next().message).isEqualTo(expectedMessage);
+
+ assertThat((Iterable<?>)cherry.get().messages).hasSize(1);
+ Iterator<ChangeMessageInfo> cherryIt = cherry.get().messages.iterator();
+ expectedMessage = "Patch Set 1: Cherry Picked from branch master.";
+ assertThat(cherryIt.next().message).isEqualTo(expectedMessage);
+
assertThat(cherry.get().subject).contains(in.message);
assertThat(cherry.get().topic).isEqualTo("someTopic");
cherry.current().review(ReviewInput.approve());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index ee8ea69..f4f7087 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -15,7 +15,6 @@
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
-import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
@@ -60,21 +59,20 @@
}
/**
- * Delete branches. For each branch to be deleted a separate DELETE request is
- * fired to the server. The {@code onSuccess} method of the provided callback
- * is invoked once after all requests succeeded. If any request fails the
- * callbacks' {@code onFailure} method is invoked. In a failure case it can be
- * that still some of the branches were successfully deleted.
+ * Delete branches. One call is fired to the server to delete all the
+ * branches.
*/
public static void deleteBranches(Project.NameKey name,
Set<String> refs, AsyncCallback<VoidResult> cb) {
- CallbackGroup group = new CallbackGroup();
- for (String ref : refs) {
- project(name).view("branches").id(ref)
- .delete(group.add(cb));
- cb = CallbackGroup.emptyCallback();
+ if (refs.size() == 1) {
+ project(name).view("branches").id(refs.iterator().next()).delete(cb);
+ } else {
+ DeleteBranchesInput d = DeleteBranchesInput.create();
+ for (String ref : refs) {
+ d.add_branch(ref);
+ }
+ project(name).view("branches:delete").post(d, cb);
}
- group.done();
}
public static void getConfig(Project.NameKey name,
@@ -292,4 +290,18 @@
final native void setRef(String r) /*-{ if(r)this.ref=r; }-*/;
}
+
+ private static class DeleteBranchesInput extends JavaScriptObject {
+ static DeleteBranchesInput create() {
+ DeleteBranchesInput d = createObject().cast();
+ d.init();
+ return d;
+ }
+
+ protected DeleteBranchesInput() {
+ }
+
+ final native void init() /*-{ this.branches = []; }-*/;
+ final native void add_branch(String b) /*-{ this.branches.push(b); }-*/;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 374cde3..9606397 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -22,6 +22,7 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
@@ -34,6 +35,7 @@
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -76,7 +78,9 @@
private final CommitValidators.Factory commitValidatorsFactory;
private final ChangeInserter.Factory changeInserterFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
- final MergeUtil.Factory mergeUtilFactory;
+ private final MergeUtil.Factory mergeUtilFactory;
+ private final ChangeMessagesUtil changeMessagesUtil;
+ private final ChangeUpdate.Factory updateFactory;
@Inject
CherryPickChange(Provider<ReviewDb> db,
@@ -87,7 +91,9 @@
CommitValidators.Factory commitValidatorsFactory,
ChangeInserter.Factory changeInserterFactory,
PatchSetInserter.Factory patchSetInserterFactory,
- MergeUtil.Factory mergeUtilFactory) {
+ MergeUtil.Factory mergeUtilFactory,
+ ChangeMessagesUtil changeMessagesUtil,
+ ChangeUpdate.Factory updateFactory) {
this.db = db;
this.queryProvider = queryProvider;
this.gitManager = gitManager;
@@ -97,6 +103,8 @@
this.changeInserterFactory = changeInserterFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
this.mergeUtilFactory = mergeUtilFactory;
+ this.changeMessagesUtil = changeMessagesUtil;
+ this.updateFactory = updateFactory;
}
public Change.Id cherryPick(Change change, PatchSet patch,
@@ -185,9 +193,17 @@
} else {
// Change key not found on destination branch. We can create a new
// change.
- return createNewChange(git, revWalk, changeKey, project,
- patch.getId(), destRef, cherryPickCommit, refControl,
+ Change newChange = createNewChange(git, revWalk, changeKey, project,
+ destRef, cherryPickCommit, refControl,
identifiedUser, change.getTopic());
+
+ addMessageToSourceChange(change, patch.getId(), destinationBranch,
+ cherryPickCommit, identifiedUser, refControl);
+
+ addMessageToDestinationChange(newChange, change.getDest().getShortName(),
+ identifiedUser, refControl);
+
+ return newChange.getId();
}
} finally {
revWalk.release();
@@ -216,8 +232,8 @@
return change.getId();
}
- private Change.Id createNewChange(Repository git, RevWalk revWalk,
- Change.Key changeKey, Project.NameKey project, PatchSet.Id patchSetId,
+ private Change createNewChange(Repository git, RevWalk revWalk,
+ Change.Key changeKey, Project.NameKey project,
Ref destRef, RevCommit cherryPickCommit, RefControl refControl,
IdentifiedUser identifiedUser, String topic)
throws OrmException, InvalidChangeOperationException, IOException {
@@ -254,31 +270,51 @@
change.getDest().getParentKey().get(), ru.getResult()));
}
- ins.setMessage(buildChangeMessage(patchSetId, change, cherryPickCommit,
- identifiedUser))
- .insert();
+ ins.insert();
- return change.getId();
+ return change;
}
- private ChangeMessage buildChangeMessage(PatchSet.Id patchSetId, Change dest,
- RevCommit cherryPickCommit, IdentifiedUser identifiedUser)
- throws OrmException {
- ChangeMessage cmsg = new ChangeMessage(
+ private void addMessageToSourceChange(Change change, PatchSet.Id patchSetId,
+ String destinationBranch, RevCommit cherryPickCommit,
+ IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+ ChangeMessage changeMessage = new ChangeMessage(
new ChangeMessage.Key(
patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
- String destBranchName = dest.getDest().get();
- StringBuilder msgBuf = new StringBuilder("Patch Set ")
+ StringBuilder sb = new StringBuilder("Patch Set ")
.append(patchSetId.get())
.append(": Cherry Picked")
.append("\n\n")
.append("This patchset was cherry picked to branch ")
- .append(destBranchName.substring(
- destBranchName.indexOf("refs/heads/") + "refs/heads/".length()))
+ .append(destinationBranch)
.append(" as commit ")
.append(cherryPickCommit.getId().getName());
- cmsg.setMessage(msgBuf.toString());
- return cmsg;
+ changeMessage.setMessage(sb.toString());
+
+ ChangeControl ctl = refControl.getProjectControl().controlFor(change);
+ ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+ changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+ }
+
+ private void addMessageToDestinationChange(Change change, String sourceBranch,
+ IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+ PatchSet.Id patchSetId =
+ db.get().patchSets().get(change.currentPatchSetId()).getId();
+ ChangeMessage changeMessage = new ChangeMessage(
+ new ChangeMessage.Key(
+ patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
+ identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
+
+ StringBuilder sb = new StringBuilder("Patch Set ")
+ .append(patchSetId.get())
+ .append(": Cherry Picked from branch ")
+ .append(sourceBranch)
+ .append(".");
+ changeMessage.setMessage(sb.toString());
+
+ ChangeControl ctl = refControl.getProjectControl().controlFor(change);
+ ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+ changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 9a714f8..4aba333 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -32,6 +32,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
@@ -42,6 +43,8 @@
@Singleton
public class DeleteBranch implements RestModifyView<BranchResource, Input>{
private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class);
+ private static final int MAX_LOCK_FAILURE_CALLS = 10;
+ private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
static class Input {
}
@@ -81,14 +84,28 @@
Repository r = repoManager.openRepository(rsrc.getNameKey());
try {
RefUpdate.Result result;
- RefUpdate u;
- try {
- u = r.updateRef(rsrc.getRef());
- u.setForceUpdate(true);
- result = u.delete();
- } catch (IOException e) {
- log.error("Cannot delete " + rsrc.getBranchKey(), e);
- throw e;
+ RefUpdate u = r.updateRef(rsrc.getRef());
+ u.setForceUpdate(true);
+ int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+ for (;;) {
+ try {
+ result = u.delete();
+ } catch (LockFailedException e) {
+ result = RefUpdate.Result.LOCK_FAILURE;
+ } catch (IOException e) {
+ log.error("Cannot delete " + rsrc.getBranchKey(), e);
+ throw e;
+ }
+ if (result == RefUpdate.Result.LOCK_FAILURE
+ && --remainingLockFailureCalls > 0) {
+ try {
+ Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+ } catch (InterruptedException ie) {
+ // ignore
+ }
+ } else {
+ break;
+ }
}
switch (result) {
@@ -104,7 +121,7 @@
break;
case REJECTED_CURRENT_BRANCH:
- log.warn("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
+ log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
throw new ResourceConflictException("cannot delete current branch");
default:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
new file mode 100644
index 0000000..bdc67ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -0,0 +1,181 @@
+// Copyright (C) 2015 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.project;
+
+import static java.lang.String.format;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.DeleteBranches.Input;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+class DeleteBranches implements RestModifyView<ProjectResource, Input> {
+ private static final Logger log = LoggerFactory.getLogger(DeleteBranches.class);
+
+ static class Input {
+ List<String> branches;
+
+ static Input init(Input in) {
+ if (in == null) {
+ in = new Input();
+ }
+ if (in.branches == null) {
+ in.branches = Lists.newArrayListWithCapacity(1);
+ }
+ return in;
+ }
+ }
+
+ private final Provider<IdentifiedUser> identifiedUser;
+ private final GitRepositoryManager repoManager;
+ private final Provider<ReviewDb> dbProvider;
+ private final Provider<InternalChangeQuery> queryProvider;
+ private final GitReferenceUpdated referenceUpdated;
+ private final ChangeHooks hooks;
+
+ @Inject
+ DeleteBranches(Provider<IdentifiedUser> identifiedUser,
+ GitRepositoryManager repoManager,
+ Provider<ReviewDb> dbProvider,
+ Provider<InternalChangeQuery> queryProvider,
+ GitReferenceUpdated referenceUpdated,
+ ChangeHooks hooks) {
+ this.identifiedUser = identifiedUser;
+ this.repoManager = repoManager;
+ this.dbProvider = dbProvider;
+ this.queryProvider = queryProvider;
+ this.referenceUpdated = referenceUpdated;
+ this.hooks = hooks;
+ }
+
+ @Override
+ public Response<?> apply(ProjectResource project, Input input)
+ throws OrmException, IOException, ResourceConflictException {
+ input = Input.init(input);
+ Repository r = repoManager.openRepository(project.getNameKey());
+ try {
+ BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate();
+ for (String branch : input.branches) {
+ batchUpdate.addCommand(createDeleteCommand(project, r, branch));
+ }
+ RevWalk rw = new RevWalk(r);
+ try {
+ batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ } finally {
+ rw.release();
+ }
+ StringBuilder errorMessages = new StringBuilder();
+ for (ReceiveCommand command : batchUpdate.getCommands()) {
+ if (command.getResult() == Result.OK) {
+ postDeletion(project, command);
+ } else {
+ appendAndLogErrorMessage(errorMessages, command);
+ }
+ }
+ if (errorMessages.length() > 0) {
+ throw new ResourceConflictException(errorMessages.toString());
+ }
+ } finally {
+ r.close();
+ }
+ return Response.none();
+ }
+
+ private ReceiveCommand createDeleteCommand(ProjectResource project,
+ Repository r, String branch) throws OrmException, IOException {
+ Ref ref = r.getRefDatabase().getRef(branch);
+ ReceiveCommand command;
+ if (ref == null) {
+ command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), branch);
+ command.setResult(Result.REJECTED_OTHER_REASON,
+ "it doesn't exist or you do not have permission to delete it");
+ return command;
+ }
+ command =
+ new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
+ Branch.NameKey branchKey =
+ new Branch.NameKey(project.getNameKey(), ref.getName());
+ if (!project.getControl().controlForRef(branchKey).canDelete()) {
+ command.setResult(Result.REJECTED_OTHER_REASON,
+ "it doesn't exist or you do not have permission to delete it");
+ }
+ if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
+ command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
+ }
+ return command;
+ }
+
+ private void appendAndLogErrorMessage(StringBuilder errorMessages,
+ ReceiveCommand cmd) {
+ String msg = null;
+ switch (cmd.getResult()) {
+ case REJECTED_CURRENT_BRANCH:
+ msg = format("Cannot delete %s: it is the current branch",
+ cmd.getRefName());
+ break;
+ case REJECTED_OTHER_REASON:
+ msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage());
+ break;
+ default:
+ msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
+ break;
+ }
+ log.error(msg);
+ errorMessages.append(msg);
+ errorMessages.append("\n");
+ }
+
+ private void postDeletion(ProjectResource project, ReceiveCommand cmd)
+ throws OrmException {
+ referenceUpdated.fire(project.getNameKey(), cmd.getRefName(),
+ cmd.getOldId(), cmd.getNewId());
+ Branch.NameKey branchKey =
+ new Branch.NameKey(project.getNameKey(), cmd.getRefName());
+ hooks.doRefUpdatedHook(branchKey, cmd.getOldId(), cmd.getNewId(),
+ identifiedUser.get().getAccount());
+ ResultSet<SubmoduleSubscription> submoduleSubscriptions =
+ dbProvider.get().submoduleSubscriptions().bySuperProject(branchKey);
+ dbProvider.get().submoduleSubscriptions().delete(submoduleSubscriptions);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index ace221d..430d8f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -64,6 +64,7 @@
put(BRANCH_KIND).to(PutBranch.class);
get(BRANCH_KIND).to(GetBranch.class);
delete(BRANCH_KIND).to(DeleteBranch.class);
+ post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
get(BRANCH_KIND, "reflog").to(GetReflog.class);
child(BRANCH_KIND, "files").to(FilesCollection.class);