diff options
author | Mika Hamalainen <mika.hamalainen@accenture.com> | 2011-03-28 09:44:02 +0300 |
---|---|---|
committer | Mika Hamalainen <mika.hamalainen@accenture.com> | 2011-08-03 14:59:38 +0300 |
commit | 5914eaae280e9de548f6c13c89c0b9e6b20cf90a (patch) | |
tree | b652d55e45fd85a36e51570ccaa1fa3766d1bc48 | |
parent | 9e7eb6c453f05425562a40b7f97dc8db7f8e38d5 (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
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 = ⇦Prev pagedChangeListNext = Next⇨ 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 Binary files differindex 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 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 Binary files differindex 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 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(); + } + } + } +} |