summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMika Hamalainen <mika.hamalainen@accenture.com>2011-03-28 09:44:02 +0300
committerMika Hamalainen <mika.hamalainen@accenture.com>2011-08-03 14:59:38 +0300
commit5914eaae280e9de548f6c13c89c0b9e6b20cf90a (patch)
treeb652d55e45fd85a36e51570ccaa1fa3766d1bc48
parent9e7eb6c453f05425562a40b7f97dc8db7f8e38d5 (diff)
Staging branch feature
Staging branch allows user to move changes to staging branch for further testing before they are merged into repository. Staging branches are located in folder refs/staging/<branch>. <branch> matches branch under refs/heads. A staging branch is moved to testable build with SSH commands. Build refs are under refs/builds. Staging branch is updated in the following situations - User moves a new change to staging. The change is merged to staging branch. - User creates a new build. Staging is re-created from refs/heads. - User merges a build to refs/heads. The staging branch is updated to match the updated head. Changes include: new button in change screen, new category called 'staging', SSH commands for managing staging and build branches. Change-Id: I74e9aef595db9b78ca0998bc576df5ec6071c99c
-rw-r--r--Documentation/cmd-index.txt12
-rw-r--r--Documentation/cmd-staging-approve.txt75
-rw-r--r--Documentation/cmd-staging-ls.txt64
-rw-r--r--Documentation/cmd-staging-new-build.txt69
-rw-r--r--Documentation/cmd-staging-rebuild.txt55
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java18
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java7
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java1
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java50
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java38
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.pngbin285 -> 277 bytes
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.pngbin308 -> 227 bytes
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java39
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java4
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java19
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java8
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/StagingAction.java144
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/UnstageChange.java149
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java22
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java3
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java11
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java279
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java7
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java258
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java23
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/StagingMergeDelegate.java139
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/StagingUtil.java266
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitMergeDelegate.java80
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java51
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java112
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java15
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/workflow/StagingFunction.java51
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg2
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java1
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java4
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java98
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingApprove.java407
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingCommand.java166
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingListChanges.java96
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingNewBuild.java143
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingRebuild.java89
56 files changed, 3051 insertions, 70 deletions
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 8a6cb6d3bc..175c6934e6 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -120,6 +120,18 @@ link:cmd-show-queue.html[ps]::
link:cmd-suexec.html[suexec]::
Execute a command as any registered user account.
+link:cmd-staging-ls.html[gerrit staging-ls]::
+ List open changes in a staging branch.
+
+link:cmd-staging-new-build.html[gerrit staging-new-build]::
+ Create a new build ref from a staging branch.
+
+link:cmd-staging-approve.html[gerrit staging-approve]::
+ Approve staging changes in a build ref.
+
+link:cmd-staging-rebuild.html[gerrit staging-rebuild]::
+ Rebuild a staging branch.
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-staging-approve.txt b/Documentation/cmd-staging-approve.txt
new file mode 100644
index 0000000000..4aac4898e1
--- /dev/null
+++ b/Documentation/cmd-staging-approve.txt
@@ -0,0 +1,75 @@
+gerrit review
+==============
+
+NAME
+----
+gerrit staging-approve - Approve or reject changes in a build reference.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit staging-approve' <\--project <PROJECT>> <\--build-id <BUILD_ID>> <\--result <RESULT>> [\--message <-|MESSAGE>]
+
+DESCRIPTION
+-----------
+Submits a result to a build reference. Result can be either pass or fail.
+If result is pass, all changes are submitted to their destination branch.
+If result is fail, all changes are returned to New status and they must be
+fixed and reviewed again.
+
+OPTIONS
+-------
+
+\--project::
+-p::
+ Name of the project the intended changes are contained
+ within.
+
+\--build-id::
+-i::
+ Name of the the build branch. Existing build ids are not overwritten.
+
+\--result::
+-r::
+ Result of the build. Only values 'pass' or 'fail' are accepted.
+
+\--message::
+-m::
+ An option message, that is added to each change in the build reference.
+ If value '-' is given, the message is read from STDIN.
+
+\--help::
+-h::
+ Display site-specific usage information, including the
+ complete listing of supported approval categories and values.
+
+ACCESS
+------
+Any user who has configured an SSH key and has Staging access right.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Approve changes in build "refs/builds/this/project/2011-05-16".
+=====
+ $ ssh -p 29418 review.example.com gerrit staging-approve --project this/project --build-id this/project/2011-05-16 --result pass
+=====
+
+Reject changes in build "refs/builds/this/project/2011-05-16".
+=====
+ $ ssh -p 29418 review.example.com gerrit staging-approve --project this/project --build-id this/project/2011-05-16 --result fail
+=====
+
+
+SEE ALSO
+--------
+
+* link:cmd-staging-new-build.html[Staging New Build]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-staging-ls.txt b/Documentation/cmd-staging-ls.txt
new file mode 100644
index 0000000000..92021d96ff
--- /dev/null
+++ b/Documentation/cmd-staging-ls.txt
@@ -0,0 +1,64 @@
+gerrit review
+==============
+
+NAME
+----
+gerrit staging-ls - List open changes in a staging ref.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit staging-ls' <\--project <PROJECT>> <\--branch <BRANCH>>
+
+DESCRIPTION
+-----------
+Prints changes that are considered open from a branch. This command is useful
+to check if there are any changing the staging branch before creating a build
+reference from it.
+
+The output is printed in the following format:
+<SHA> <Change number>,<Patch set number> <Commit message subject>
+
+No output is printed if there are no open changes in the given branch.
+
+OPTIONS
+-------
+
+\--project::
+-p::
+ Name of the project the intended changes are contained
+ within.
+
+\--branch::
+-b::
+ Name of the branch to search for open changes.
+
+\--help::
+-h::
+ Display site-specific usage information, including the
+ complete listing of supported approval categories and values.
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+List open changes in "refs/staging/master"
+=====
+ $ ssh -p 29418 review.example.com gerrit staging-ls --project this/project --branch refs/staging/master
+=====
+
+SEE ALSO
+--------
+
+* link:cmd-staging-new-build.html[Staging New Build]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-staging-new-build.txt b/Documentation/cmd-staging-new-build.txt
new file mode 100644
index 0000000000..77ba5e6b05
--- /dev/null
+++ b/Documentation/cmd-staging-new-build.txt
@@ -0,0 +1,69 @@
+gerrit review
+==============
+
+NAME
+----
+gerrit staging-new-build - Create a new build reference from a staging branch
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit staging-new-build' <\--project <PROJECT>> <\--staging-branch <BRANCH>> <\--build-id <BUILD_ID>>
+
+DESCRIPTION
+-----------
+Creates a new build reference from a staging branch. Builds references are
+stored under "refs/builds". Staging branches are stored in "refs/staging".
+
+A build reference creates a copy of the current state of the staging branch
+and resets staging branch so it can accept more changes. A build reference
+is meant to be used for building and testing changes. Changes are contained
+in a build reference until they can be approved or rejected.
+
+OPTIONS
+-------
+
+\--project::
+-p::
+ Name of the project the intended changes are contained
+ within.
+
+\--staging-branch::
+-s::
+ Name of the staging branch to create the build reference from.
+
+\--build-id::
+-i::
+ Name of the the build branch. Existing build ids are not overwritten.
+
+\--help::
+-h::
+ Display site-specific usage information, including the
+ complete listing of supported approval categories and values.
+
+ACCESS
+------
+Any user who has configured an SSH key and has Staging access right.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Create a build reference "refs/builds/master-2011-05-16" from "refs/staging/master".
+=====
+ $ ssh -p 29418 review.example.com gerrit staging-new-build --project this/project --staging-branch refs/staging/master --build-id 2011-05-16
+=====
+
+SEE ALSO
+--------
+
+* link:cmd-staging-ls.html[Staging List Changes]
+* link:cmd-staging-approve.html[Staging Approve]
+* link:cmd-staging-rebuild.html[Staging Rebuild Staging]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-staging-rebuild.txt b/Documentation/cmd-staging-rebuild.txt
new file mode 100644
index 0000000000..a8e3796d57
--- /dev/null
+++ b/Documentation/cmd-staging-rebuild.txt
@@ -0,0 +1,55 @@
+gerrit review
+==============
+
+NAME
+----
+gerrit staging-rebuild - Re-creates a staging branch.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit staging-new-build' <\--project <PROJECT>> <\--branch <BRANCH>>
+
+DESCRIPTION
+-----------
+Re-creates a staging branch. All changes in the current staging branch are set
+to staging merge queue and staging branch is re-created from the destination
+branch of the changes. All changes are merged again to the staging branch.
+
+OPTIONS
+-------
+
+\--project::
+-p::
+ Name of the project the intended changes are contained
+ within.
+
+\--branch::
+-b::
+ Name of the branch from which the staging branch is created.
+
+ACCESS
+------
+Any user who has configured an SSH key and has Staging access right.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Re-create staging branch "refs/staging/master"
+=====
+ $ ssh -p 29418 review.example.com gerrit staging-rebuild --project this/project --branch refs/heads/master
+=====
+
+SEE ALSO
+--------
+
+* link:cmd-staging-ls.html[Staging List Changes]
+* link:cmd-staging-approve.html[Staging Approve]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 864c9617e9..e223422923 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -27,6 +27,8 @@ import java.util.Set;
/** Detail necessary to display a change. */
public class ChangeDetail extends CommonDetail {
+ protected boolean canStage;
+ protected boolean canUnstage;
protected Change change;
protected List<PatchSet> patchSets;
protected boolean canSubmit;
@@ -39,6 +41,22 @@ public class ChangeDetail extends CommonDetail {
public ChangeDetail() {
}
+ public boolean canStage() {
+ return canStage;
+ }
+
+ public void setCanStage(final boolean a) {
+ canStage = a;
+ }
+
+ public boolean canUnstage() {
+ return canUnstage;
+ }
+
+ public void setCanUnstage(final boolean a) {
+ canUnstage = a;
+ }
+
public Change getChange() {
return change;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
index cb38c3b802..fb954b5595 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
@@ -37,4 +37,11 @@ public interface ChangeManageService extends RemoteJsonService {
@SignInRequired
void restoreChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);
+
+ @SignInRequired
+ void stage(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
+
+ @SignInRequired
+ void unstageChange(PatchSet.Id patchSetId,
+ AsyncCallback<ChangeDetail> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
index ed580fcbad..507a37f22d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
@@ -25,6 +25,7 @@ public class PatchSetPublishDetail extends CommonPublishDetail<PatchSetApproval>
protected PatchSetInfo patchSetInfo;
protected Change change;
protected List<PatchLineComment> drafts;
+ protected boolean stagingBranchAllowed;
public void setPatchSetInfo(PatchSetInfo patchSetInfo) {
this.patchSetInfo = patchSetInfo;
@@ -38,6 +39,10 @@ public class PatchSetPublishDetail extends CommonPublishDetail<PatchSetApproval>
this.drafts = drafts;
}
+ public void setStagingBranchAllowed(boolean allowed) {
+ stagingBranchAllowed = allowed;
+ }
+
public Change getChange() {
return change;
}
@@ -49,4 +54,8 @@ public class PatchSetPublishDetail extends CommonPublishDetail<PatchSetApproval>
public List<PatchLineComment> getDrafts() {
return drafts;
}
+
+ public boolean isStagingBranchAllowed() {
+ return stagingBranchAllowed;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index c27d9d9b0d..c9202d5626 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -31,6 +31,7 @@ public class Permission implements Comparable<Permission> {
public static final String PUSH_TAG = "pushTag";
public static final String READ = "read";
public static final String SUBMIT = "submit";
+ public static final String STAGE = "stage";
private static final List<String> NAMES_LC;
@@ -47,6 +48,7 @@ public class Permission implements Comparable<Permission> {
NAMES_LC.add(PUSH_TAG.toLowerCase());
NAMES_LC.add(LABEL.toLowerCase());
NAMES_LC.add(SUBMIT.toLowerCase());
+ NAMES_LC.add(STAGE.toLowerCase());
}
/** @return true if the name is recognized as a permission name. */
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d0e51928d6..00516b5a15 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -450,6 +450,8 @@ public class Gerrit implements EntryPoint {
m = new LinkMenuBar();
addLink(m, C.menuAllOpen(), PageLinks.toChangeQuery("status:open"));
+ addLink(m, C.menuAllStaged(), PageLinks.toChangeQuery("status:staged"));
+ addLink(m, C.menuAllIntegrating(), PageLinks.toChangeQuery("status:integrating"));
addLink(m, C.menuAllMerged(), PageLinks.toChangeQuery("status:merged"));
addLink(m, C.menuAllAbandoned(), PageLinks.toChangeQuery("status:abandoned"));
menuLeft.add(m, C.menuAll());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index fbec6aa882..5700c0f1ad 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -52,6 +52,8 @@ public interface GerritConstants extends Constants {
String menuAll();
String menuAllOpen();
+ String menuAllStaged();
+ String menuAllIntegrating();
String menuAllMerged();
String menuAllAbandoned();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 617ab6b80e..9d5f4522bb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -35,6 +35,8 @@ inactiveAccountBody = This user is currently inactive.
menuAll = All
menuAllOpen = Open
+menuAllStaged = Staged
+menuAllIntegrating = Integrating
menuAllMerged = Merged
menuAllAbandoned = Abandoned
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 5e93f8407f..b331b6a45e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -99,7 +99,8 @@ permissionNames = \
pushMerge, \
pushTag, \
read, \
- submit
+ submit, \
+ stage
create = Create Reference
forgeAuthor = Forge Author Identity
forgeCommitter = Forge Committer Identity
@@ -110,6 +111,7 @@ pushMerge = Push Merge Commit
pushTag = Push Annotated Tag
read = Read
submit = Submit
+stage = Stage
refErrorEmpty = Reference must be supplied
refErrorBeginSlash = Reference must not start with '/'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 31132e668b..d3f8b7c74a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -123,8 +123,10 @@ public interface ChangeConstants extends Constants {
String buttonPublishCommentsSend();
String buttonPublishSubmitSend();
String buttonPublishCommentsCancel();
+ String buttonPublishStagingSend();
String headingCoverMessage();
String headingPatchComments();
+ String buttonUnstagingChange();
String buttonRestoreChangeBegin();
String restoreChangeTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 33b7d7c58e..a1a49be005 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -106,9 +106,12 @@ buttonReview = Review
buttonPublishCommentsSend = Publish Comments
buttonPublishSubmitSend = Publish and Submit
buttonPublishCommentsCancel = Cancel
+buttonPublishStagingSend = Publish and merge to Staging
headingCoverMessage = Cover Message:
headingPatchComments = Patch Comments:
+buttonUnstagingChange = Remove change from Staging
+
pagedChangeListPrev = &#x21e6;Prev
pagedChangeListNext = Next&#x21e8;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index cbf4a6bd23..e1143c7cd6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -30,6 +30,7 @@ public interface ChangeMessages extends Messages {
String patchSetHeader(int id);
String loadingPatchSet(int id);
String submitPatchSet(int id);
+ String mergeToStagingPatchSet(int id);
String patchTableComments(@PluralCount int count);
String patchTableDrafts(@PluralCount int count);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 5982ad5d06..9ae7ec7dff 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -36,3 +36,5 @@ accountInactive = {0} is not an active user.
changeNotVisibleTo = {0} cannot access the change.
anonymousDownload = Anonymous {0}
+
+mergeToStagingPatchSet = Merge Patch Set {0} to Staging
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index e6d756cd81..2ab4dcc058 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -192,6 +192,38 @@ class PatchSetComplexDisclosurePanel extends CommonComplexDisclosurePanel {
private void populateActions(final PatchSetDetail detail) {
final boolean isOpen = changeDetail.getChange().getStatus().isOpen();
+ final boolean isNew =
+ changeDetail.getChange().getStatus() == Change.Status.NEW;
+
+ // Staging is allowed only for NEW changes. User is required to have
+ // STAGING approval category.
+ if (isNew && changeDetail.canStage()) {
+ // Create button new button and add click handler.
+ final Button stagingButton = new Button(Util.M.mergeToStagingPatchSet(detail.getPatchSet().getPatchSetId()));
+ stagingButton.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ // Disable the button until this click is handled.
+ stagingButton.setEnabled(false);
+
+ // Move change to staging.
+ Util.MANAGE_SVC.stage(patchSet.getId(), new GerritCallback<ChangeDetail>() {
+ public void onSuccess(ChangeDetail result) {
+ // Borrow submit result function. It works fine for staging.
+ onSubmitResult(result);
+ }
+
+ public void onFailure(Throwable caught) {
+ // Re-enable the button and display error message.
+ stagingButton.setEnabled(true);
+ super.onFailure(caught);
+ }
+ });
+ }
+ });
+ actionsPanel.add(stagingButton);
+ }
+
if (isOpen && changeDetail.canSubmit()) {
final Button b =
new Button(Util.M
@@ -256,6 +288,24 @@ class PatchSetComplexDisclosurePanel extends CommonComplexDisclosurePanel {
actionsPanel.add(b);
}
+ if (changeDetail.canUnstage()) {
+ final Button b = new Button(Util.C.buttonUnstagingChange());
+ b.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ b.setEnabled(false);
+ Util.MANAGE_SVC.unstageChange(patchSet.getId(),
+ new GerritCallback<ChangeDetail>() {
+ @Override
+ public void onSuccess(ChangeDetail result) {
+ changeScreen.update(result);
+ }
+ });
+ }
+ });
+ actionsPanel.add(b);
+ }
+
if (changeDetail.canRestore()) {
final Button b = new Button(Util.C.buttonRestoreChangeBegin());
b.addClickHandler(new ClickHandler() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index f7f8ae16a5..7783eb4016 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -62,6 +62,8 @@ public class PublishCommentScreen extends AccountScreen implements
ClickHandler, CommentEditorContainer {
private static SavedState lastState;
+ private enum Action { NOOP, SUBMIT, STAGING };
+
private final PatchSet.Id patchSetId;
private Collection<ValueRadioButton> approvalButtons;
private ChangeDescriptionBlock descBlock;
@@ -70,6 +72,7 @@ public class PublishCommentScreen extends AccountScreen implements
private FlowPanel draftsPanel;
private Button send;
private Button submit;
+ private Button staging;
private Button cancel;
private boolean saveStateOnUnload = true;
private List<CommentEditorPanel> commentEditors;
@@ -113,6 +116,10 @@ public class PublishCommentScreen extends AccountScreen implements
send.addClickHandler(this);
buttonRow.add(send);
+ staging = new Button(Util.C.buttonPublishStagingSend());
+ staging.addClickHandler(this);
+ buttonRow.add(staging);
+
submit = new Button(Util.C.buttonPublishSubmitSend());
submit.addClickHandler(this);
buttonRow.add(submit);
@@ -163,12 +170,14 @@ public class PublishCommentScreen extends AccountScreen implements
public void onClick(final ClickEvent event) {
final Widget sender = (Widget) event.getSource();
if (send == sender) {
- onSend(false);
+ onSend(Action.NOOP);
} else if (submit == sender) {
- onSend(true);
+ onSend(Action.SUBMIT);
} else if (cancel == sender) {
saveStateOnUnload = false;
goChange();
+ } else if(staging == sender) {
+ onSend(Action.STAGING);
}
}
@@ -317,11 +326,12 @@ public class PublishCommentScreen extends AccountScreen implements
}
submit.setVisible(r.canSubmit());
+ staging.setVisible(r.isStagingBranchAllowed());
}
- private void onSend(final boolean submit) {
+ private void onSend(final Action action) {
if (commentEditors.isEmpty()) {
- onSend2(submit);
+ onSend2(action);
} else {
final GerritCallback<VoidResult> afterSaveDraft =
new GerritCallback<VoidResult>() {
@@ -330,7 +340,7 @@ public class PublishCommentScreen extends AccountScreen implements
@Override
public void onSuccess(final VoidResult result) {
if (++done == commentEditors.size()) {
- onSend2(submit);
+ onSend2(action);
}
}
};
@@ -340,7 +350,7 @@ public class PublishCommentScreen extends AccountScreen implements
}
}
- private void onSend2(final boolean submit) {
+ private void onSend2(final Action action) {
final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> values =
new HashMap<ApprovalCategory.Id, ApprovalCategoryValue.Id>();
for (final ValueRadioButton b : approvalButtons) {
@@ -354,8 +364,10 @@ public class PublishCommentScreen extends AccountScreen implements
new HashSet<ApprovalCategoryValue.Id>(values.values()),
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
- if(submit) {
+ if(action == Action.SUBMIT) {
submit();
+ } else if (action == Action.STAGING) {
+ staging();
} else {
saveStateOnUnload = false;
goChange();
@@ -386,6 +398,18 @@ public class PublishCommentScreen extends AccountScreen implements
});
}
+ private void staging() {
+ // Move change to staging. onSuccess takes same action for staging and
+ // submit results.
+ Util.MANAGE_SVC.stage(patchSetId, new GerritCallback<ChangeDetail>() {
+ @Override
+ public void onSuccess(ChangeDetail result) {
+ saveStateOnUnload = false;
+ goChange();
+ }
+ });
+ }
+
private void goChange() {
final Change.Id ck = patchSetId.getParentKey();
Gerrit.display(PageLinks.toChange(ck), new ChangeScreen(ck));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.png
index cd70687c82..19e9fddb52 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.png
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.png
index 4e83a8fdb5..fd9f467ec7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.png
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.png
Binary files differ
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
index 8eb1bbe672..34a4f753ba 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
@@ -23,16 +23,23 @@ import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
import javax.annotation.Nullable;
class AbandonChange extends Handler<ChangeDetail> {
@@ -51,6 +58,9 @@ class AbandonChange extends Handler<ChangeDetail> {
private final String message;
private final ChangeHookRunner hooks;
+ private final MergeQueue merger;
+ private final MergeOp.Factory opFactory;
+ private final GitRepositoryManager gitManager;
@Inject
AbandonChange(final ChangeControl.Factory changeControlFactory,
@@ -58,7 +68,9 @@ class AbandonChange extends Handler<ChangeDetail> {
final AbandonedSender.Factory abandonedSenderFactory,
final ChangeDetailFactory.Factory changeDetailFactory,
@Assisted final PatchSet.Id patchSetId,
- @Assisted @Nullable final String message, final ChangeHookRunner hooks) {
+ @Assisted @Nullable final String message, final ChangeHookRunner hooks,
+ MergeQueue merger, MergeOp.Factory opFactory,
+ GitRepositoryManager gitManager) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
@@ -68,13 +80,15 @@ class AbandonChange extends Handler<ChangeDetail> {
this.patchSetId = patchSetId;
this.message = message;
this.hooks = hooks;
+ this.merger = merger;
+ this.opFactory = opFactory;
+ this.gitManager = gitManager;
}
@Override
public ChangeDetail call() throws NoSuchChangeException, OrmException,
EmailException, NoSuchEntityException, InvalidChangeOperationException,
PatchSetInfoNotAvailableException {
-
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl control = changeControlFactory.validateFor(changeId);
if (!control.canAbandon()) {
@@ -84,6 +98,27 @@ class AbandonChange extends Handler<ChangeDetail> {
ChangeUtil.abandon(patchSetId, currentUser, message, db,
abandonedSenderFactory, hooks);
+ final Change change = db.changes().get(changeId);
+ final boolean staged = change.getStatus() == Change.Status.STAGED;
+
+ // If the change was staged, the staging branch needs to be updated.
+ if (staged) {
+ Repository git = null;
+ try {
+ git = gitManager.openRepository(change.getProject());
+ ChangeUtil.rebuildStaging(change.getDest(), currentUser, db, git,
+ opFactory, merger, hooks);
+ } catch (IOException e) {
+ // Failed to open git repository.
+ } catch (NoSuchRefException e) {
+ // Invalid change destination branch.
+ } finally {
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+
return changeDetailFactory.create(changeId).call();
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 0a9e247b60..527974d9e8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -111,6 +111,10 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet() && (change.getTopicId() == null));
+ final CanSubmitResult canStageResult = control.canMergeToStaging(patch.getId());
+ detail.setCanStage(canStageResult == CanSubmitResult.OK);
+ detail.setCanUnstage(change.getStatus() == Change.Status.STAGED && control.canAbandon());
+
loadPatchSets();
loadMessages();
if (change.currentPatchSetId() != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
index 9e04756edf..225813f082 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
@@ -25,16 +25,22 @@ class ChangeManageServiceImpl implements ChangeManageService {
private final AbandonChange.Factory abandonChangeFactory;
private final RestoreChange.Factory restoreChangeFactory;
private final RevertChange.Factory revertChangeFactory;
+ private final StagingAction.Factory stagingActionFactory;
+ private final UnstageChange.Factory unstageChangeFactory;
@Inject
ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction,
final AbandonChange.Factory abandonChangeFactory,
final RestoreChange.Factory restoreChangeFactory,
- final RevertChange.Factory revertChangeFactory) {
+ final RevertChange.Factory revertChangeFactory,
+ final StagingAction.Factory stagingActionFactory,
+ final UnstageChange.Factory unstageChangeFactory) {
this.submitAction = patchSetAction;
this.abandonChangeFactory = abandonChangeFactory;
this.restoreChangeFactory = restoreChangeFactory;
this.revertChangeFactory = revertChangeFactory;
+ this.stagingActionFactory = stagingActionFactory;
+ this.unstageChangeFactory = unstageChangeFactory;
}
public void submit(final PatchSet.Id patchSetId,
@@ -56,4 +62,15 @@ class ChangeManageServiceImpl implements ChangeManageService {
final AsyncCallback<ChangeDetail> callback) {
restoreChangeFactory.create(patchSetId, message).to(callback);
}
+
+ public void stage(final PatchSet.Id patchSetId,
+ final AsyncCallback<ChangeDetail> callback) {
+ // Forward call to StagingAction implementation.
+ stagingActionFactory.create(patchSetId).to(callback);
+ }
+
+ public void unstageChange(final PatchSet.Id patchSetId,
+ final AsyncCallback<ChangeDetail> callback) {
+ unstageChangeFactory.create(patchSetId).to(callback);
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
index 95c438c15d..b48f755138 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
@@ -36,6 +36,8 @@ public class ChangeModule extends RpcServletModule {
factory(PatchSetDetailFactory.Factory.class);
factory(PatchSetPublishDetailFactory.Factory.class);
factory(SubmitAction.Factory.class);
+ factory(StagingAction.Factory.class);
+ factory(UnstageChange.Factory.class);
}
});
rpc(ChangeDetailServiceImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index c3e8caad57..70ffc3a402 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -110,7 +110,13 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
if (canSubmitResult == CanSubmitResult.OK
&& (change.getTopicId() == null)) {
detail.setCanSubmit(true);
- }
+ }
+
+ final CanSubmitResult canMergeToStaging = control.canMergeToStaging(patchSetId);
+ if (canMergeToStaging == CanSubmitResult.OK
+ && (change.getTopicId() == null)) {
+ detail.setStagingBranchAllowed(true);
+ }
return detail;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/StagingAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/StagingAction.java
new file mode 100644
index 0000000000..9a368f9f91
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/StagingAction.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2011 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.httpd.rpc.changedetail;
+
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.CanSubmitResult;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.workflow.FunctionState;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+
+/**
+ * RPC service implementation that uses Guice for moving change to staging
+ * branch.
+ *
+ * ChangeDetail is returned as a result to callers.
+ *
+ */
+class StagingAction extends Handler<ChangeDetail> {
+ /**
+ * Guice (Gerrit) factory interface for creating StagingAction for a specific
+ * patch set.
+ */
+ interface Factory {
+ StagingAction create(PatchSet.Id patchSet);
+ }
+
+ private final ReviewDb db;
+ private final MergeQueue merger;
+ private final ApprovalTypes approvalTypes;
+ private final FunctionState.Factory functionState;
+ private final IdentifiedUser user;
+ private final ChangeDetailFactory.Factory changeDetailFactory;
+ private final ChangeControl.Factory changeControlFactory;
+ private final MergeOp.Factory mergeFactory;
+ private final PatchSet.Id patchSetId;
+ private final GitRepositoryManager gitManager;
+ private final ChangeHookRunner hooks;
+
+ @Inject
+ StagingAction(final ReviewDb db, final MergeQueue merger,
+ final ApprovalTypes approvalTypes,
+ final FunctionState.Factory functionState,
+ final IdentifiedUser user,
+ final ChangeDetailFactory.Factory changeDetailFactory,
+ final ChangeControl.Factory changeControlFactory,
+ final MergeOp.Factory stagingFactory,
+ @Assisted final PatchSet.Id patchSetId,
+ final GitRepositoryManager gitManager,
+ final ChangeHookRunner hooks) {
+ this.db = db;
+ this.merger = merger;
+ this.approvalTypes = approvalTypes;
+ this.functionState = functionState;
+ this.user = user;
+ this.changeDetailFactory = changeDetailFactory;
+ this.changeControlFactory = changeControlFactory;
+ this.mergeFactory = stagingFactory;
+ this.patchSetId = patchSetId;
+ this.gitManager = gitManager;
+ this.hooks = hooks;
+ }
+
+ /**
+ * RPC service call method for moving a change from NEW state to STAGING
+ * state.
+ *
+ * @see ChangeUtil.moveToStaging for actual implementation.
+ */
+ @Override
+ public ChangeDetail call() throws OrmException, NoSuchEntityException,
+ PatchSetInfoNotAvailableException, NoSuchChangeException {
+ final Change.Id changeId = patchSetId.getParentKey();
+ // Construct a change control object that will be used to check if the
+ // change can be merged.
+ final ChangeControl changeControl =
+ changeControlFactory.validateFor(changeId);
+
+ // Check if the change can be merged to staging branch.
+ CanSubmitResult err =
+ changeControl.canMergeToStaging(patchSetId, db, approvalTypes,
+ functionState);
+
+ Repository git = null;
+ if (err == CanSubmitResult.OK) {
+ try {
+ // Open a handle to Git repository.
+ git =
+ gitManager.openRepository(changeControl.getProject().getNameKey());
+
+ // Move change to staging.
+ ChangeUtil.moveToStaging(mergeFactory, patchSetId, user, db, merger,
+ git, hooks);
+ } catch (IOException e) {
+ throw new IllegalStateException(e.getMessage());
+ } catch (NoSuchRefException e) {
+ throw new IllegalStateException(e.getMessage());
+ } finally {
+ // Make sure that access to Git repository is closed.
+ if (git != null) {
+ git.close();
+ }
+ }
+ return changeDetailFactory.create(changeId).call();
+ } else {
+ // Report error message to user. User cannot move this change to staging.
+ // The problem is caused because of illegal stage of missing access
+ // rights.
+ throw new IllegalStateException(err.getMessage());
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/UnstageChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/UnstageChange.java
new file mode 100644
index 0000000000..d2db9483f8
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/UnstageChange.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2011 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.httpd.rpc.changedetail;
+
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.CanSubmitResult;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.workflow.FunctionState;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+
+/**
+ * RPC service implementation that uses Guice for moving change to staging
+ * branch.
+ *
+ * ChangeDetail is returned as a result to callers.
+ *
+ */
+class UnstageChange extends Handler<ChangeDetail> {
+ /**
+ * Guice (Gerrit) factory interface for creating StagingAction for a specific
+ * patch set.
+ */
+ interface Factory {
+ UnstageChange create(PatchSet.Id patchSet);
+ }
+
+ private final ReviewDb db;
+ private final MergeQueue merger;
+ private final ApprovalTypes approvalTypes;
+ private final FunctionState.Factory functionState;
+ private final IdentifiedUser user;
+ private final ChangeDetailFactory.Factory changeDetailFactory;
+ private final ChangeControl.Factory changeControlFactory;
+ private final MergeOp.Factory mergeFactory;
+ private final PatchSet.Id patchSetId;
+ private final GitRepositoryManager gitManager;
+ private final ChangeHookRunner hooks;
+
+ @Inject
+ UnstageChange(final ReviewDb db, final MergeQueue merger,
+ final ApprovalTypes approvalTypes,
+ final FunctionState.Factory functionState,
+ final IdentifiedUser user,
+ final ChangeDetailFactory.Factory changeDetailFactory,
+ final ChangeControl.Factory changeControlFactory,
+ final MergeOp.Factory stagingFactory,
+ @Assisted final PatchSet.Id patchSetId,
+ final GitRepositoryManager gitManager,
+ final ChangeHookRunner hooks) {
+ this.db = db;
+ this.merger = merger;
+ this.approvalTypes = approvalTypes;
+ this.functionState = functionState;
+ this.user = user;
+ this.changeDetailFactory = changeDetailFactory;
+ this.changeControlFactory = changeControlFactory;
+ this.mergeFactory = stagingFactory;
+ this.patchSetId = patchSetId;
+ this.gitManager = gitManager;
+ this.hooks = hooks;
+ }
+
+ /**
+ * RPC service call method for moving a change from NEW state to STAGING
+ * state.
+ *
+ * @see ChangeUtil.moveToStaging for actual implementation.
+ */
+ @Override
+ public ChangeDetail call() throws OrmException, NoSuchEntityException,
+ PatchSetInfoNotAvailableException, NoSuchChangeException {
+ final Change.Id changeId = patchSetId.getParentKey();
+ // Construct a change control object that will be used to check if the
+ // change can be merged.
+ final ChangeControl changeControl =
+ changeControlFactory.validateFor(changeId);
+
+ // Check if the change can be merged to staging branch.
+ CanSubmitResult err =
+ changeControl.canMergeToStaging(patchSetId, db, approvalTypes,
+ functionState);
+
+ Repository git = null;
+ if (changeControl.canAbandon()) {
+ try {
+ // Open a handle to Git repository.
+ git =
+ gitManager.openRepository(changeControl.getProject().getNameKey());
+
+ // Remove staging approvals and reset status.
+ ChangeUtil.rejectStagedChange(patchSetId, user, db);
+
+ // Rebuild staging branch.
+ final Branch.NameKey branch = changeControl.getChange().getDest();
+ ChangeUtil.rebuildStaging(branch, user, db, git, mergeFactory, merger,
+ hooks);
+ } catch (IOException e) {
+ throw new IllegalStateException(e.getMessage());
+ } catch (NoSuchRefException e) {
+ throw new IllegalStateException(e.getMessage());
+ } finally {
+ // Make sure that access to Git repository is closed.
+ if (git != null) {
+ git.close();
+ }
+ }
+ return changeDetailFactory.create(changeId).call();
+ } else {
+ // Report error message to user. User cannot move this change to staging.
+ // The problem is caused because of illegal stage of missing access
+ // rights.
+ throw new IllegalStateException(err.getMessage());
+ }
+ }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java
index fdba26ff8e..e516d4ec59 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractEntity.java
@@ -98,6 +98,12 @@ public abstract class AbstractEntity {
protected static final char STATUS_NEW = 'n';
/** Database constant for {@link Status#SUBMITTED}. */
protected static final char STATUS_SUBMITTED = 's';
+ /** Database constant for {@link Status#STAGING}. */
+ protected static final char STATUS_STAGING = 'q';
+ /** Database constant for {@link Status#STAGED}. */
+ protected static final char STATUS_STAGED = 'r';
+ /** Database constant for {@link Status#INTEGRATING}. */
+ protected static final char STATUS_INTEGRATING = 'i';
/** Maximum database status constant for an open change. */
private static final char MAX_OPEN = 'z';
@@ -170,6 +176,22 @@ public abstract class AbstractEntity {
MERGED(STATUS_MERGED),
/**
+ * Changes is open and ready to be merged into staging the branch.
+ */
+ STAGING(STATUS_STAGING),
+
+ /**
+ * Change is merged into the staging branch.
+ */
+ STAGED(STATUS_STAGED),
+
+ /**
+ * Change is in build branch under refs/builds and is being build
+ * and tested by continuous integration system.
+ */
+ INTEGRATING(STATUS_INTEGRATING),
+
+ /**
* Change/topic is closed, but was not submitted to its destination branch.
*
* <p>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
index 58d5482501..ad170362df 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
@@ -24,6 +24,9 @@ public final class ApprovalCategory {
public static final ApprovalCategory.Id SUBMIT =
new ApprovalCategory.Id("SUBM");
+ /** Id of the custom "Staging" category. */
+ public static final ApprovalCategory.Id STAGING = new ApprovalCategory.Id("STGN");
+
public static class Id extends StringKey<Key<?>> {
private static final long serialVersionUID = 1L;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
index 0ee381c2c1..203f343d08 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
@@ -117,6 +117,7 @@ public final class Change extends AbstractEntity {
return PatchSet.Id.fromRef(ref).getParentKey();
}
}
+
/** Locally assigned unique identifier of the change */
@Column(id = 1)
protected Id changeId;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
index 17f3010c6f..4f576f9a78 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
@@ -102,4 +102,15 @@ public interface ChangeAccess extends Access<Change, Change.Id> {
@Query
ResultSet<Change> all() throws OrmException;
+
+ @Query("WHERE dest = ? AND status = '" + Change.STATUS_STAGING
+ + "' ORDER BY lastUpdatedOn")
+ ResultSet<Change> staging(Branch.NameKey dest) throws OrmException;
+
+ @Query("WHERE dest = ? AND status = '" + Change.STATUS_STAGED
+ + "' ORDER BY lastUpdatedOn")
+ ResultSet<Change> staged(Branch.NameKey dest) throws OrmException;
+
+ @Query("WHERE status = '" + Change.STATUS_STAGING + "'")
+ ResultSet<Change> allStaging() throws OrmException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 5ad8431127..24e93b66cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -15,9 +15,11 @@
package com.google.gerrit.server;
import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
+import static com.google.gerrit.reviewdb.ApprovalCategory.STAGING;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Branch;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ChangeMessage;
@@ -38,6 +40,8 @@ import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.git.StagingUtil;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -48,7 +52,9 @@ import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmConcurrencyException;
import com.google.gwtorm.client.OrmException;
-
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -181,7 +187,10 @@ public class ChangeUtil {
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
- if (change.getStatus() == Change.Status.NEW) {
+ if (change.getStatus() == Change.Status.NEW
+ || change.getStatus() == Change.Status.STAGED
+ || change.getStatus() == Change.Status.STAGING
+ || change.getStatus() == Change.Status.INTEGRATING) {
change.setStatus(Change.Status.SUBMITTED);
ChangeUtil.updated(change);
}
@@ -511,6 +520,67 @@ public class ChangeUtil {
reviewerId, addReviewerCategoryId), (short) 0);
}
+ public static PatchSetApproval createStagingApproval(PatchSet.Id patchSetId,
+ IdentifiedUser user, ReviewDb db)
+ throws OrmException {
+ // Get all existing approvals for this patch set.
+ final List<PatchSetApproval> allApprovals =
+ new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
+ patchSetId).toList());
+
+ // Key for staging approval.
+ final PatchSetApproval.Key akey =
+ new PatchSetApproval.Key(patchSetId, user.getAccountId(), STAGING);
+
+ // Search existing approvals for a staging approval.
+ for (final PatchSetApproval approval : allApprovals) {
+ if (akey.equals(approval.getKey())) {
+ // Existing approval found.
+ approval.setValue((short) 1);
+ approval.setGranted();
+ return approval;
+ }
+ }
+
+ // Create a new approval.
+ return new PatchSetApproval(akey, (short) 1);
+ }
+
+ /**
+ * Creates a staging removing PatchSetApproval. Caller of this method needs
+ * to update the database to remove tha staging approval.
+ *
+ * @param patchSetId Patch set ID.
+ * @param user User taking this action.
+ * @param db Review database.
+ * @return Existing patch set approval or null if the patch set does not
+ * have a staging approval.
+ * @throws OrmException
+ */
+ public static PatchSetApproval removeStagingApproval(PatchSet.Id patchSetId,
+ IdentifiedUser user, ReviewDb db) throws OrmException {
+ // Get current approvals.
+ final List<PatchSetApproval> allApprovals =
+ new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
+ patchSetId).toList());
+
+ // Key for staging approvals.
+ final PatchSetApproval.Key akey =
+ new PatchSetApproval.Key(patchSetId, user.getAccountId(), STAGING);
+
+ // Find existing staging approval. If there are several, first one is
+ // enough.
+ for (final PatchSetApproval approval : allApprovals) {
+ if (akey.equals(approval.getKey())) {
+ approval.setValue((short) 0);
+ approval.setGranted();
+ return approval;
+ }
+ }
+
+ return null;
+ }
+
public static String sortKey(long lastUpdated, int id){
// The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
// We overrun approximately 4,085 years later, so ~6093.
@@ -529,6 +599,211 @@ public class ChangeUtil {
c.setSortKey(sortKey(lastUpdated, id));
}
+ /**
+ * Moves a change to staging branch.
+ *
+ * @param mergeFactory Merge factory for creating merge operators.
+ * @param patchSetId Id of the patch set that is to be merged.
+ * @param user User taking this action.
+ * @param db Review database.
+ * @param merger Merge queue.
+ * @param git Git repository.
+ * @throws OrmException Thrown if access to Review Db fails.
+ * @throws IOException Thrown if access to Git repository fails.
+ * @throws NoSuchRefException Thrown if source branch does not exist.
+ */
+ public static void moveToStaging(MergeOp.Factory mergeFactory,
+ PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db,
+ MergeQueue merger, Repository git, ChangeHookRunner hooks)
+ throws OrmException, IOException, NoSuchRefException {
+ // Create and insert staging approval to the database.
+ final PatchSetApproval approval =
+ createStagingApproval(patchSetId, user, db);
+ db.patchSetApprovals().upsert(Collections.singleton(approval));
+
+ // Change change state from NEW to STAGING.
+ final Change.Id changeId = patchSetId.getParentKey();
+ AtomicUpdate<Change> atomicUpdate =
+ getUpdateToState(Change.Status.NEW, Change.Status.STAGING);
+ final Change change = db.changes().atomicUpdate(changeId, atomicUpdate);
+
+ // Check if staging branch exists. Create the staging branch if it does not
+ // exist.
+ final Branch.NameKey stagingBranch =
+ StagingUtil.getStagingBranch(change.getDest());
+ if (!StagingUtil.branchExists(git, stagingBranch)) {
+ StagingUtil.createStagingBranch(git, change.getDest());
+ }
+
+ // Activate the merge queue.
+ merger.merge(mergeFactory, stagingBranch);
+ }
+
+ /**
+ * Removes a commit from staging branch. Status of the change in reset to
+ * NEW.
+ *
+ * @param patchSetId Patch set to be removed from staging.
+ * @param user User taking this action.
+ * @param db Review database.
+ * @throws OrmException If review database cannot be accessed.
+ */
+ public static void rejectStagedChange(PatchSet.Id patchSetId,
+ IdentifiedUser user, ReviewDb db) throws OrmException {
+ // Delete all STAGING approvals for the patch set.
+ final PatchSetApproval.Key stagingKey =
+ new PatchSetApproval.Key(patchSetId, user.getAccountId(), STAGING);
+ db.patchSetApprovals().deleteKeys(Collections.singleton(stagingKey));
+
+ // Set change state to NEW.
+ final Change.Id changeId = patchSetId.getParentKey();
+ AtomicUpdate<Change> atomicUpdate =
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus() == Change.Status.INTEGRATING
+ || change.getStatus() == Change.Status.STAGED) {
+ change.setStatus(Change.Status.NEW);
+ ChangeUtil.updated(change);
+ }
+ return change;
+ }
+ };
+ db.changes().atomicUpdate(changeId, atomicUpdate);
+ }
+
+ /**
+ * Moves change from integrating to merged. Only database is updated.
+ *
+ * @param patchSetId Patch set id for accessing the change.
+ * @param user User taking the action.
+ * @param db Review database.
+ * @throws OrmException Thrown, if access to review database fails.
+ */
+ public static void setIntegratingToMerged(PatchSet.Id patchSetId, IdentifiedUser user,
+ ReviewDb db) throws OrmException {
+ final Change.Id changeId = patchSetId.getParentKey();
+ AtomicUpdate<Change> atomicUpdate =
+ getUpdateToState(Change.Status.INTEGRATING, Change.Status.MERGED);
+ db.changes().atomicUpdate(changeId, atomicUpdate);
+ }
+
+ /**
+ * Merges an already merged change once more to staging. This method should
+ * be used when an update in main branch causes the staging branch to be
+ * updated.
+ *
+ * @param mergeFactory Merge operator.
+ * @param patchSetId Patch set ID.
+ * @param user User taking the action.
+ * @param db Review database.
+ * @param merger Merge queue.
+ * @param git Git repository.
+ * @throws OrmException Thrown, if review database cannot be accessed.
+ * @throws IOException Thrown, if Git repository cannot be accessed.
+ * @throws NoSuchRefException Thrown, if source branch is not available.
+ */
+ public static void restage(MergeOp.Factory mergeFactory,
+ PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db,
+ MergeQueue merger, Repository git) throws OrmException, IOException,
+ NoSuchRefException {
+ // In order to make the patch set visible to merge queue, move it from
+ // STAGED to STAGING state.
+ final Change.Id changeId = patchSetId.getParentKey();
+ AtomicUpdate<Change> atomicUpdate =
+ getUpdateToState(Change.Status.STAGED, Change.Status.STAGING);
+ final Change change = db.changes().atomicUpdate(changeId, atomicUpdate);
+
+ // Check if staging branch exists. Create a new staging branch if it does
+ // not exist.
+ final Branch.NameKey stagingBranch =
+ StagingUtil.getStagingBranch(change.getDest());
+ if (!StagingUtil.branchExists(git, stagingBranch)) {
+ StagingUtil.createStagingBranch(git, change.getDest());
+ }
+
+ // Activate the merge queue.
+ merger.merge(mergeFactory, stagingBranch);
+ }
+
+ /**
+ * Reset the staging branch. This method should be called if some change
+ * is removed from staging branch. For example, this method is called after
+ * abandoning a change.
+ *
+ * @param branch Destination branch. E.g. refs/heads/master
+ * @param user User taking this action.
+ * @param db Review database.
+ * @param git Git repository.
+ * @param mergeFactory Merge operator factory.
+ * @param merger Merge queue.
+ * @param ChangeHookRunner Hooks runner. Ref update will be send as part
+ * the rebuild.
+ * @throws OrmException Thrown, if review database cannot be accessed.
+ * @throws IOException Thrown, if Git repository cannot be accessed.
+ * @throws NoSuchRefException Thrown, if destination branch is not available.
+ */
+ public static void rebuildStaging(Branch.NameKey branch, IdentifiedUser user,
+ ReviewDb db, Repository git, MergeOp.Factory mergeFactory,
+ MergeQueue merger, ChangeHookRunner hooks)
+ throws OrmException, IOException, NoSuchRefException {
+ final Branch.NameKey stagingBranch = StagingUtil.getStagingBranch(branch);
+
+ // Start staging branch from scratch.
+ Ref ref = git.getRef(stagingBranch.get());
+ ObjectId oldTip = null;
+ if (ref != null) {
+ oldTip = ref.getObjectId();
+ }
+ StagingUtil.createStagingBranch(git, branch);
+ ref = git.getRef(branch.get());
+ ObjectId newTip = null;
+ if (ref != null) {
+ newTip = ref.getObjectId();
+ }
+
+ if (oldTip != null && newTip != null && !oldTip.equals(newTip)) {
+ hooks.doRefUpdatedHook(branch, oldTip, newTip, user.getAccount());
+ }
+
+ // Loop through all changes with status STAGED.
+ List<Change> staged = db.changes().staged(branch).toList();
+ for (Change change : staged) {
+ final PatchSet.Id currentPatchSet = change.currentPatchSetId();
+ final Change.Id changeId = currentPatchSet.getParentKey();
+
+ // Reset status to STAGING.
+ AtomicUpdate<Change> atomicUpdate =
+ getUpdateToState(Change.Status.STAGED, Change.Status.STAGING);
+ db.changes().atomicUpdate(changeId, atomicUpdate);
+ }
+
+ // Merge all changes.
+ merger.merge(mergeFactory, stagingBranch);
+ }
+
+ public static void setIntegrating(PatchSet.Id patchSetId, ReviewDb db)
+ throws OrmException {
+ final Change.Id changeId = patchSetId.getParentKey();
+ AtomicUpdate<Change> atomicUpdate = getUpdateToState(Change.Status.STAGED,
+ Change.Status.INTEGRATING);
+ db.changes().atomicUpdate(changeId, atomicUpdate);
+ }
+
+ private static AtomicUpdate<Change> getUpdateToState(final Change.Status from,
+ final Change.Status to) {
+ return new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus() == from) {
+ change.setStatus(to);
+ ChangeUtil.updated(change);
+ }
+ return change;
+ }
+ };
+ }
+
private static final char[] hexchar =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
'a', 'b', 'c', 'd', 'e', 'f'};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 7212e50ead..e98b49b509 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -26,6 +26,8 @@ import com.google.gerrit.server.git.CreateCodeReviewNotes;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.StagingMergeDelegate;
+import com.google.gerrit.server.git.SubmitMergeDelegate;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.mail.CommentSender;
@@ -63,6 +65,8 @@ public class GerritRequestModule extends FactoryModule {
factory(ReceiveCommits.Factory.class);
factory(MergeOp.Factory.class);
factory(CreateCodeReviewNotes.Factory.class);
+ factory(StagingMergeDelegate.Factory.class);
+ factory(SubmitMergeDelegate.Factory.class);
// Not really per-request, but dammit, I don't know where else to
// easily park this stuff.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 863c0bd80f..48a1e647b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -17,11 +17,15 @@ package com.google.gerrit.server.git;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.merge.MergeResult;
import org.eclipse.jgit.revwalk.RevCommit;
import java.util.List;
+import java.util.Map;
/** Extended commit entity with code review specific metadata. */
class CodeReviewCommit extends RevCommit {
@@ -59,6 +63,9 @@ class CodeReviewCommit extends RevCommit {
/** Commits which are missing ancestors of this commit. */
List<CodeReviewCommit> missing;
+ /** Merge results. */
+ Map<String, MergeResult<? extends Sequence>> mergeResults;
+
CodeReviewCommit(final AnyObjectId id) {
super(id);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 5cca5a841b..74f8795c70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -43,6 +43,7 @@ import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.workflow.CategoryFunction;
@@ -55,6 +56,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -67,8 +69,10 @@ import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeResult;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.FooterLine;
@@ -91,6 +95,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
@@ -115,6 +120,51 @@ public class MergeOp {
MergeOp create(Branch.NameKey branch);
}
+ /**
+ * Merge delegate allows variation of MergeOp behavior. This interface
+ * was added along with staging changes because staging and submit merges
+ * are slightly different. They share most of the functionality.
+ *
+ */
+ public interface MergeDelegate {
+ /**
+ * Returns a list of changes to merge.
+ * @param destBranch Destination branch for this merge request.
+ * @return List of changes to merge.
+ * @throws MergeException Thrown, if listing changes fails.
+ */
+ public List<Change> createMergeList(Branch.NameKey destBranch)
+ throws MergeException;
+
+ /**
+ * Gets the required category to complete this type of merge.
+ * @return Approval category id for the required category.
+ */
+ public ApprovalCategory.Id getRequiredApprovalCategory();
+
+ /**
+ * Customized merge status message.
+ * @param status Merge status.
+ * @param commit Target commit of the merge.
+ * @return Custom message or null. If null is returned, default submit
+ * messages are used.
+ */
+ public String getMessageForMergeStatus(CommitMergeStatus status,
+ CodeReviewCommit commit);
+
+ /**
+ * The status that the change should be set to after a successful merge.
+ * @return Final status of the change.
+ */
+ public Change.Status getStatus();
+
+ /**
+ * Indicates if staging rebuild is required after a change is merged.
+ * @return True, if staging should be rebuild after successful merge.
+ */
+ public boolean rebuildStaging();
+ }
+
private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
private static final String R_HEADS_MASTER =
Constants.R_HEADS + Constants.MASTER;
@@ -129,6 +179,8 @@ public class MergeOp {
private static final long DEPENDENCY_DELAY =
MILLISECONDS.convert(15, MINUTES);
+ private static final String R_STAGING = "refs/staging/";
+
private final GitRepositoryManager repoManager;
private final SchemaFactory<ReviewDb> schemaFactory;
private final ProjectCache projectCache;
@@ -141,12 +193,13 @@ public class MergeOp {
private final PatchSetInfoFactory patchSetInfoFactory;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final MergeQueue mergeQueue;
+ private final MergeDelegate mergeDelegate;
private final PersonIdent myIdent;
private final Branch.NameKey destBranch;
private Project destProject;
private final List<CodeReviewCommit> toMerge;
- private List<Change> submitted;
+ private List<Change> changes;
private final Map<Change.Id, CodeReviewCommit> commits;
private ReviewDb schema;
private Repository db;
@@ -160,6 +213,9 @@ public class MergeOp {
private final ChangeHookRunner hooks;
private final AccountCache accountCache;
private final CreateCodeReviewNotes.Factory codeReviewNotesFactory;
+ private final StagingMergeDelegate.Factory stagingFactory;
+ private final SubmitMergeDelegate.Factory submitFactory;
+ private final MergeOp.Factory mergeFactory;
@Inject
MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
@@ -172,7 +228,10 @@ public class MergeOp {
@GerritPersonIdent final PersonIdent myIdent,
final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
final ChangeHookRunner hooks, final AccountCache accountCache,
- final CreateCodeReviewNotes.Factory crnf) {
+ final CreateCodeReviewNotes.Factory crnf,
+ final StagingMergeDelegate.Factory stagingFactory,
+ final SubmitMergeDelegate.Factory submitFactory,
+ final MergeOp.Factory mergeFactory) {
repoManager = grm;
schemaFactory = sf;
functionState = fs;
@@ -193,6 +252,27 @@ public class MergeOp {
destBranch = branch;
toMerge = new ArrayList<CodeReviewCommit>();
commits = new HashMap<Change.Id, CodeReviewCommit>();
+
+ this.stagingFactory = stagingFactory;
+ this.submitFactory = submitFactory;
+ this.mergeDelegate = getDelegate(destBranch);
+ this.mergeFactory = mergeFactory;
+ }
+
+ /**
+ * Gets a merge delegate for this branch. It is assumed that different
+ * branches are used for different type of merging. E.g. refs/heads is
+ * used for submit and refs/staging for staging branch.
+ *
+ * @param branch Branch to get the delegete fore. E.g. refs/heads/master.
+ * @return Staging delegate.
+ */
+ private MergeDelegate getDelegate(final Branch.NameKey branch) {
+ if (branch.get().startsWith(R_STAGING)) {
+ return stagingFactory.create();
+ } else {
+ return submitFactory.create();
+ }
}
public void merge() throws MergeException {
@@ -224,7 +304,7 @@ public class MergeOp {
private void mergeImpl() throws MergeException {
openRepository();
openBranch();
- listPendingSubmits();
+ changes = mergeDelegate.createMergeList(destBranch);
validateChangeList();
mergeTip = branchTip;
switch (destProject.getSubmitType()) {
@@ -293,14 +373,6 @@ public class MergeOp {
}
}
- private void listPendingSubmits() throws MergeException {
- try {
- submitted = schema.changes().submitted(destBranch).toList();
- } catch (OrmException e) {
- throw new MergeException("Cannot query the database", e);
- }
- }
-
private void validateChangeList() throws MergeException {
final Set<ObjectId> tips = new HashSet<ObjectId>();
for (final Ref r : db.getAllRefs().values()) {
@@ -308,7 +380,7 @@ public class MergeOp {
}
int commitOrder = 0;
- for (final Change chg : submitted) {
+ for (final Change chg : changes) {
final Change.Id changeId = chg.getId();
if (chg.currentPatchSetId() == null) {
commits.put(changeId, CodeReviewCommit
@@ -460,6 +532,9 @@ public class MergeOp {
} else {
failed(n, CommitMergeStatus.PATH_CONFLICT);
+ if (m instanceof ResolveMerger) {
+ n.mergeResults = ((ResolveMerger) m).getMergeResults();
+ }
}
} catch (IOException e) {
if (e.getMessage().startsWith("Multiple merge bases for")) {
@@ -568,6 +643,13 @@ public class MergeOp {
identifiedUserFactory.create(submitter.getAccountId());
Set<String> emails = new HashSet<String>();
for (RevCommit c : codeReviewCommits) {
+ try {
+ rw.parseBody(c);
+ } catch (MissingObjectException e) {
+ log.error(e.getMessage());
+ } catch (IOException e) {
+ log.error(e.getMessage());
+ }
emails.add(c.getAuthorIdent().getEmailAddress());
}
@@ -666,6 +748,9 @@ public class MergeOp {
} else {
n.statusCode = CommitMergeStatus.PATH_CONFLICT;
+ if (m instanceof ResolveMerger) {
+ n.mergeResults = ((ResolveMerger) m).getMergeResults();
+ }
}
} else {
@@ -775,6 +860,10 @@ public class MergeOp {
continue;
}
+ if (ApprovalCategory.STAGING.equals(a.getCategoryId())) {
+ continue;
+ }
+
final Account acc =
identifiedUserFactory.create(a.getAccountId()).getAccount();
final StringBuilder identbuf = new StringBuilder();
@@ -932,8 +1021,8 @@ public class MergeOp {
private void updateChangeStatus() throws MergeException {
List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
+ for (final Change c : changes) {
- for (final Change c : submitted) {
final CodeReviewCommit commit = commits.get(c.getId());
final CommitMergeStatus s = commit != null ? commit.statusCode : null;
if (s == null) {
@@ -943,19 +1032,19 @@ public class MergeOp {
continue;
}
+ String txt = mergeDelegate.getMessageForMergeStatus(s, commit);
+ if (txt == null) {
+ txt = getDefaultMessage(s, commit);
+ }
+
switch (s) {
case CLEAN_MERGE: {
- final String txt =
- "Change has been successfully merged into the git repository.";
setMerged(c, message(c, txt));
merged.add(commit);
break;
}
case CLEAN_PICK: {
- final String txt =
- "Change has been successfully cherry-picked as " + commit.name()
- + ".";
setMerged(c, message(c, txt));
merged.add(commit);
break;
@@ -967,37 +1056,30 @@ public class MergeOp {
break;
case PATH_CONFLICT: {
- final String txt =
- "Your change could not be merged due to a path conflict.\n"
- + "\n"
- + "Please merge (or rebase) the change locally and upload the resolution for review.";
+ if (commit.mergeResults != null) {
+ txt += "\n\nConflicting files:";
+ for (Entry<String, MergeResult<? extends Sequence>> entry
+ : commit.mergeResults.entrySet()) {
+ if (entry.getValue().containsConflicts()) {
+ txt += "\n- " + entry.getKey();
+ }
+ }
+ }
setNew(c, message(c, txt));
break;
}
case CRISS_CROSS_MERGE: {
- final String txt =
- "Your change requires a recursive merge to resolve.\n"
- + "\n"
- + "Please merge (or rebase) the change locally and upload the resolution for review.";
setNew(c, message(c, txt));
break;
}
case CANNOT_CHERRY_PICK_ROOT: {
- final String txt =
- "Cannot cherry-pick an initial commit onto an existing branch.\n"
- + "\n"
- + "Please merge the change locally and upload the merge commit for review.";
setNew(c, message(c, txt));
break;
}
case NOT_FAST_FORWARD: {
- final String txt =
- "Project policy requires all submissions to be a fast-forward.\n"
- + "\n"
- + "Please rebase the change locally and upload again for review.";
setNew(c, message(c, txt));
break;
}
@@ -1008,13 +1090,13 @@ public class MergeOp {
}
default:
- setNew(c, message(c, "Unspecified merge failure: " + s.name()));
+ setNew(c, message(c, txt));
break;
}
}
CreateCodeReviewNotes codeReviewNotes =
- codeReviewNotesFactory.create(schema, db);
+ codeReviewNotesFactory.create(schema, db);
try {
codeReviewNotes.create(merged, computeAuthor(merged));
} catch (CodeReviewNoteCreationException e) {
@@ -1024,6 +1106,60 @@ public class MergeOp {
GitRepositoryManager.REFS_NOTES_REVIEW);
}
+ private String getDefaultMessage(final CommitMergeStatus status,
+ final CodeReviewCommit commit) {
+ switch (status) {
+ case CLEAN_MERGE: {
+ return
+ "Change has been successfully merged into the git repository.";
+ }
+
+ case CLEAN_PICK: {
+ return
+ "Change has been successfully cherry-picked as " + commit.name()
+ + ".";
+ }
+
+ case ALREADY_MERGED:
+ return null;
+
+ case PATH_CONFLICT: {
+ return
+ "Your change could not be merged due to a path conflict.\n"
+ + "\n"
+ + "Please merge (or rebase) the change locally and upload the resolution for review.";
+ }
+
+ case CRISS_CROSS_MERGE: {
+ return
+ "Your change requires a recursive merge to resolve.\n"
+ + "\n"
+ + "Please merge (or rebase) the change locally and upload the resolution for review.";
+ }
+
+ case CANNOT_CHERRY_PICK_ROOT: {
+ return
+ "Cannot cherry-pick an initial commit onto an existing branch.\n"
+ + "\n"
+ + "Please merge the change locally and upload the merge commit for review.";
+ }
+
+ case NOT_FAST_FORWARD: {
+ return
+ "Project policy requires all submissions to be a fast-forward.\n"
+ + "\n"
+ + "Please rebase the change locally and upload again for review.";
+ }
+
+ case MISSING_DEPENDENCY: {
+ dependencyError(commit);
+ }
+
+ default:
+ return "Unspecified merge failure: " + status.name();
+ }
+ }
+
private void dependencyError(final CodeReviewCommit commit) {
final Change c = commit.change;
if (commit.missing == null) {
@@ -1182,7 +1318,7 @@ public class MergeOp {
schema.patchSetApprovals().byPatchSet(c).toList();
for (PatchSetApproval a : approvals) {
if (a.getValue() > 0
- && ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+ && mergeDelegate.getRequiredApprovalCategory().equals(a.getCategoryId())) {
if (submitter == null
|| a.getGranted().compareTo(submitter.getGranted()) > 0) {
submitter = a;
@@ -1198,12 +1334,13 @@ public class MergeOp {
final Topic.Id topicId = c.getTopicId();
final Change.Id changeId = c.getId();
final PatchSet.Id merged = c.currentPatchSetId();
+ final Change.Status newStatus = mergeDelegate.getStatus();
try {
schema.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
@Override
public Change update(Change c) {
- c.setStatus(Change.Status.MERGED);
+ c.setStatus(newStatus);
if (!merged.equals(c.currentPatchSetId())) {
// Uncool; the patch set changed after we merged it.
// Go back to the patch set that was actually merged.
@@ -1263,7 +1400,7 @@ public class MergeOp {
//
PatchSetApproval submitter = null;
try {
- c.setStatus(Change.Status.MERGED);
+ c.setStatus(newStatus);
final List<PatchSetApproval> approvals =
schema.patchSetApprovals().byChange(changeId).toList();
final FunctionState fs = functionState.create(c, merged, approvals);
@@ -1272,7 +1409,7 @@ public class MergeOp {
}
for (PatchSetApproval a : approvals) {
if (a.getValue() > 0
- && ApprovalCategory.SUBMIT.equals(a.getCategoryId())
+ && mergeDelegate.getRequiredApprovalCategory().equals(a.getCategoryId())
&& a.getPatchSetId().equals(merged)) {
if (submitter == null
|| a.getGranted().compareTo(submitter.getGranted()) > 0) {
@@ -1297,17 +1434,20 @@ public class MergeOp {
}
}
- try {
- final MergedSender cm = mergedSenderFactory.create(c);
- if (submitter != null) {
- cm.setFrom(submitter.getAccountId());
+ // Send notification e-mail only about merges to refs/heads
+ if (destBranch.get().startsWith(Constants.R_HEADS)) {
+ try {
+ final MergedSender cm = mergedSenderFactory.create(c);
+ if (submitter != null) {
+ cm.setFrom(submitter.getAccountId());
+ }
+ cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
+ cm.send();
+ } catch (OrmException e) {
+ log.error("Cannot send email for submitted patch set " + c.getId(), e);
+ } catch (EmailException e) {
+ log.error("Cannot send email for submitted patch set " + c.getId(), e);
}
- cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
- cm.send();
- } catch (OrmException e) {
- log.error("Cannot send email for submitted patch set " + c.getId(), e);
- } catch (EmailException e) {
- log.error("Cannot send email for submitted patch set " + c.getId(), e);
}
try {
@@ -1317,6 +1457,24 @@ public class MergeOp {
} catch (OrmException ex) {
log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
}
+
+ if (mergeDelegate.rebuildStaging()) {
+ try {
+ IdentifiedUser who =
+ identifiedUserFactory.create(submitter.getAccountId());
+ ChangeUtil.rebuildStaging(c.getDest(), who, schema, db, mergeFactory,
+ mergeQueue, hooks);
+ } catch (OrmException e) {
+ log.error("Cannot rebuild staging after merging patch set "
+ + c.getId(), e);
+ } catch (IOException e) {
+ log.error("Cannot rebuild staging after merging patch set "
+ + c.getId(), e);
+ } catch (NoSuchRefException e) {
+ log.error("Cannot rebuild staging after merging patch set "
+ + c.getId(), e);
+ }
+ }
}
private void setNew(Change c, ChangeMessage msg) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index dd89a5a44a..9c58bf5b0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -59,6 +59,7 @@ import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl;
@@ -159,6 +160,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
private final String canonicalWebUrl;
private final PersonIdent gerritIdent;
private final TrackingFooters trackingFooters;
+ private final MergeOp.Factory mergeFactory;
+ private final MergeQueue merger;
private final ProjectControl projectControl;
private final Project project;
@@ -197,6 +200,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
@GerritPersonIdent final PersonIdent gerritIdent,
final TrackingFooters trackingFooters,
+ final MergeOp.Factory mergeFactory,
+ final MergeQueue merger,
@Assisted final ProjectControl projectControl,
@Assisted final Repository repo) throws IOException {
@@ -216,6 +221,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
this.canonicalWebUrl = canonicalWebUrl;
this.gerritIdent = gerritIdent;
this.trackingFooters = trackingFooters;
+ this.mergeFactory = mergeFactory;
+ this.merger = merger;
this.projectControl = projectControl;
this.project = projectControl.getProject();
@@ -1562,6 +1569,10 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
reject(request.cmd, "change " + request.ontoChange + " closed");
return null;
}
+ if (change.getStatus() == Change.Status.INTEGRATING) {
+ reject(request.cmd, "change " + request.ontoChange + " is already INTEGRATING");
+ return null;
+ }
if (change.getStatus().equals(AbstractEntity.Status.ABANDONED)) {
// It is needed a special behavior in case we are working with topics
//
@@ -1759,6 +1770,10 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
db.changeMessages().insert(Collections.singleton(msg));
result.msg = msg;
+ // Check staging status before change status is updated.
+ boolean inStaging = (change.getStatus() == Change.Status.STAGED
+ || change.getStatus() == Change.Status.STAGING);
+
if (result.mergedIntoRef != null) {
// Change was already submitted to a branch, close it.
//
@@ -1840,6 +1855,14 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
sendMergedEmail(result);
+ if (inStaging) {
+ try {
+ ChangeUtil.rebuildStaging(change.getDest(), currentUser, db,
+ repo, mergeFactory, merger, hooks);
+ } catch (NoSuchRefException e) {
+ // Destination branch not available.
+ }
+ }
return result != null ? result.info.getKey() : null;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
index 356981d4db..972a48e80b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
@@ -53,6 +53,10 @@ public class ReloadSubmitQueueOp extends DefaultQueueOp {
for (final Change change : c.changes().allSubmitted()) {
pending.add(change.getDest());
}
+ // Include staging changes.
+ for (final Change change : c.changes().allStaging()) {
+ pending.add(StagingUtil.getStagingBranch(change.getDest()));
+ }
} finally {
c.close();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/StagingMergeDelegate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/StagingMergeDelegate.java
new file mode 100644
index 0000000000..42172fbb96
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/StagingMergeDelegate.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2011 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.git;
+
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.git.MergeOp.MergeDelegate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+
+import java.util.List;
+
+/**
+ * MergeOp variation for staging merges.
+ *
+ */
+public class StagingMergeDelegate implements MergeDelegate {
+ /**
+ * Factory interface for creating delegates.
+ */
+ public interface Factory {
+ StagingMergeDelegate create();
+ }
+
+ private final SchemaFactory<ReviewDb> reviewDbFactory;
+
+ @Inject
+ public StagingMergeDelegate(final SchemaFactory<ReviewDb> reviewDbFactory) {
+ this.reviewDbFactory = reviewDbFactory;
+ }
+
+ @Override
+ public List<Change> createMergeList(final Branch.NameKey destBranch)
+ throws MergeException {
+ ReviewDb reviewDb = null;
+ try {
+ // Open the review database.
+ reviewDb = reviewDbFactory.open();
+
+ // List all changes with STAGING status in the destination branch.
+ Branch.NameKey sourceBranch = StagingUtil.getSourceBranch(destBranch);
+ List<Change> inStaging = reviewDb.changes().staging(sourceBranch).toList();
+ return inStaging;
+ } catch (OrmException e) {
+ throw new MergeException("Cannot query the database", e);
+ } finally {
+ // Close the review database.
+ if (reviewDb != null) {
+ reviewDb.close();
+ }
+ }
+ }
+
+ @Override
+ public ApprovalCategory.Id getRequiredApprovalCategory() {
+ return ApprovalCategory.STAGING;
+ }
+
+ @Override
+ public String getMessageForMergeStatus(final CommitMergeStatus status,
+ final CodeReviewCommit commit) {
+ switch (status) {
+ case CLEAN_MERGE: {
+ return
+ "Change has been successfully merged into the staging branch.";
+ }
+ case CLEAN_PICK: {
+ return
+ "Change has been successfully cherry-picked to the staging branch as " + commit.name()
+ + ".";
+ }
+ case PATH_CONFLICT: {
+ return
+ "Your change could not be merged due to a path conflict.\n"
+ + "\n"
+ + "Please rebase the change locally against the destination branch and upload a new patch set.\n"
+ + "\n"
+ + "If the conflict is with another change, wait until it gets integrated before rebasing.";
+ }
+ case CRISS_CROSS_MERGE: {
+ return
+ "Your change requires a recursive merge to resolve.\n"
+ + "\n"
+ + "Please rebase the change locally against the destination branch and upload a new patch set.\n"
+ + "\n"
+ + "If the conflict is with another change, wait until it gets integrated before rebasing.";
+ }
+ case CANNOT_CHERRY_PICK_ROOT: {
+ return
+ "Cannot cherry-pick an initial commit onto an existing branch.\n"
+ + "\n"
+ + "Please rebase the change locally against the destination branch and upload a new patch set.\n"
+ + "\n"
+ + "If the conflict is with another change, wait until it gets integrated before rebasing.";
+ }
+ case NOT_FAST_FORWARD: {
+ return
+ "Project policy requires all submissions to be a fast-forward.\n"
+ + "\n"
+ + "Please rebase the change locally against the destination branch and upload a new patch set.\n"
+ + "\n"
+ + "If the conflict is with another change, wait until it gets integrated before rebasing.";
+ }
+ default: {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "staging";
+ }
+
+ @Override
+ public Change.Status getStatus() {
+ return Change.Status.STAGED;
+ }
+
+ @Override
+ public boolean rebuildStaging() {
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/StagingUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/StagingUtil.java
new file mode 100644
index 0000000000..1c3f1654fa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/StagingUtil.java
@@ -0,0 +1,266 @@
+// Copyright (C) 2011 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.git;
+
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.server.project.NoSuchRefException;
+
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import java.io.IOException;
+
+/**
+ * Utility methods for working with staging branches.
+ */
+public class StagingUtil {
+ private static final String R_HEADS = "refs/heads/";
+ private static final String R_STAGING = "refs/staging/";
+ private static final String R_BUILDS = "refs/builds/";
+
+ /**
+ * Gets a staging branch for a branch.
+ * @param branch Branch under refs/heads. E.g. refs/heads/master. Can be short
+ * name.
+ * @return Matching staging branch. E.g. refs/staging/master
+ */
+ public static Branch.NameKey getStagingBranch(final Branch.NameKey branch) {
+ return getBranchWithNewPrefix(branch, R_HEADS, R_STAGING);
+ }
+
+ /**
+ * Gets a source branch for a staging branch.
+ * @param staging Staging branch, e.g. refs/staging/master. Can be short name.
+ * @return Matching branch refs/heads, e.g. refs/heads/master.
+ */
+ public static Branch.NameKey getSourceBranch(final Branch.NameKey staging) {
+ return getBranchWithNewPrefix(staging, R_STAGING, R_HEADS);
+ }
+
+ private static Branch.NameKey getBranchWithNewPrefix(
+ final Branch.NameKey branch,
+ final String oldPrefix,
+ final String newPrefix) {
+ final String ref = branch.get();
+
+ // Calculate number of components in old prefix.
+ final char separatorChar = '/';
+
+ // There is at least one component in each string, string itself.
+ // Components are split by '/'.
+ int componentCount = 1;
+
+ // Calculate number of components. Each component is separated by
+ // '/' character. calling String.indexOf(char, int) with index + 1 causes
+ // it to search from the previous position of the separator character.
+ // indexOf method returns -1 when no separator character was found.
+ int index = 0;
+ while ((index = oldPrefix.indexOf(separatorChar, index + 1)) != -1) {
+ componentCount++;
+ }
+
+ final String[] components = ref.split("/", componentCount);
+ if (ref.startsWith(oldPrefix) && components.length == componentCount) {
+ // Create new ref replacing the old prefix with new.
+ return new Branch.NameKey(branch.getParentKey(),
+ newPrefix + components[components.length - 1]);
+ } else {
+ // Treat the ref as short name.
+ return new Branch.NameKey(branch.getParentKey(),
+ newPrefix + ref);
+ }
+ }
+
+ /**
+ * Checks if branch exists.
+ * @param git Git repository to search for the branch.
+ * @param branch Branch name key.
+ * @return True if branch exists.
+ * @throws IOException Thrown, if the repository cannot be accessed.
+ */
+ public static boolean branchExists(Repository git,
+ final Branch.NameKey branch) throws IOException {
+ return git.getRef(branch.get()) != null;
+ }
+
+ /**
+ * Create a staging branch from branch in refs/heads.
+ *
+ * @param git Git repository.
+ * @param sourceBranch Branch under refs/heads. Can be short name,
+ * e.g. master.
+ * @return Result of the ref update.
+ * @throws IOException Thrown if repository cannot be accessed.
+ * @throws NoSuchRefException Thrown if sourceBranch parameter does not exist
+ * in the repository.
+ */
+ public static Result createStagingBranch(Repository git,
+ final Branch.NameKey sourceBranch) throws IOException, NoSuchRefException {
+ final String sourceBranchName;
+ if (sourceBranch.get().startsWith(R_HEADS)) {
+ sourceBranchName = sourceBranch.get();
+ } else {
+ sourceBranchName = R_HEADS + sourceBranch.get();
+ }
+
+ final String stagingBranch = R_STAGING + sourceBranch.getShortName();
+
+ return updateRef(git, stagingBranch, sourceBranchName, true);
+ }
+
+ /**
+ * Create a staging branch from branch in refs/heads. Before updating the
+ * staging branch, it is verfieid that its SHA value matches oldRef.
+ *
+ * @param git Git repository.
+ * @param sourceBranch Branch under refs/heads. Can be short name,
+ * e.g. master.
+ * @param oldRef Staging branch SHA value must match the value of this ref.
+ * @return Result of the ref update.
+ * @throws IOException Thrown if repository cannot be accessed.
+ * @throws NoSuchRefException Thrown if sourceBranch parameter does not exist
+ * in the repository.
+ */
+ public static Result createStagingBranch(Repository git,
+ final Branch.NameKey sourceBranch, final Branch.NameKey oldRef)
+ throws IOException, NoSuchRefException {
+ final String sourceBranchName;
+ if (sourceBranch.get().startsWith(R_HEADS)) {
+ sourceBranchName = sourceBranch.get();
+ } else {
+ sourceBranchName = R_HEADS + sourceBranch.get();
+ }
+
+ final String stagingBranch = R_STAGING + sourceBranch.getShortName();
+
+ return updateRef(git, stagingBranch, sourceBranchName, oldRef.get());
+ }
+
+ /**
+ * Creates a build ref. Build refs are stored under refs/builds.
+ *
+ * @param git Git repository.
+ * @param stagingBranch Staging branch to create the build ref from. Can be
+ * short name.
+ * @param newBranch Build ref name, under refs/builds. Can be short name.
+ * @return
+ * @throws IOException
+ * @throws NoSuchRefException
+ */
+ public static Result createBuildRef(Repository git,
+ final Branch.NameKey stagingBranch, final Branch.NameKey newBranch)
+ throws IOException, NoSuchRefException {
+ final String stagingBranchName;
+ if (stagingBranch.get().startsWith(R_STAGING)) {
+ stagingBranchName = stagingBranch.get();
+ } else {
+ stagingBranchName = R_STAGING + stagingBranch.get();
+ }
+
+ final String buildBranchName;
+ if (newBranch.get().startsWith(R_BUILDS)) {
+ buildBranchName = newBranch.get();
+ } else {
+ buildBranchName = R_BUILDS + newBranch.get();
+ }
+
+ return updateRef(git, buildBranchName, stagingBranchName, false);
+ }
+
+ /**
+ * Update branch from build ref. Replaces the branch ref with a build ref.
+ *
+ * @param git Git repository.
+ * @param branch Branch name under refs/heads. Can be short name.
+ * @param build Build branch name under refs/builds. Can be short name.
+ * @return Update ref result.
+ * @throws IOException Thrown if Git repository cannot be accessed.
+ * @throws NoSuchRefException Thrown if Build ref does not exist
+ */
+ public static Result updateBrachFromBuild(Repository git,
+ final Branch.NameKey branch, final Branch.NameKey build)
+ throws IOException, NoSuchRefException {
+ final String branchName;
+ if (branch.get().startsWith(R_HEADS)) {
+ branchName = branch.get();
+ } else {
+ branchName = R_HEADS + branch.get();
+ }
+
+ final String buildName;
+ if (build.get().startsWith(R_BUILDS)) {
+ buildName = build.get();
+ } else {
+ buildName = R_BUILDS + build.get();
+ }
+
+ return updateRef(git, branchName, buildName, false);
+ }
+
+ /**
+ * Checks if a patch set (commit) is in the staging branch.
+ *
+ * @param git Git repository.
+ * @param branch Branch under refs/heads.
+ * @param patchSetId Patch set id.
+ * @return True if the patch set can be found from the staging branch.
+ */
+ public static boolean isInStagingBranch(Repository git,
+ final Branch.NameKey branch, final PatchSet.Id patchSetId) {
+ Ref ref = null;
+ try {
+ ref = git.getRef(R_STAGING + branch.getShortName());
+ } catch (IOException e) {
+ // Could not access git repository. Fall through to return false.
+ }
+
+ if (ref == null) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private static Result updateRef(Repository git, final String ref,
+ final String newValue, final String oldValue) throws IOException,
+ NoSuchRefException {
+ Ref newRef = git.getRef(newValue);
+ if (newRef == null) {
+ throw new NoSuchRefException(newValue);
+ }
+ Ref oldRef = git.getRef(oldValue);
+ if (oldRef == null) {
+ throw new NoSuchRefException(oldValue);
+ }
+ RefUpdate refUpdate = git.updateRef(ref);
+ refUpdate.setNewObjectId(newRef.getObjectId());
+ refUpdate.setExpectedOldObjectId(oldRef.getObjectId());
+ return refUpdate.update();
+ }
+
+ private static Result updateRef(Repository git, final String ref,
+ final String newValue, final boolean force) throws IOException,
+ NoSuchRefException {
+ Ref sourceRef = git.getRef(newValue);
+ if (sourceRef == null) {
+ throw new NoSuchRefException(newValue);
+ }
+ RefUpdate refUpdate = git.updateRef(ref);
+ refUpdate.setNewObjectId(sourceRef.getObjectId());
+ refUpdate.setForceUpdate(force);
+ return refUpdate.update();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitMergeDelegate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitMergeDelegate.java
new file mode 100644
index 0000000000..364992b9fe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitMergeDelegate.java
@@ -0,0 +1,80 @@
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.ApprovalCategory.Id;
+import com.google.gerrit.reviewdb.Branch.NameKey;
+import com.google.gerrit.server.git.MergeOp.MergeDelegate;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+
+import java.util.List;
+
+/**
+ * MergeOp variation for submit merges.
+ *
+ */
+public class SubmitMergeDelegate implements MergeDelegate {
+ /**
+ * Factory interface for creating delegates.
+ */
+ public interface Factory {
+ SubmitMergeDelegate create();
+ }
+
+ private final SchemaFactory<ReviewDb> reviewDbFactory;
+
+ @Inject
+ public SubmitMergeDelegate(final SchemaFactory<ReviewDb> reviewDbFactory) {
+ this.reviewDbFactory = reviewDbFactory;
+ }
+
+ @Override
+ public List<Change> createMergeList(NameKey destBranch) throws MergeException {
+ ReviewDb reviewDb = null;
+ try {
+ // Open review database.
+ reviewDb = reviewDbFactory.open();
+
+ // List all submitted changes in the destination branch.
+ List<Change> inStaging = reviewDb.changes().submitted(destBranch).toList();
+ return inStaging;
+ } catch (OrmException e) {
+ throw new MergeException("Cannot query the database", e);
+ } finally {
+ if (reviewDb != null) {
+ // Close the review database.
+ reviewDb.close();
+ }
+ }
+ }
+
+ @Override
+ public Id getRequiredApprovalCategory() {
+ return ApprovalCategory.SUBMIT;
+ }
+
+ @Override
+ public String getMessageForMergeStatus(CommitMergeStatus status,
+ CodeReviewCommit commit) {
+ // Use default messages.
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "submit";
+ }
+
+ @Override
+ public Change.Status getStatus() {
+ return Change.Status.MERGED;
+ }
+
+ @Override
+ public boolean rebuildStaging() {
+ return true;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index db297aec17..a2a7f22eb4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -27,19 +27,26 @@ import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.mail.CommentSender;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -67,6 +74,9 @@ public class PublishComments implements Callable<VoidResult> {
private final ChangeControl.Factory changeControlFactory;
private final FunctionState.Factory functionStateFactory;
private final ChangeHookRunner hooks;
+ private final GitRepositoryManager gitManager;
+ private final MergeOp.Factory mergeFactory;
+ private final MergeQueue merger;
private final PatchSet.Id patchSetId;
private final String messageText;
@@ -85,6 +95,9 @@ public class PublishComments implements Callable<VoidResult> {
final ChangeControl.Factory changeControlFactory,
final FunctionState.Factory functionStateFactory,
final ChangeHookRunner hooks,
+ final GitRepositoryManager gitManager,
+ final MergeOp.Factory mergeFactory,
+ final MergeQueue merger,
@Assisted final PatchSet.Id patchSetId,
@Assisted final String messageText,
@@ -97,6 +110,9 @@ public class PublishComments implements Callable<VoidResult> {
this.changeControlFactory = changeControlFactory;
this.functionStateFactory = functionStateFactory;
this.hooks = hooks;
+ this.gitManager = gitManager;
+ this.mergeFactory = mergeFactory;
+ this.merger = merger;
this.patchSetId = patchSetId;
this.messageText = messageText;
@@ -104,7 +120,8 @@ public class PublishComments implements Callable<VoidResult> {
}
@Override
- public VoidResult call() throws NoSuchChangeException, OrmException {
+ public VoidResult call() throws NoSuchChangeException, OrmException,
+ InvalidChangeOperationException, NoSuchRefException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl ctl = changeControlFactory.validateFor(changeId);
change = ctl.getChange();
@@ -117,8 +134,17 @@ public class PublishComments implements Callable<VoidResult> {
publishDrafts();
final boolean isCurrent = patchSetId.equals(change.currentPatchSetId());
- if (isCurrent && change.getStatus().isOpen()) {
+ // Only message will be published for changes with status INTEGRATING.
+ if (isCurrent && change.getStatus().isOpen()
+ && change.getStatus() != Change.Status.INTEGRATING) {
publishApprovals();
+ // Update staging, if score required for staging was removed.
+ // E.g. Existing +2 code review changed to +1 or -2 score was added.
+ if (change.getStatus() == Change.Status.STAGED && !canRemainInStaging()) {
+ removeChangeFromStaging();
+ }
+ } else if (!change.getStatus().isOpen() && !approvals.isEmpty()) {
+ throw new InvalidChangeOperationException("Change is closed");
} else {
publishMessageOnly();
}
@@ -316,4 +342,25 @@ public class PublishComments implements Callable<VoidResult> {
}
}
}
+
+ private void removeChangeFromStaging() throws NoSuchChangeException,
+ OrmException, IOException, NoSuchRefException {
+ ChangeUtil.rejectStagedChange(patchSetId, user, db);
+ Repository git = gitManager.openRepository(change.getProject());
+ try {
+ ChangeUtil.rebuildStaging(change.getDest(), user, db, git,
+ mergeFactory, merger, hooks);
+ } finally {
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+
+ private boolean canRemainInStaging() throws OrmException,
+ NoSuchChangeException {
+ final ChangeControl control = changeControlFactory.controlFor(change);
+ return control.hasValidCategoryFunctions(patchSet.getId(), db, types,
+ functionStateFactory);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index fcf7455ba7..1feee1b2cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -22,14 +22,17 @@ import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.workflow.CategoryFunction;
+import com.google.gerrit.server.workflow.MaxWithBlock;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.util.ArrayList;
import java.util.List;
@@ -151,11 +154,16 @@ public class ChangeControl {
/** Can this user abandon this change? */
public boolean canAbandon() {
if (change.getTopicId() != null) return false;
- return isOwner() // owner (aka creator) of the change can abandon
+ boolean userCan = isOwner() // owner (aka creator) of the change can abandon
|| getRefControl().isOwner() // branch owner can abandon
|| getProjectControl().isOwner() // project owner can abandon
|| getCurrentUser().isAdministrator() // site administers are god
;
+
+ // Cannot abandon changes that are already processed by the continuous
+ // integration system.
+ return userCan
+ && (change.getStatus() != Change.Status.INTEGRATING);
}
/** Can this user restore this change? */
@@ -269,4 +277,106 @@ public class ChangeControl {
return CanSubmitResult.OK;
}
+
+ /**
+ * Checks if patch set can be merged to a staging branch.
+ * @param patchSetId Patch set ID.
+ * @param db Review database.
+ * @param approvalTypes Approval types for this patch set.
+ * @param functionStateFactory Factory for creating check functions for
+ * different approval categories.
+ * @return Result indicating if the patch set can be merged or not.
+ * @throws OrmException Thrown if review database cannot accessed.
+ */
+ public CanSubmitResult canMergeToStaging(final PatchSet.Id patchSetId,
+ final ReviewDb db, final ApprovalTypes approvalTypes,
+ FunctionState.Factory functionStateFactory)
+ throws OrmException {
+ // Check that the state of the patch set and its parent change are valid.
+ CanSubmitResult result = canMergeToStaging(patchSetId);
+ if (result != CanSubmitResult.OK) {
+ return result;
+ }
+
+ // List all approvals for this patch set.
+ final List<PatchSetApproval> allApprovals =
+ new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
+ patchSetId).toList());
+
+ // Create function for staging approval category and run it.
+ final FunctionState fs =
+ functionStateFactory.create(change, patchSetId, allApprovals);
+ for (ApprovalType c : approvalTypes.getApprovalTypes()) {
+ CategoryFunction.forCategory(c.getCategory()).run(c, fs);
+ }
+
+ // There is nothing preventing the merge. Return OK result.
+ return CanSubmitResult.OK;
+ }
+
+ /**
+ * Checks if the patch set and its parent change are in correct state for
+ * merge to staging.
+ *
+ * @param patchSetId Patch set ID.
+ * @return CanSubmitResult.OK if patch set can be merged to staging.
+ */
+ public CanSubmitResult canMergeToStaging(final PatchSet.Id patchSetId) {
+ if (change.getStatus() != Change.Status.NEW) {
+ return new CanSubmitResult("Change " + change.currPatchSetId().getParentKey() + " is not NEW");
+ }
+ if (!patchSetId.equals(change.currentPatchSetId())) {
+ return new CanSubmitResult("Patch set " + patchSetId + " is not current");
+ }
+ if (!getRefControl().canBranchToStaging()) {
+ return new CanSubmitResult("User does not have permission to submit");
+ }
+ if (!(getCurrentUser() instanceof IdentifiedUser)) {
+ return new CanSubmitResult("User is not signed-in");
+ }
+ return CanSubmitResult.OK;
+ }
+
+ /**
+ * Checks if change has valid approval in categories that are using
+ * MaxWithBlock function. This function is used by categories like code
+ * review or verified.
+ *
+ * @param patchSetId Patch set ID.
+ * @param db Review database.
+ * @param approvalTypes Configured approval types.
+ * @param functionStateFactory Function state factory.
+ * @return True if there is max score available in all MaxWithBlock
+ * categories.
+ * @throws OrmException Thrown, if datbase access fails.
+ */
+ public boolean hasValidCategoryFunctions(final PatchSet.Id patchSetId,
+ final ReviewDb db, final ApprovalTypes approvalTypes,
+ FunctionState.Factory functionStateFactory) throws OrmException {
+ // List all approvals for this patch set.
+ final List<PatchSetApproval> allApprovals =
+ new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
+ patchSetId).toList());
+
+ // Create function for staging approval category and run it.
+ final FunctionState fs =
+ functionStateFactory.create(change, patchSetId, allApprovals);
+
+ // Flag to summarize category approvals.
+ boolean succeeded = true;
+
+ // Loop through all configured approval types.
+ for (ApprovalType c : approvalTypes.getApprovalTypes()) {
+ // Only check categoriess using MaxWithBlock function.
+ if (c.getCategory().getFunctionName().equals(MaxWithBlock.NAME)) {
+ // Run the function and check status from Function State object.
+ CategoryFunction.forCategory(c.getCategory()).run(c, fs);
+ if (!fs.isValid(c)) {
+ succeeded = false;
+ }
+ }
+ }
+
+ return succeeded;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index ff1d09fd53..0f1aa27f8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.project;
+import static com.google.gerrit.reviewdb.ApprovalCategory.STAGING;
+
import com.google.gerrit.common.CollectionsUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ParamertizedString;
@@ -304,6 +306,10 @@ public class RefControl {
return canPerform(Permission.FORGE_SERVER);
}
+ public boolean canBranchToStaging() {
+ return canPerform(Permission.STAGE);
+ }
+
/** All value ranges of any allowed label permission. */
public List<PermissionRange> getLabelRanges() {
List<PermissionRange> r = new ArrayList<PermissionRange>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 9b6812fe5d..764558a684 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -32,6 +32,7 @@ import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.workflow.StagingFunction;
import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtjsonrpc.server.SignedToken;
@@ -116,6 +117,7 @@ public class SchemaCreator {
// TODO This should never be null when initializing a site.
initWildCardProject();
}
+ initStagingCategory(db);
final SqlDialect d = jdbc.getDialect();
if (d instanceof DialectH2) {
@@ -304,6 +306,19 @@ public class SchemaCreator {
c.approvalCategoryValues().insert(vals);
}
+ private void initStagingCategory(final ReviewDb c) throws OrmException {
+ final ApprovalCategory cat;
+ final ArrayList<ApprovalCategoryValue> vals;
+
+ cat = new ApprovalCategory(ApprovalCategory.STAGING, "Staging");
+ cat.setPosition((short) -1);
+ cat.setFunctionName(StagingFunction.NAME);
+ vals = new ArrayList<ApprovalCategoryValue>();
+ vals.add(value(cat, 1, "Staging"));
+ c.approvalCategories().insert(Collections.singleton(cat));
+ c.approvalCategoryValues().insert(vals);
+ }
+
private static ApprovalCategoryValue value(final ApprovalCategory cat,
final int value, final String name) {
return new ApprovalCategoryValue(new ApprovalCategoryValue.Id(cat.getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
index ffed95a1de..94901c3428 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
@@ -32,6 +32,7 @@ public abstract class CategoryFunction {
all.put(MaxNoBlock.NAME, new MaxNoBlock());
all.put(NoOpFunction.NAME, new NoOpFunction());
all.put(NoBlock.NAME, new NoBlock());
+ all.put(StagingFunction.NAME, new StagingFunction());
}
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/StagingFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/StagingFunction.java
new file mode 100644
index 0000000000..cd0b700f39
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/StagingFunction.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2011 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.workflow;
+
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.CurrentUser;
+
+/**
+ * Staging category function. Checks that the user has staging access and
+ * that the change has status NEW.
+ *
+ */
+public class StagingFunction extends CategoryFunction {
+ public static String NAME = "Staging";
+
+ @Override
+ public void run(final ApprovalType at, final FunctionState state) {
+ state.valid(at, valid(state));
+ }
+
+ @Override
+ public boolean isValid(final CurrentUser user, final ApprovalType at,
+ final FunctionState state) {
+ return !state.controlFor(user).getRange(Permission.forLabel(NAME)).isEmpty();
+ }
+
+ private static boolean valid(final FunctionState state) {
+ if (state.getChange().getStatus() != Change.Status.NEW) {
+ return false;
+ }
+ for (final ApprovalType t : state.getApprovalTypes()) {
+ if (!state.isValid(t)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index a0e3554c8b..f83d6d805f 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -17,7 +17,7 @@
# limitations under the License.
#
-CHANGE_ID_AFTER="Bug|Issue"
+CHANGE_ID_AFTER="Bug|Issue|Task-number"
MSG="$1"
# Check for, and add if missing, a unique Change-Id
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index 1d70d4efb6..3a8f301266 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -19,6 +19,7 @@ import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.workflow.StagingFunction;
import com.google.gerrit.testutil.InMemoryDatabase;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.jdbc.JdbcSchema;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 466d454da3..2c20b361ae 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -34,5 +34,9 @@ public class MasterCommandModule extends CommandModule {
command(gerrit, "replicate").to(AdminReplicate.class);
command(gerrit, "set-project-parent").to(AdminSetParent.class);
command(gerrit, "review").to(ReviewCommand.class);
+ command(gerrit, "staging-new-build").to(StagingNewBuild.class);
+ command(gerrit, "staging-ls").to(StagingListChanges.class);
+ command(gerrit, "staging-approve").to(StagingApprove.class);
+ command(gerrit, "staging-rebuild").to(StagingRebuild.class);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index a8561a535b..c28efd00b1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -23,12 +23,15 @@ import com.google.gerrit.reviewdb.Branch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.git.StagingUtil;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.patch.PublishComments;
@@ -36,6 +39,7 @@ import com.google.gerrit.server.project.CanSubmitResult;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gerrit.sshd.BaseCommand;
@@ -45,6 +49,8 @@ import com.google.gwtorm.client.ResultSet;
import com.google.inject.Inject;
import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -99,6 +105,9 @@ public class ReviewCommand extends BaseCommand {
@Option(name = "--submit", aliases = "-s", usage = "submit the patch set")
private boolean submitChange;
+ @Option(name = "--staging", aliases ="t", usage = "merge patch set to staging")
+ private boolean staging;
+
@Inject
private ReviewDb db;
@@ -127,12 +136,24 @@ public class ReviewCommand extends BaseCommand {
private PublishComments.Factory publishCommentsFactory;
@Inject
+ private MergeQueue stagingQueue;
+
+ @Inject
private ChangeHookRunner hooks;
+ @Inject
+ private GitRepositoryManager gitManager;
+
private List<ApproveOption> optionList;
+ private Repository git;
+
private Set<PatchSet.Id> toSubmit = new HashSet<PatchSet.Id>();
+ private Set<PatchSet.Id> toStaging = new HashSet<PatchSet.Id>();
+
+ private Project.NameKey currentProject;
+
@Override
public final void start(final Environment env) {
startThread(new CommandRunnable() {
@@ -199,13 +220,62 @@ public class ReviewCommand extends BaseCommand {
throw new Failure(1, "one or more submits failed", updateError);
}
}
+
+ if (!toStaging.isEmpty()) {
+ final Set<Branch.NameKey> toMerge = new HashSet<Branch.NameKey>();
+ try {
+ for (PatchSet.Id patchSetId : toStaging) {
+ final Change change = db.changes().get(patchSetId.getParentKey());
+ openRepository(change.getProject());
+ ChangeUtil.moveToStaging(opFactory, patchSetId, currentUser, db,
+ new MergeQueue() {
+ @Override
+ public void schedule(Branch.NameKey branch) {
+ toMerge.add(branch);
+ }
+
+ @Override
+ public void recheckAfter(Branch.NameKey branch, long delay,
+ TimeUnit delayUnit) {
+ toMerge.add(branch);
+ }
+
+ @Override
+ public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
+ toMerge.add(branch);
+ }
+ }, git, hooks);
+ }
+
+ for (Branch.NameKey stagingBranch : toMerge) {
+ Branch.NameKey branch =
+ StagingUtil.getSourceBranch(stagingBranch);
+ if (!StagingUtil.branchExists(git, stagingBranch)) {
+ StagingUtil.createStagingBranch(git, branch);
+ }
+
+ stagingQueue.merge(opFactory, stagingBranch);
+ }
+ } catch (OrmException e) {
+ throw new Failure(1, "one or more staging merges failed", e);
+ } catch (IOException e) {
+ throw new Failure(1, "Failed to access git repository", e);
+ } catch (NoSuchRefException e) {
+ throw new Failure(1, "Invalid destination branch", e);
+ } finally {
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
}
});
}
private void approveOne(final PatchSet.Id patchSetId) throws
- NoSuchChangeException, UnloggedFailure, OrmException, EmailException {
-
+ NoSuchChangeException, UnloggedFailure, OrmException,
+ NoSuchRefException, IOException, EmailException, Failure,
+ InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
ChangeControl changeControl = changeControlFactory.validateFor(changeId);
@@ -258,6 +328,14 @@ public class ReviewCommand extends BaseCommand {
} else {
throw error(result.getMessage());
}
+ } else if (staging) {
+ CanSubmitResult result = changeControl.canMergeToStaging(patchSetId,
+ db, approvalTypes, functionStateFactory);
+ if (result == CanSubmitResult.OK) {
+ toStaging.add(patchSetId);
+ } else {
+ throw error(result.getMessage());
+ }
}
}
@@ -369,4 +447,20 @@ public class ReviewCommand extends BaseCommand {
private static UnloggedFailure error(final String msg) {
return new UnloggedFailure(1, msg);
}
+
+ private void openRepository(final Project.NameKey project) throws RepositoryNotFoundException {
+ try {
+ if (git == null) {
+ // Open git repository, for the first time.
+ git = gitManager.openRepository(project);
+ } else if (!currentProject.equals(project)) {
+ // Another repository is already open. Close current repository
+ // and open a new one.
+ git.close();
+ git = gitManager.openRepository(project);
+ }
+ } finally {
+ currentProject = project;
+ }
+ }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingApprove.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingApprove.java
new file mode 100644
index 0000000000..454b4bf948
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingApprove.java
@@ -0,0 +1,407 @@
+// Copyright (C) 2011 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.sshd.commands;
+
+import static com.google.gerrit.sshd.commands.StagingCommand.R_BUILDS;
+
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.patch.PublishComments;
+import com.google.gerrit.server.project.CanSubmitResult;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.workflow.FunctionState;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.commands.StagingCommand.BranchNotFoundException;
+import com.google.gwtorm.client.AtomicUpdate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A command to report pass or fail status for builds. When a build receives
+ * pass status, the branch is updated with build ref and all open changes in
+ * the build are marked as merged. When a build receives fail status, all
+ * change in the build are marked as new and they need to be staged again.
+ * <p>
+ * For example, how to approve a build
+ * $ ssh -p 29418 localhost gerrit staging-approve -p project -b master -i 123 -r=pass
+ */
+public class StagingApprove extends BaseCommand {
+
+ private class MergeException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public MergeException(String message) {
+ super(message);
+ }
+
+ public MergeException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+ }
+
+ /** Parameter value for pass result. */
+ private static final String PASS = "pass";
+ /** Parameter value for fail result. */
+ private static final String FAIL = "fail";
+ /** Parameter value for stdin message. */
+ private static final String STDIN_MESSAGE = "-";
+
+ @Inject
+ private GitRepositoryManager gitManager;
+
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ private PublishComments.Factory publishCommentsFactory;
+
+ @Inject
+ private ChangeControl.Factory changeControlFactory;
+
+ @Inject
+ private ApprovalTypes approvalTypes;
+
+ @Inject
+ private FunctionState.Factory functionStateFactory;
+
+ @Inject
+ private IdentifiedUser currentUser;
+
+ @Inject
+ private ChangeHookRunner hooks;
+
+ @Inject
+ private MergeQueue merger;
+
+ @Inject
+ private MergeOp.Factory opFactory;
+
+ @Option(name = "--project", aliases = {"-p"},
+ required = true, usage = "project name")
+ private String project;
+
+ @Option(name = "--build-id", aliases = {"-i"},
+ required = true, usage = "build branch containing changes, e.g. refs/builds/123 or 123")
+ private String buildBranch;
+
+ @Option(name = "--result", aliases = {"-r"},
+ required = true, usage = "pass or fail")
+ private String result;
+
+ @Option(name = "--message", aliases = {"-m"}, metaVar ="-|MESSAGE",
+ usage = "message added to all changes")
+ private String message;
+
+ private Repository git;
+
+ private List<PatchSet> toApprove;
+
+ private Branch.NameKey destination;
+
+ @Override
+ public void start(final Environment env) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ StagingApprove.this.approve();
+ }
+ });
+ }
+
+ private void approve() throws UnloggedFailure {
+ // Check result parameter value.
+ final boolean passed;
+ if (result.toLowerCase().equals(PASS)) {
+ passed = true;
+ } else if (result.toLowerCase().equals(FAIL)) {
+ passed = false;
+ } else {
+ // A valid result parameter value was not used.
+ throw new UnloggedFailure(1,
+ "fatal: result argument accepts only value pass or fail.");
+ }
+
+ final PrintWriter stdout = toPrintWriter(out);
+
+ // Name key for the build branch.
+ Branch.NameKey buildBranchNameKey =
+ StagingCommand.getNameKey(project, R_BUILDS, buildBranch);
+
+ try {
+ openRepository(project);
+
+ // Initialize and populate open changes list.
+ toApprove = StagingCommand.openChanges(git, db, buildBranchNameKey.get());
+
+ // Notify user that build did not have any open changes. The build has
+ // already been approved.
+ if (toApprove.isEmpty()) {
+ throw new UnloggedFailure(1, "No open changes in the build branch");
+ }
+
+ // Validate change status and destination branch.
+ validateChanges();
+
+ // If result is passed, check that the user has required access rights
+ // to submit changes.
+ if (passed) {
+ validateSubmitRights();
+ try {
+ updateDestinationBranch(buildBranchNameKey);
+ } catch (IOException e) {
+ resetChangeStatus();
+ throw e;
+ } catch (MergeException e) {
+ resetChangeStatus();
+ throw e;
+ }
+
+ // Rebuild staging branch.
+ ChangeUtil.rebuildStaging(destination, currentUser, db, git, opFactory,
+ merger, hooks);
+ }
+
+ // Use current message or read it from stdin.
+ prepareMessage();
+
+ // Iterate through each open change and publish message.
+ for (PatchSet patchSet : toApprove) {
+ final PatchSet.Id patchSetId = patchSet.getId();
+ publishMessage(patchSetId);
+
+ if (passed) {
+ // Set change status to merged.
+ pass(patchSetId);
+ } else {
+ // Reset change status.
+ reject(patchSetId);
+ }
+ }
+
+ } catch (IOException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to update destination branch", e);
+ } catch (OrmException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to access database", e);
+ } catch (NoSuchChangeException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to validate access rights", e);
+ } catch (BranchNotFoundException e) {
+ throw new UnloggedFailure(1, "fatal: " + e.getMessage(), e);
+ } catch (NoSuchRefException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to access change destination branch", e);
+ } catch (MergeException e) {
+ throw new UnloggedFailure(1, "fatal: " + e.getMessage(), e);
+ } catch (InvalidChangeOperationException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to publish comments", e);
+ } finally {
+ stdout.flush();
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+
+ private void validateChanges() throws OrmException, UnloggedFailure {
+ for (PatchSet patchSet : toApprove) {
+ Change change = db.changes().get(patchSet.getId().getParentKey());
+
+ // All changes must originate from the same destination branch.
+ if (destination == null) {
+ destination = change.getDest();
+ } else if (!destination.get().equals(change.getDest().get())) {
+ throw new UnloggedFailure(1,
+ "All changes in build must belong to same destination branch."
+ + " (" + destination + " != " + change.getDest() + ")");
+ }
+
+ // All changes must be in state INTEGRATING.
+ if (change.getStatus() != Change.Status.INTEGRATING) {
+ throw new UnloggedFailure(1,
+ "Change not in INTEGRATING state (" + change.getKey() + ")");
+ }
+ }
+ }
+
+ private void openRepository(final String project) throws RepositoryNotFoundException {
+ Project.NameKey projectNameKey = new Project.NameKey(project);
+ git = gitManager.openRepository(projectNameKey);
+ }
+
+ private void validateSubmitRights() throws UnloggedFailure,
+ NoSuchChangeException, OrmException {
+ for (PatchSet patchSet : toApprove) {
+ final Change.Id changeId = patchSet.getId().getParentKey();
+ final ChangeControl changeControl =
+ changeControlFactory.validateFor(changeId);
+
+ CanSubmitResult result =
+ changeControl.canSubmit(patchSet.getId(), db, approvalTypes, functionStateFactory);
+
+ if (result != CanSubmitResult.OK) {
+ throw new UnloggedFailure(1, result.getMessage());
+ }
+ }
+ }
+
+ private void publishMessage(final PatchSet.Id patchSetId)
+ throws NoSuchChangeException, OrmException, NoSuchRefException,
+ IOException, InvalidChangeOperationException {
+ if (message != null && message.length() > 0) {
+ publishCommentsFactory.create(patchSetId, message,
+ new HashSet<ApprovalCategoryValue.Id>()).call();
+ }
+ }
+
+ private void updateDestinationBranch(final Branch.NameKey buildBranchKey)
+ throws IOException, MergeException {
+ RevWalk rw = null;
+ try {
+ // Setup RevWalk.
+ rw = new RevWalk(git);
+ rw.sort(RevSort.TOPO);
+ rw.sort(RevSort.COMMIT_TIME_DESC, true);
+
+ // Prepare branch update. Set destination branch tip as old object id.
+ RefUpdate branchUpdate = git.updateRef(destination.get());
+ // Access tip of build branch.
+ Ref buildRef = git.getRef(buildBranchKey.get());
+
+ // Access commits from destination and build branches.
+ RevCommit branchTip = rw.parseCommit(branchUpdate.getOldObjectId());
+ RevCommit buildTip = rw.parseCommit(buildRef.getObjectId());
+
+ // Setup branch update.
+ branchUpdate.setForceUpdate(false);
+
+ // We are updating old destination branch tip to build branch tip.
+ branchUpdate.setNewObjectId(buildTip);
+
+ // Make sure that the build tip is reachable from the branch tip.
+ if (!rw.isMergedInto(branchTip, buildTip)) {
+ throw new MergeException(destination.get() + " is not reachable from "
+ + buildBranchKey.get());
+ }
+
+ // Update destination branch.
+ switch (branchUpdate.update(rw)) {
+ // Only fast-forward result is reported as success.
+ case FAST_FORWARD:
+ hooks.doRefUpdatedHook(destination, branchUpdate,
+ currentUser.getAccount());
+ break;
+ default:
+ throw new MergeException("Could not fast-forward build to destination branch");
+ }
+ } finally {
+ if (rw != null) {
+ rw.dispose();
+ }
+ }
+ }
+
+ private void pass(final PatchSet.Id patchSetId) throws OrmException {
+ // Update change status from INTEGRATING to MERGED.
+ ChangeUtil.setIntegratingToMerged(patchSetId, currentUser, db);
+ }
+
+ private void reject(final PatchSet.Id patchSetId) throws OrmException,
+ IOException {
+ // Remove staging approval and update status from INTEGRATING to NEW.
+ ChangeUtil.rejectStagedChange(patchSetId, currentUser, db);
+ }
+
+ private void prepareMessage() throws IOException {
+ // No message given.
+ if (message == null) {
+ return;
+ }
+
+ // User will submit message through stdin.
+ if (message.equals(STDIN_MESSAGE)) {
+ // Clear stdin indicator.
+ message = "";
+
+ // Read message from stdin.
+ BufferedReader stdin
+ = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+ String line;
+ while ((line = stdin.readLine()) != null) {
+ message += line + "\n";
+ }
+ } // Else, use current message value.
+ }
+
+ private void resetChangeStatus() throws MergeException {
+ // Return changes to staging branch.
+ for (PatchSet patchSet : toApprove) {
+ try {
+ // Reset status of changes.
+ final PatchSet.Id patchSetId = patchSet.getId();
+ final Change.Id changeId = patchSetId.getParentKey();
+ AtomicUpdate<Change> atomicUpdate =
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus() == Change.Status.INTEGRATING) {
+ change.setStatus(Change.Status.STAGING);
+ ChangeUtil.updated(change);
+ }
+ return change;
+ }
+ };
+ db.changes().atomicUpdate(changeId, atomicUpdate);
+ } catch (Exception e) {
+ // Failed to reset change status.
+ }
+ }
+
+ try {
+ ChangeUtil.rebuildStaging(destination, currentUser, db, git, opFactory,
+ merger, hooks);
+ } catch (Exception e) {
+ throw new MergeException("fatal: Failed to rebuild staging branch after failed fast-forward", e);
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingCommand.java
new file mode 100644
index 0000000000..4ff0da3800
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingCommand.java
@@ -0,0 +1,166 @@
+// Copyright (C) 2011 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.sshd.commands;
+
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetAccess;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.RevId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Constants and utility methods for staging commands.
+ *
+ */
+public class StagingCommand {
+ public static class BranchNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+ public BranchNotFoundException(final String message) {
+ super(message);
+ }
+ }
+
+ public static class UpdateRefException extends Exception {
+ private static final long serialVersionUID = 1L;
+ public UpdateRefException(final String message) {
+ super(message);
+ }
+ }
+
+ /** Prefix for head refs. */
+ public static final String R_HEADS = "refs/heads/";
+ /** Prefix for build refs. */
+ public static final String R_BUILDS = "refs/builds/";
+ /** Prefix for staging refs. */
+ public static final String R_STAGING = "refs/staging/";
+
+ /** Private constructor. This class should not be instantiated. */
+ private StagingCommand() {
+
+ }
+
+ /**
+ * Creates a branch key including ref prefix.
+ * @param project Project for the branch key.
+ * @param prefix Expected prefix.
+ * @param branch Branch name with or without prefix.
+ * @return Branch name key with prefix.
+ */
+ public static Branch.NameKey getNameKey(final String project,
+ final String prefix, final String branch) {
+ final Project.NameKey projectKey = new Project.NameKey(project);
+ if (branch.startsWith(prefix)) {
+ return new Branch.NameKey(projectKey, branch);
+ } else {
+ return new Branch.NameKey(projectKey, prefix + branch);
+ }
+ }
+
+ /**
+ * Creates a branch key without any prefix.
+ * @param project Project for the branch key.
+ * @param prefix Prefix to remove.
+ * @param branch Branch name with or without prefix.
+ * @return Branch name key without prefix.
+ */
+ public static Branch.NameKey getShortNameKey(final String project,
+ final String prefix, final String branch) {
+ final Project.NameKey projectKey = new Project.NameKey(project);
+ if (branch.startsWith(prefix)) {
+ return new Branch.NameKey(projectKey, branch.substring(prefix.length()));
+ } else {
+ return new Branch.NameKey(projectKey, branch);
+ }
+ }
+
+ /**
+ * Lists open changes in a branch.
+ * @param git jGit Repository. Must be open.
+ * @param db ReviewDb of a Gerrit site.
+ * @param branch Branch to search for open changes-
+ * @return List of open changes.
+ * @throws IOException Thrown by Repository or RevWalk if repository is not
+ * accessible.
+ * @throws OrmException Thrown if ReviewDb is not accessible.
+ */
+ public static List<PatchSet> openChanges(Repository git, ReviewDb db,
+ final String branch) throws IOException, OrmException,
+ BranchNotFoundException {
+ List<PatchSet> open = new ArrayList<PatchSet>();
+ PatchSetAccess patchSetAccess = db.patchSets();
+
+ RevWalk revWalk = new RevWalk(git);
+
+ try {
+ Ref ref = git.getRef(branch);
+ if (ref == null) {
+ throw new BranchNotFoundException("No such branch: " + branch);
+ }
+ RevCommit firstCommit = revWalk.parseCommit(ref.getObjectId());
+ revWalk.markStart(firstCommit);
+ Iterator<RevCommit> i = revWalk.iterator();
+
+ final String changeIdFooter = "Change-Id";
+ FooterKey changeIdKey = new FooterKey(changeIdFooter);
+ while (i.hasNext()) {
+ RevCommit commit = i.next();
+ RevId revId = new RevId(ObjectId.toString(commit));
+ List<String> changeIds = commit.getFooterLines(changeIdKey);
+
+ if (changeIds.isEmpty()) {
+ // No Change-Id footer available. Search by patch set revision.
+ List<PatchSet> patchSets = patchSetAccess.byRevision(revId).toList();
+ for (PatchSet patchSet : patchSets) {
+ Change.Id changeId = patchSet.getId().getParentKey();
+ Change change = db.changes().get(changeId);
+ if (change.getStatus().isOpen()) {
+ open.add(patchSet);
+ break;
+ }
+ }
+ } else {
+ // Change-Id footer found in commit message. Search by Change-Id
+ // value. Usually, there is only 1 Change-Id footer.
+ for (String changeId : changeIds) {
+ List<Change> changes =
+ db.changes().byKey(Change.Key.parse(changeId)).toList();
+ for (Change change : changes) {
+ if (change.getStatus().isOpen()) {
+ open.add(patchSetAccess.get(change.currentPatchSetId()));
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ revWalk.dispose();
+ }
+ return open;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingListChanges.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingListChanges.java
new file mode 100644
index 0000000000..c37a096f43
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingListChanges.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2011 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.sshd.commands;
+
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.commands.StagingCommand.BranchNotFoundException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+public class StagingListChanges extends BaseCommand {
+
+ @Inject
+ private GitRepositoryManager gitManager;
+
+ @Inject
+ private ReviewDb db;
+
+ private Repository git;
+
+ @Option(name = "--project", aliases = {"-p"},
+ required = true, usage = "project name")
+ private String project;
+
+ @Option(name = "--branch", aliases = {"-b"},
+ required = true, usage = "branch name, e.g. refs/builds/1")
+ private String branch;
+
+ @Override
+ public void start(final Environment env) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ StagingListChanges.this.list();
+ }
+ });
+ }
+
+ private void list() throws UnloggedFailure {
+ final PrintWriter stdout = toPrintWriter(out);
+ try {
+ openRepository(project);
+
+ List<PatchSet> open = StagingCommand.openChanges(git, db, branch);
+
+ for (PatchSet patchSet : open) {
+ Change.Id changeId = patchSet.getId().getParentKey();
+ Change change = db.changes().get(changeId);
+ if (change.getStatus().isOpen()) {
+ stdout.println(patchSet.getRevision().get() + " " + patchSet.getId() + " " + change.getSubject());
+ }
+ }
+ } catch (IOException e) {
+ throw new UnloggedFailure(1, "Fatal: cannot access repository", e);
+ } catch (OrmException e) {
+ throw new UnloggedFailure(1, "Fatal: cannot access Gerrit database", e);
+ } catch (BranchNotFoundException e) {
+ throw new UnloggedFailure(1, "fatal: " + e.getMessage(), e);
+ } finally {
+ stdout.flush();
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+
+ public void openRepository(final String project) throws RepositoryNotFoundException {
+ Project.NameKey projectNameKey = new Project.NameKey(project);
+ git = gitManager.openRepository(projectNameKey);
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingNewBuild.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingNewBuild.java
new file mode 100644
index 0000000000..afeb5528ee
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingNewBuild.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2011 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.sshd.commands;
+
+import static com.google.gerrit.sshd.commands.StagingCommand.R_BUILDS;
+import static com.google.gerrit.sshd.commands.StagingCommand.R_STAGING;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.StagingUtil;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.commands.StagingCommand.BranchNotFoundException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+public class StagingNewBuild extends BaseCommand {
+
+ @Inject
+ private GitRepositoryManager gitManager;
+
+ @Inject
+ private ReviewDb db;
+
+ private Repository git;
+
+ @Option(name = "--project", aliases = {"-p"},
+ required = true, usage = "project name")
+ private String project;
+
+ @Option(name = "--staging-branch", aliases = {"-s"},
+ required = true, usage = "branch name, e.g. refs/staging/master")
+ private String stagingBranch;
+
+ @Option(name = "--build-id", aliases = {"-i"},
+ required = true, usage = "build id, e.g. 123 (refs/build/123)")
+ private String build;
+
+ @Override
+ public void start(final Environment env) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ StagingNewBuild.this.rename();
+ }
+ });
+ }
+
+ private void rename() throws UnloggedFailure {
+ final PrintWriter stdout = toPrintWriter(out);
+
+ try {
+ openRepository();
+
+ Branch.NameKey buildBranchKey =
+ StagingCommand.getNameKey(project, R_BUILDS, build);
+ if (git.getRef(buildBranchKey.get()) != null) {
+ throw new UnloggedFailure(1, "fatal: Target build already exists!");
+ }
+
+ Branch.NameKey stagingBranchKey =
+ StagingCommand.getNameKey(project, R_STAGING, stagingBranch);
+
+ // Make sure that are changes in the staging branch.
+ if (StagingCommand.openChanges(git, db, stagingBranchKey.get()).isEmpty()) {
+ stdout.println("No changes in staging branch. Not creating a build reference");
+ return;
+ }
+
+ // Create build reference.
+ Result result =
+ StagingUtil.createBuildRef(git, stagingBranchKey, buildBranchKey);
+
+ if (result != Result.NEW && result != Result.FAST_FORWARD) {
+ throw new UnloggedFailure(1, "fatal: failed to create new build ref: " + result);
+ } else {
+ updateChangeStatus(buildBranchKey);
+ }
+
+ // Re-create staging branch.
+ Branch.NameKey branchNameKey =
+ StagingCommand.getShortNameKey(project, R_STAGING, stagingBranch);
+ result = StagingUtil.createStagingBranch(git, branchNameKey);
+ if (result != Result.NEW && result != Result.FAST_FORWARD
+ && result != Result.FORCED && result != Result.NO_CHANGE) {
+ throw new UnloggedFailure(1, "fatal: failed to reset staging branch: " + result);
+ }
+ } catch (IOException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to access repository", e);
+ } catch (OrmException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to access database", e);
+ } catch (BranchNotFoundException e) {
+ throw new UnloggedFailure(1, "fatal: Failed to access build ref", e);
+ } catch (NoSuchRefException e) {
+ throw new UnloggedFailure(1, "fatal: Invalid branch name", e);
+ } finally {
+ stdout.flush();
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+
+ private void openRepository() throws RepositoryNotFoundException {
+ Project.NameKey projectKey = new Project.NameKey(project);
+ git = gitManager.openRepository(projectKey);
+ }
+
+ private void updateChangeStatus(final Branch.NameKey buildBranchKey)
+ throws IOException, OrmException, BranchNotFoundException {
+ List<PatchSet> patchSets =
+ StagingCommand.openChanges(git, db, buildBranchKey.get());
+ for (PatchSet patchSet : patchSets) {
+ ChangeUtil.setIntegrating(patchSet.getId(), db);
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingRebuild.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingRebuild.java
new file mode 100644
index 0000000000..af252cdb77
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StagingRebuild.java
@@ -0,0 +1,89 @@
+package com.google.gerrit.sshd.commands;
+
+import static com.google.gerrit.sshd.commands.StagingCommand.R_HEADS;
+
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public class StagingRebuild extends BaseCommand {
+
+ @Inject
+ private GitRepositoryManager gitManager;
+
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ private IdentifiedUser currentUser;
+
+ @Inject
+ private MergeQueue merger;
+
+ @Inject
+ private MergeOp.Factory opFactory;
+
+ @Inject
+ private ChangeHookRunner hooks;
+
+ private Repository git;
+
+ @Option(name = "--project", aliases = {"-p"},
+ required = true, usage = "project name")
+ private String project;
+
+ @Option(name = "--branch", aliases = {"-b"},
+ required = true, usage = "branch name, e.g. refs/builds/1")
+ private String branch;
+
+ @Override
+ public void start(final Environment env) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ StagingRebuild.this.rebuild();
+ }
+ });
+ }
+
+ private void rebuild() throws UnloggedFailure {
+ final PrintWriter stdout = toPrintWriter(out);
+ try {
+ final Branch.NameKey branchNameKey =
+ StagingCommand.getNameKey(project, R_HEADS, branch);
+
+ git = gitManager.openRepository(branchNameKey.getParentKey());
+ ChangeUtil.rebuildStaging(branchNameKey, currentUser, db, git, opFactory,
+ merger, hooks);
+ } catch (NoSuchRefException e) {
+ throw new UnloggedFailure(1, "Fatal: branch does not exist", e);
+ } catch (OrmException e) {
+ throw new UnloggedFailure(1, "Fatal: failed to access database", e);
+ } catch (IOException e) {
+ throw new UnloggedFailure(1, "Fatal: failed to access repository", e);
+ } finally {
+ stdout.flush();
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+}