1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Releeph (Phabricator part)

Summary: A copy of the Releeph release tool.

Test Plan: Generally, click everything at least once.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Korvin, AnhNhan

Maniphest Tasks: T2094

Differential Revision: https://secure.phabricator.com/D4932
This commit is contained in:
Edward Speyer 2013-03-15 11:28:43 +00:00
parent daed35e36c
commit 2497e5b5ed
100 changed files with 9774 additions and 4 deletions

View file

@ -0,0 +1,92 @@
CREATE TABLE {$NAMESPACE}_releeph.`releeph_project` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dateCreated` int(10) unsigned NOT NULL,
`dateModified` int(10) unsigned NOT NULL,
`phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`name` varchar(255) NOT NULL,
`trunkBranch` varchar(255) NOT NULL,
`repositoryID` int(10) unsigned NOT NULL,
`repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`arcanistProjectID` int(10) unsigned NOT NULL,
`createdByUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`isActive` tinyint(1) NOT NULL DEFAULT '1',
`projectID` int(10) unsigned DEFAULT NULL,
`details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `projectName` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE {$NAMESPACE}_releeph.`releeph_branch` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dateCreated` int(10) unsigned NOT NULL,
`dateModified` int(10) unsigned NOT NULL,
`basename` varchar(64) NOT NULL,
`releephProjectID` int(10) unsigned NOT NULL,
`createdByUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`cutPointCommitIdentifier`
varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`cutPointCommitPHID`
varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`isActive` tinyint(1) NOT NULL DEFAULT '1',
`symbolicName` varchar(64) DEFAULT NULL,
`details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`name` varchar(128) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `releephProjectID_2` (`releephProjectID`,`basename`),
UNIQUE KEY `releephProjectID_name` (`releephProjectID`,`name`),
UNIQUE KEY `releephProjectID` (`releephProjectID`,`symbolicName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE {$NAMESPACE}_releeph.`releeph_request` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dateCreated` int(10) unsigned NOT NULL,
`dateModified` int(10) unsigned NOT NULL,
`phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`branchID` int(10) unsigned NOT NULL,
`summary` longtext CHARACTER SET utf8 COLLATE utf8_bin,
`requestUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`requestCommitIdentifier`
varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`requestCommitPHID`
varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`requestCommitOrdinal` int(10) unsigned NOT NULL,
`commitIdentifier`
varchar(40) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`committedByUserPHID`
varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`commitPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`pickStatus` tinyint(4) DEFAULT NULL,
`details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`userIntents` longtext CHARACTER SET utf8 COLLATE utf8_bin,
`inBranch` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `phid` (`phid`),
UNIQUE KEY `requestIdentifierBranch` (`requestCommitIdentifier`,`branchID`),
KEY `branchID` (`branchID`,`requestCommitOrdinal`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE {$NAMESPACE}_releeph.`releeph_requestevent` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dateCreated` int(10) unsigned NOT NULL,
`dateModified` int(10) unsigned NOT NULL,
`releephRequestID` int(10) unsigned NOT NULL,
`actorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`type` varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE {$NAMESPACE}_releeph.`releeph_event` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dateCreated` int(10) unsigned NOT NULL,
`dateModified` int(10) unsigned NOT NULL,
`releephProjectID` int(10) unsigned NOT NULL,
`releephBranchID` int(10) unsigned DEFAULT NULL,
`type` varchar(32) NOT NULL,
`epoch` int(10) unsigned DEFAULT NULL,
`actorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View file

@ -1977,7 +1977,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-pholio-mock-view' =>
array(
'uri' => '/res/eefc43b3/rsrc/js/application/pholio/behavior-pholio-mock-view.js',
'uri' => '/res/ecf5f969/rsrc/js/application/pholio/behavior-pholio-mock-view.js',
'type' => 'js',
'requires' =>
array(
@ -2062,6 +2062,54 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/behavior-refresh-csrf.js',
),
'javelin-behavior-releeph-preview-branch' =>
array(
'uri' => '/res/a77ebc86/rsrc/js/application/releeph/releeph-preview-branch.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-uri',
4 => 'javelin-util',
),
'disk' => '/rsrc/js/application/releeph/releeph-preview-branch.js',
),
'javelin-behavior-releeph-request-state-change' =>
array(
'uri' => '/res/38f96ba8/rsrc/js/application/releeph/releeph-request-state-change.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-util',
4 => 'phabricator-keyboard-shortcut',
5 => 'phabricator-notification',
),
'disk' => '/rsrc/js/application/releeph/releeph-request-state-change.js',
),
'javelin-behavior-releeph-request-typeahead' =>
array(
'uri' => '/res/b52096e2/rsrc/js/application/releeph/releeph-request-typeahead.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-typeahead',
4 => 'javelin-tokenizer',
5 => 'javelin-typeahead-preloaded-source',
6 => 'javelin-typeahead-ondemand-source',
7 => 'javelin-dom',
8 => 'javelin-stratcom',
9 => 'javelin-util',
),
'disk' => '/rsrc/js/application/releeph/releeph-request-typeahead.js',
),
'javelin-behavior-repository-crossreference' =>
array(
'uri' => '/res/4b5fab1c/rsrc/js/application/repository/repository-crossreference.js',
@ -2637,7 +2685,7 @@ celerity_register_resource_map(array(
),
'paste-css' =>
array(
'uri' => '/res/5081cf13/rsrc/css/application/paste/paste.css',
'uri' => '/res/044639be/rsrc/css/application/paste/paste.css',
'type' => 'css',
'requires' =>
array(
@ -3099,7 +3147,7 @@ celerity_register_resource_map(array(
),
'phabricator-source-code-view-css' =>
array(
'uri' => '/res/9373e769/rsrc/css/layout/phabricator-source-code-view.css',
'uri' => '/res/979d5280/rsrc/css/layout/phabricator-source-code-view.css',
'type' => 'css',
'requires' =>
array(
@ -3336,7 +3384,7 @@ celerity_register_resource_map(array(
),
'pholio-css' =>
array(
'uri' => '/res/bc10bf21/rsrc/css/application/pholio/pholio.css',
'uri' => '/res/b0947e46/rsrc/css/application/pholio/pholio.css',
'type' => 'css',
'requires' =>
array(
@ -3433,6 +3481,87 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/raphael/g.raphael.line.js',
),
'releeph-branch' =>
array(
'uri' => '/res/6ad6420d/rsrc/css/application/releeph/releeph-branch.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-branch.css',
),
'releeph-colors' =>
array(
'uri' => '/res/dff4b26a/rsrc/css/application/releeph/releeph-colors.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-colors.css',
),
'releeph-core' =>
array(
'uri' => '/res/853f4a73/rsrc/css/application/releeph/releeph-core.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-core.css',
),
'releeph-intents' =>
array(
'uri' => '/res/4e73e9dd/rsrc/css/application/releeph/releeph-intents.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-intents.css',
),
'releeph-preview-branch' =>
array(
'uri' => '/res/65e5dece/rsrc/css/application/releeph/releeph-preview-branch.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-preview-branch.css',
),
'releeph-project' =>
array(
'uri' => '/res/b9376e59/rsrc/css/application/releeph/releeph-project.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-project.css',
),
'releeph-request-differential-create-dialog' =>
array(
'uri' => '/res/4df30ce1/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css',
),
'releeph-request-typeahead-css' =>
array(
'uri' => '/res/9c9a1acf/rsrc/css/application/releeph/releeph-request-typeahead.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-request-typeahead.css',
),
'releeph-status' =>
array(
'uri' => '/res/588529df/rsrc/css/application/releeph/releeph-status.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/releeph/releeph-status.css',
),
'setup-issue-css' =>
array(
'uri' => '/res/efbb3673/rsrc/css/application/config/setup-issue.css',

View file

@ -187,6 +187,19 @@ phutil_register_library_map(array(
'ConduitAPI_phriction_info_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_info_Method.php',
'ConduitAPI_project_Method' => 'applications/project/conduit/ConduitAPI_project_Method.php',
'ConduitAPI_project_query_Method' => 'applications/project/conduit/ConduitAPI_project_query_Method.php',
'ConduitAPI_releeph_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_Method.php',
'ConduitAPI_releeph_getbranches_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_getbranches_Method.php',
'ConduitAPI_releeph_projectinfo_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_projectinfo_Method.php',
'ConduitAPI_releeph_request_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_request_Method.php',
'ConduitAPI_releephwork_canpush_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_canpush_Method.php',
'ConduitAPI_releephwork_getauthorinfo_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getauthorinfo_Method.php',
'ConduitAPI_releephwork_getbranch_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getbranch_Method.php',
'ConduitAPI_releephwork_getbranchcommitmessage_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getbranchcommitmessage_Method.php',
'ConduitAPI_releephwork_getcommitmessage_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getcommitmessage_Method.php',
'ConduitAPI_releephwork_getorigcommitmessage_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getorigcommitmessage_Method.php',
'ConduitAPI_releephwork_nextrequest_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_nextrequest_Method.php',
'ConduitAPI_releephwork_record_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_record_Method.php',
'ConduitAPI_releephwork_recordpickstatus_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_recordpickstatus_Method.php',
'ConduitAPI_remarkup_process_Method' => 'applications/remarkup/conduit/ConduitAPI_remarkup_process_Method.php',
'ConduitAPI_repository_Method' => 'applications/repository/conduit/ConduitAPI_repository_Method.php',
'ConduitAPI_repository_create_Method' => 'applications/repository/conduit/ConduitAPI_repository_create_Method.php',
@ -333,6 +346,7 @@ phutil_register_library_map(array(
'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php',
'DifferentialPeopleMenuEventListener' => 'applications/differential/events/DifferentialPeopleMenuEventListener.php',
'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php',
'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php',
'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php',
'DifferentialReplyHandler' => 'applications/differential/DifferentialReplyHandler.php',
'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php',
@ -690,6 +704,8 @@ phutil_register_library_map(array(
'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php',
'PhabricatorApplicationPonder' => 'applications/ponder/application/PhabricatorApplicationPonder.php',
'PhabricatorApplicationProject' => 'applications/project/application/PhabricatorApplicationProject.php',
'PhabricatorApplicationReleeph' => 'applications/releeph/application/PhabricatorApplicationReleeph.php',
'PhabricatorApplicationReleephConfigOptions' => 'applications/releeph/config/PhabricatorApplicationReleephConfigOptions.php',
'PhabricatorApplicationRepositories' => 'applications/repository/application/PhabricatorApplicationRepositories.php',
'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php',
'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php',
@ -1561,6 +1577,73 @@ phutil_register_library_map(array(
'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php',
'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php',
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
'ReleephActiveProjectListView' => 'applications/releeph/view/project/list/ReleephActiveProjectListView.php',
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
'ReleephBranchBoxView' => 'applications/releeph/view/branch/ReleephBranchBoxView.php',
'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php',
'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php',
'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php',
'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php',
'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php',
'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php',
'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php',
'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php',
'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php',
'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php',
'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php',
'ReleephController' => 'applications/releeph/controller/ReleephController.php',
'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php',
'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php',
'ReleephDefaultUserView' => 'applications/releeph/view/user/ReleephDefaultUserView.php',
'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php',
'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php',
'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php',
'ReleephDifferentialRevisionDetailRenderer' => 'applications/releeph/differential/ReleephDifferentialRevisionDetailRenderer.php',
'ReleephEvent' => 'applications/releeph/storage/event/ReleephEvent.php',
'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php',
'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php',
'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php',
'ReleephFieldSpecificationIncompleteException' => 'applications/releeph/field/exception/ReleephFieldSpecificationIncompleteException.php',
'ReleephInactiveProjectListView' => 'applications/releeph/view/project/list/ReleephInactiveProjectListView.php',
'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php',
'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php',
'ReleephObjectHandleLoader' => 'applications/releeph/ReleephObjectHandleLoader.php',
'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php',
'ReleephPHIDConstants' => 'applications/releeph/ReleephPHIDConstants.php',
'ReleephProject' => 'applications/releeph/storage/ReleephProject.php',
'ReleephProjectActionController' => 'applications/releeph/controller/project/ReleephProjectActionController.php',
'ReleephProjectCreateController' => 'applications/releeph/controller/project/ReleephProjectCreateController.php',
'ReleephProjectEditController' => 'applications/releeph/controller/project/ReleephProjectEditController.php',
'ReleephProjectListController' => 'applications/releeph/controller/project/ReleephProjectListController.php',
'ReleephProjectView' => 'applications/releeph/view/ReleephProjectView.php',
'ReleephProjectViewController' => 'applications/releeph/controller/project/ReleephProjectViewController.php',
'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php',
'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php',
'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php',
'ReleephRequestCreateController' => 'applications/releeph/controller/request/ReleephRequestCreateController.php',
'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php',
'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php',
'ReleephRequestEditor' => 'applications/releeph/editor/ReleephRequestEditor.php',
'ReleephRequestEvent' => 'applications/releeph/storage/request/ReleephRequestEvent.php',
'ReleephRequestEventListView' => 'applications/releeph/view/requestevent/ReleephRequestEventListView.php',
'ReleephRequestException' => 'applications/releeph/storage/request/exception/ReleephRequestException.php',
'ReleephRequestHeaderListView' => 'applications/releeph/view/request/header/ReleephRequestHeaderListView.php',
'ReleephRequestHeaderView' => 'applications/releeph/view/request/header/ReleephRequestHeaderView.php',
'ReleephRequestIntentsView' => 'applications/releeph/view/request/ReleephRequestIntentsView.php',
'ReleephRequestMail' => 'applications/releeph/editor/mail/ReleephRequestMail.php',
'ReleephRequestStatusView' => 'applications/releeph/view/request/ReleephRequestStatusView.php',
'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php',
'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php',
'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php',
'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php',
'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php',
'ReleephRiskFieldSpecification' => 'applications/releeph/field/specification/ReleephRiskFieldSpecification.php',
'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php',
'ReleephStatusFieldSpecification' => 'applications/releeph/field/specification/ReleephStatusFieldSpecification.php',
'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
'ReleephUserView' => 'applications/releeph/view/user/ReleephUserView.php',
),
'function' =>
array(
@ -1758,6 +1841,19 @@ phutil_register_library_map(array(
'ConduitAPI_phriction_info_Method' => 'ConduitAPI_phriction_Method',
'ConduitAPI_project_Method' => 'ConduitAPIMethod',
'ConduitAPI_project_query_Method' => 'ConduitAPI_project_Method',
'ConduitAPI_releeph_Method' => 'ConduitAPIMethod',
'ConduitAPI_releeph_getbranches_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releeph_projectinfo_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releeph_request_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_canpush_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_getauthorinfo_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_getbranch_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_getbranchcommitmessage_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_getcommitmessage_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_getorigcommitmessage_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_nextrequest_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_record_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_releephwork_recordpickstatus_Method' => 'ConduitAPI_releeph_Method',
'ConduitAPI_remarkup_process_Method' => 'ConduitAPIMethod',
'ConduitAPI_repository_Method' => 'ConduitAPIMethod',
'ConduitAPI_repository_create_Method' => 'ConduitAPI_repository_Method',
@ -1899,6 +1995,7 @@ phutil_register_library_map(array(
'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification',
'DifferentialPeopleMenuEventListener' => 'PhutilEventListener',
'DifferentialPrimaryPaneView' => 'AphrontView',
'DifferentialReleephRequestFieldSpecification' => 'DifferentialFieldSpecification',
'DifferentialRemarkupRule' => 'PhabricatorRemarkupRuleObject',
'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler',
'DifferentialResultsTableView' => 'AphrontView',
@ -2216,6 +2313,8 @@ phutil_register_library_map(array(
'PhabricatorApplicationPhriction' => 'PhabricatorApplication',
'PhabricatorApplicationPonder' => 'PhabricatorApplication',
'PhabricatorApplicationProject' => 'PhabricatorApplication',
'PhabricatorApplicationReleeph' => 'PhabricatorApplication',
'PhabricatorApplicationReleephConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorApplicationRepositories' => 'PhabricatorApplication',
'PhabricatorApplicationSettings' => 'PhabricatorApplication',
'PhabricatorApplicationSlowvote' => 'PhabricatorApplication',
@ -3107,5 +3206,65 @@ phutil_register_library_map(array(
'PonderVoteEditor' => 'PhabricatorEditor',
'PonderVoteSaveController' => 'PonderController',
'QueryFormattingTestCase' => 'PhabricatorTestCase',
'ReleephActiveProjectListView' => 'AphrontView',
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranch' => 'ReleephDAO',
'ReleephBranchAccessController' => 'ReleephController',
'ReleephBranchBoxView' => 'AphrontView',
'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranchCreateController' => 'ReleephController',
'ReleephBranchEditController' => 'ReleephController',
'ReleephBranchEditor' => 'PhabricatorEditor',
'ReleephBranchNamePreviewController' => 'PhabricatorController',
'ReleephBranchPreviewView' => 'AphrontFormControl',
'ReleephBranchViewController' => 'ReleephController',
'ReleephCommitFinderException' => 'Exception',
'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephController' => 'PhabricatorController',
'ReleephDAO' => 'PhabricatorLiskDAO',
'ReleephDefaultFieldSelector' => 'ReleephFieldSelector',
'ReleephDefaultUserView' => 'ReleephUserView',
'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification',
'ReleephEvent' => 'ReleephDAO',
'ReleephFieldParseException' => 'Exception',
'ReleephFieldSpecificationIncompleteException' => 'Exception',
'ReleephInactiveProjectListView' => 'AphrontView',
'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification',
'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification',
'ReleephObjectHandleLoader' => 'ObjectHandleLoader',
'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephProject' => 'ReleephDAO',
'ReleephProjectActionController' => 'ReleephController',
'ReleephProjectCreateController' => 'ReleephController',
'ReleephProjectEditController' => 'ReleephController',
'ReleephProjectListController' => 'PhabricatorController',
'ReleephProjectView' => 'AphrontView',
'ReleephProjectViewController' => 'ReleephController',
'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRequest' => 'ReleephDAO',
'ReleephRequestActionController' => 'ReleephController',
'ReleephRequestCreateController' => 'ReleephController',
'ReleephRequestDifferentialCreateController' => 'ReleephController',
'ReleephRequestEditController' => 'ReleephController',
'ReleephRequestEditor' => 'PhabricatorEditor',
'ReleephRequestEvent' => 'ReleephDAO',
'ReleephRequestEventListView' => 'AphrontView',
'ReleephRequestException' => 'Exception',
'ReleephRequestHeaderListView' => 'AphrontView',
'ReleephRequestHeaderView' => 'AphrontView',
'ReleephRequestIntentsView' => 'AphrontView',
'ReleephRequestStatusView' => 'AphrontView',
'ReleephRequestTypeaheadControl' => 'AphrontFormControl',
'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController',
'ReleephRequestViewController' => 'ReleephController',
'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRiskFieldSpecification' => 'ReleephFieldSpecification',
'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification',
'ReleephStatusFieldSpecification' => 'ReleephFieldSpecification',
'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
'ReleephUserView' => 'AphrontView',
),
));

View file

@ -0,0 +1,92 @@
<?php
final class ReleephObjectHandleLoader extends ObjectHandleLoader {
/**
* The intention for phid.external-loaders is for each new 4-char PHID type
* to point to a different external loader for that type.
*
* For brevity, we instead just have this one class that can load any type of
* Releeph PHID.
*/
public function loadHandles(array $phids) {
$types = array();
foreach ($phids as $phid) {
$type = phid_get_type($phid);
$types[$type][] = $phid;
}
$handles = array();
foreach ($types as $type => $phids) {
switch ($type) {
case ReleephPHIDConstants::PHID_TYPE_RERQ:
$object = new ReleephRequest();
$instances = $object->loadAllWhere('phid in (%Ls)', $phids);
$instances = mpull($instances, null, 'getPHID');
foreach ($phids as $phid) {
$instance = $instances[$phid];
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
$handle->setURI('/RQ'.$instance->getID());
$name = 'RQ'.$instance->getID();
$handle->setName($name);
$handle->setFullName($name.': '.$instance->getSummaryForDisplay());
$handle->setComplete(true);
$handles[$phid] = $handle;
}
break;
case ReleephPHIDConstants::PHID_TYPE_REBR:
$object = new ReleephBranch();
$branches = $object->loadAllWhere('phid IN (%Ls)', $phids);
$branches = mpull($branches, null, 'getPHID');
foreach ($phids as $phid) {
$branch = $branches[$phid];
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
$handle->setURI($branch->getURI());
$handle->setName($branch->getBasename());
$handle->setFullName($branch->getName());
$handle->setComplete(true);
$handles[$phid] = $handle;
}
break;
case ReleephPHIDConstants::PHID_TYPE_REPR:
$object = new ReleephProject();
$instances = $object->loadAllWhere('phid IN (%Ls)', $phids);
$instances = mpull($instances, null, 'getPHID');
foreach ($phids as $phid) {
$instance = $instances[$phid];
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
$handle->setURI($instance->getURI());
$handle->setName($instance->getName()); // no fullName for proejcts
$handle->setComplete(true);
$handles[$phid] = $handle;
}
break;
default:
throw new Exception('unknown type '.$type);
}
}
return $handles;
}
}

View file

@ -0,0 +1,9 @@
<?php
final class ReleephPHIDConstants {
// Releeph
const PHID_TYPE_REPR = 'REPR';
const PHID_TYPE_REBR = 'REBR';
const PHID_TYPE_RERQ = 'RERQ';
}

View file

@ -0,0 +1,86 @@
<?php
final class PhabricatorApplicationReleeph extends PhabricatorApplication {
public function getName() {
return 'Releeph';
}
public function getShortDescription() {
return 'Release Branches';
}
public function getBaseURI() {
return '/releeph/';
}
public function getAutospriteName() {
return 'releeph';
}
public function getApplicationGroup() {
return self::GROUP_ORGANIZATION;
}
public function isInstalled() {
if (PhabricatorEnv::getEnvConfig('releeph.installed')) {
return parent::isInstalled();
}
return false;
}
public function getRoutes() {
return array(
'/RQ(?P<requestID>[1-9]\d*)' => 'ReleephRequestViewController',
'/releeph/' => array(
'' => 'ReleephProjectListController',
'project/' => array(
'' => 'ReleephProjectListController',
'inactive/' => 'ReleephProjectListController',
'create/' => 'ReleephProjectCreateController',
'(?P<projectID>[1-9]\d*)/' => array(
'' => 'ReleephProjectViewController',
'closedbranches/' => 'ReleephProjectViewController',
'edit/' => 'ReleephProjectEditController',
'cutbranch/' => 'ReleephBranchCreateController',
'action/(?P<action>.+)/' => 'ReleephProjectActionController',
),
),
'branch/' => array(
'edit/(?P<branchID>[1-9]\d*)/' =>
'ReleephBranchEditController',
'(?P<action>close|re-open)/(?P<branchID>[1-9]\d*)/' =>
'ReleephBranchAccessController',
'preview/' => 'ReleephBranchNamePreviewController',
// Left in, just in case the by-name stuff fails!
'(?P<branchID>[^/]+)/' =>
'ReleephBranchViewController',
),
'request/' => array(
'(?P<requestID>[1-9]\d*)/' => 'ReleephRequestViewController',
'create/' => 'ReleephRequestCreateController',
'differentialcreate/' => array(
'D(?P<diffRevID>[1-9]\d*)' =>
'ReleephRequestDifferentialCreateController',
),
'edit/(?P<requestID>[1-9]\d*)/' =>
'ReleephRequestEditController',
'action/(?P<action>.+)/(?P<requestID>[1-9]\d*)/' =>
'ReleephRequestActionController',
'typeahead/' =>
'ReleephRequestTypeaheadController',
),
// Branch navigation made pretty, as it's the most common:
'(?P<projectName>[^/]+)/(?P<branchName>[^/]+)/' => array(
'' => 'ReleephBranchViewController',
'edit/' => 'ReleephBranchEditController',
'request/' => 'ReleephRequestCreateController',
'(?P<action>close|re-open)/' => 'ReleephBranchAccessController',
),
)
);
}
}

View file

@ -0,0 +1,74 @@
<?php
final class ReleephCommitFinder {
private $releephProject;
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function fromPartial($partial_string) {
// Look for diffs
$matches = array();
if (preg_match('/^D([1-9]\d*)$/', $partial_string, $matches)) {
$diff_id = $matches[1];
$diff_rev = id(new DifferentialRevision())->load($diff_id);
if (!$diff_rev) {
throw new ReleephCommitFinderException(
"{$partial_string} does not refer to an existing diff.");
}
$commit_phids = $diff_rev->loadCommitPHIDs();
if (!$commit_phids) {
throw new ReleephCommitFinderException(
"{$partial_string} has no commits associated with it yet.");
}
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'phid IN (%Ls) ORDER BY epoch ASC',
$commit_phids);
return head($commits);
}
// Look for a raw commit number, or r<callsign><commit-number>.
$repository = $this->releephProject->loadPhabricatorRepository();
$dr_data = null;
$matches = array();
if (preg_match('/^r(?P<callsign>[A-Z]+)(?P<commit>\w+)$/',
$partial_string, $matches)) {
$callsign = $matches['callsign'];
if ($callsign != $repository->getCallsign()) {
throw new ReleephCommitFinderException(sprintf(
"%s is in a different repository to this Releeph project (%s).",
$partial_string,
$repository->getCallsign()));
} else {
$dr_data = $matches;
}
} else {
$dr_data = array(
'callsign' => $repository->getCallsign(),
'commit' => $partial_string
);
}
try {
$dr = DiffusionRequest::newFromDictionary($dr_data);
} catch (Exception $ex) {
$message = "No commit matches {$partial_string}: ".$ex->getMessage();
throw new ReleephCommitFinderException($message);
}
$phabricator_repository_commit = $dr->loadCommit();
if (!$phabricator_repository_commit) {
throw new ReleephCommitFinderException(
"The commit {$partial_string} doesn't exist in this repository.");
}
return $phabricator_repository_commit;
}
}

View file

@ -0,0 +1,3 @@
<?php
final class ReleephCommitFinderException extends Exception {}

View file

@ -0,0 +1,9 @@
<?php
abstract class ConduitAPI_releeph_Method extends ConduitAPIMethod {
public function getApplication() {
return PhabricatorApplication::getByClass('PhabricatorApplicationReleeph');
}
}

View file

@ -0,0 +1,62 @@
<?php
final class ConduitAPI_releeph_getbranches_Method
extends ConduitAPI_releeph_Method {
public function getMethodDescription() {
return "Return information about all active Releeph branches.";
}
public function defineParamTypes() {
return array(
);
}
public function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$projects = id(new ReleephProject())->loadAllWhere('isActive = 1');
foreach ($projects as $project) {
$repository = $project->loadOneRelative(
id(new PhabricatorRepository()),
'id',
'getRepositoryID');
$branches = $project->loadRelatives(
id(new ReleephBranch()),
'releephProjectID',
'getID',
'isActive = 1');
foreach ($branches as $branch) {
$full_branch_name = $branch->getName();
$cut_point_commit = $branch->loadOneRelative(
id(new PhabricatorRepositoryCommit()),
'phid',
'getCutPointCommitPHID');
$results[] = array(
'project' => $project->getName(),
'repository' => $repository->getCallsign(),
'branch' => $branch->getBasename(),
'fullBranchName' => $full_branch_name,
'symbolicName' => $branch->getSymbolicName(),
'cutPoint' => $branch->getCutPointCommitIdentifier(),
);
}
}
return $results;
}
}

View file

@ -0,0 +1,96 @@
<?php
final class ConduitAPI_releeph_projectinfo_Method
extends ConduitAPI_releeph_Method {
public function getMethodDescription() {
return
"Fetch information about all Releeph projects ".
"for a given Arcanist project.";
}
public function defineParamTypes() {
return array(
'arcProjectName' => 'optional string',
);
}
public function defineReturnType() {
return 'dict<string, wild>';
}
public function defineErrorTypes() {
return array(
"ERR_UNKNOWN_ARC" =>
"The given Arcanist project name doesn't exist in the ".
"installation of Phabricator you are accessing.",
);
}
protected function execute(ConduitAPIRequest $request) {
$arc_project_name = $request->getValue('arcProjectName');
if ($arc_project_name) {
$arc_project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere('name = %s', $arc_project_name);
if (!$arc_project) {
throw id(new ConduitException("ERR_UNKNOWN_ARC"))
->setErrorDescription(
"Unknown Arcanist project '{$arc_project_name}': ".
"are you using the correct Conduit URI?");
}
$releeph_projects = id(new ReleephProject())
->loadAllWhere('arcanistProjectID = %d', $arc_project->getID());
} else {
$releeph_projects = id(new ReleephProject())->loadAll();
}
$releeph_projects = mfilter($releeph_projects, 'getIsActive');
$result = array();
foreach ($releeph_projects as $releeph_project) {
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
$fields_info = array();
foreach ($fields as $field) {
$field->setReleephProject($releeph_project);
if ($field->isEditable()) {
$key = $field->getKeyForConduit();
$fields_info[$key] = array(
'class' => get_class($field),
'name' => $field->getName(),
'key' => $key,
'arcHelp' => $field->renderHelpForArcanist(),
);
}
}
$releeph_branches = mfilter(
id(new ReleephBranch())
->loadAllWhere('releephProjectID = %d', $releeph_project->getID()),
'getIsActive');
$releeph_branches_struct = array();
foreach ($releeph_branches as $branch) {
$releeph_branches_struct[] = array(
'branchName' => $branch->getName(),
'projectName' => $releeph_project->getName(),
'projectPHID' => $releeph_project->getPHID(),
'branchPHID' => $branch->getPHID(),
);
}
$result[] = array(
'projectName' => $releeph_project->getName(),
'projectPHID' => $releeph_project->getPHID(),
'branches' => $releeph_branches_struct,
'fields' => $fields_info,
);
}
return $result;
}
}

View file

@ -0,0 +1,130 @@
<?php
final class ConduitAPI_releeph_request_Method
extends ConduitAPI_releeph_Method {
public function getMethodDescription() {
return "Request a commit or diff to be picked to a branch.";
}
public function defineParamTypes() {
return array(
'branchPHID' => 'required string',
'things' => 'required string',
'fields' => 'dict<string, string>',
);
}
public function defineReturnType() {
return 'dict<string, wild>';
}
public function defineErrorTypes() {
return array(
"ERR_BRANCH" => 'Unknown Releeph branch.',
"ERR_FIELD_PARSE" => 'Unable to parse a Releeph field.',
);
}
protected function execute(ConduitAPIRequest $request) {
$branch_phid = $request->getValue('branchPHID');
$releeph_branch = id(new ReleephBranch())
->loadOneWhere('phid = %s', $branch_phid);
if (!$releeph_branch) {
throw id(new ConduitException("ERR_BRANCH"))->setErrorDescription(
"No ReleephBranch found with PHID {$branch_phid}!");
}
$releeph_project = $releeph_branch->loadReleephProject();
// Find the requested commit identifiers
$requested_commits = array();
$things = $request->getValue('things');
$finder = id(new ReleephCommitFinder())
->setReleephProject($releeph_project);
foreach ($things as $thing) {
try {
$requested_commits[$thing] = $finder->fromPartial($thing);
} catch (ReleephCommitFinderException $ex) {
throw id(new ConduitException('ERR_NO_MATCHES'))
->setErrorDescription($ex->getMessage());
}
}
// Find any existing requests that clash on the commit id, for this branch
$existing_releeph_requests = id(new ReleephRequest())->loadAllWhere(
'requestCommitPHID IN (%Ls) AND branchID = %d',
mpull($requested_commits, 'getPHID'),
$releeph_branch->getID());
$existing_releeph_requests = mpull(
$existing_releeph_requests,
null,
'getRequestCommitPHID');
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $field) {
$field
->setReleephProject($releeph_project)
->setReleephBranch($releeph_branch);
}
$results = array();
foreach ($requested_commits as $thing => $commit) {
$phid = $commit->getPHID();
$handles = id(new PhabricatorObjectHandleData(array($phid)))
->setViewer($request->getUser())
->loadHandles();
$name = id($handles[$phid])->getName();
$releeph_request = null;
$existing_releeph_request = idx($existing_releeph_requests, $phid);
if ($existing_releeph_request) {
$releeph_request = $existing_releeph_request;
} else {
$releeph_request = new ReleephRequest();
foreach ($fields as $field) {
if (!$field->isEditable()) {
continue;
}
$field->setReleephRequest($releeph_request);
try {
$field->setValueFromConduitAPIRequest($request);
} catch (ReleephFieldParseException $ex) {
throw id(new ConduitException('ERR_FIELD_PARSE'))
->setErrorDescription($ex->getMessage());
}
}
id(new ReleephRequestEditor($releeph_request))
->setActor($request->getUser())
->create($commit, $releeph_branch);
}
$releeph_branch->populateReleephRequestHandles(
$request->getUser(),
array($releeph_request));
$rq_handles = $releeph_request->getHandles();
$requestor_phid = $releeph_request->getRequestUserPHID();
$requestor = $rq_handles[$requestor_phid]->getName();
$url = PhabricatorEnv::getProductionURI('/RQ'.$releeph_request->getID());
$results[$thing] = array(
'thing' => $thing,
'branch' => $releeph_branch->getDisplayNameWithDetail(),
'commitName' => $name,
'commitID' => $commit->getCommitIdentifier(),
'url' => $url,
'requestID' => $releeph_request->getID(),
'requestor' => $requestor,
'requestTime' => $releeph_request->getDateCreated(),
'existing' => $existing_releeph_request !== null,
);
}
return $results;
}
}

View file

@ -0,0 +1,39 @@
<?php
final class ConduitAPI_releephwork_canpush_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Return whether the conduit user is allowed to push.";
}
public function defineParamTypes() {
return array(
'projectPHID' => 'required string',
);
}
public function defineReturnType() {
return 'bool';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$releeph_project = id(new ReleephProject())
->loadOneWhere('phid = %s', $request->getValue('projectPHID'));
if (!$releeph_project->getPushers()) {
return true;
} else {
$user = $request->getUser();
return $releeph_project->isPusher($user);
}
}
}

View file

@ -0,0 +1,43 @@
<?php
final class ConduitAPI_releephwork_getauthorinfo_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Return a string to use as the VCS author.";
}
public function defineParamTypes() {
return array(
'userPHID' => 'required string',
'vcsType' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty string';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $request->getValue('userPHID'));
$email = $user->loadPrimaryEmailAddress();
if (is_numeric($email)) {
$email = $user->getUserName().'@fb.com';
}
return sprintf(
'%s <%s>',
$user->getRealName(),
$email);
}
}

View file

@ -0,0 +1,52 @@
<?php
final class ConduitAPI_releephwork_getbranch_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Return information to help checkout / cut a Releeph branch.";
}
public function defineParamTypes() {
return array(
'branchPHID' => 'required string',
);
}
public function defineReturnType() {
return 'dict<string, wild>';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$branch = id(new ReleephBranch())
->loadOneWhere('phid = %s', $request->getValue('branchPHID'));
$cut_phid = $branch->getCutPointCommitPHID();
$phids = array($cut_phid);
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($request->getUser())
->loadHandles();
$project = $branch->loadReleephProject();
$repo = $project->loadPhabricatorRepository();
return array(
'branchName' => $branch->getName(),
'branchPHID' => $branch->getPHID(),
'vcsType' => $repo->getVersionControlSystem(),
'cutCommitID' => $branch->getCutPointCommitIdentifier(),
'cutCommitName' => $handles[$cut_phid]->getName(),
'creatorPHID' => $branch->getCreatedByUserPHID(),
'trunk' => $project->getTrunkBranch(),
);
}
}

View file

@ -0,0 +1,94 @@
<?php
final class ConduitAPI_releephwork_getbranchcommitmessage_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Get a commit message for committing a Releeph branch.";
}
public function defineParamTypes() {
return array(
'branchPHID' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty string';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$branch = id(new ReleephBranch())
->loadOneWhere('phid = %s', $request->getValue('branchPHID'));
$project = $branch->loadReleephProject();
$creator_phid = $branch->getCreatedByUserPHID();
$cut_phid = $branch->getCutPointCommitPHID();
$phids = array(
$branch->getPHID(),
$project->getPHID(),
$creator_phid,
$cut_phid,
);
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($request->getUser())
->loadHandles();
$h_branch = $handles[$branch->getPHID()];
$h_project = $handles[$project->getPHID()];
// Not as customizable as a ReleephRequest's commit message. It doesn't
// really need to be.
$commit_message = array();
$commit_message[] = $h_branch->getFullName();
$commit_message[] = $h_branch->getURI();
$commit_message[] = "Cut Point: ".$handles[$cut_phid]->getName();
$cut_point_pr_commit = id(new PhabricatorRepositoryCommit())
->loadOneWhere('phid = %s', $cut_phid);
$cut_point_commit_date = strftime(
'%Y-%m-%d %H:%M:%S%z',
$cut_point_pr_commit->getEpoch());
$commit_message[] = "Cut Point Date: {$cut_point_commit_date}";
$commit_message[] = "Created By: ".$handles[$creator_phid]->getName();
$project_uri = $project->getURI();
$commit_message[] = "Project: ".$h_project->getName()." ".$project_uri;
/**
* Required for 090-limit_new_branch_creations.sh in
* admin/scripts/git/hosting/hooks/update.d (in the E repo):
*
* http://fburl.com/2372545
*
* The commit message must have a line saying:
*
* @new-branch: <branch-name>
*
*/
$repo = $project->loadPhabricatorRepository();
switch ($repo->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$commit_message[] = sprintf(
'@new-branch: %s',
$branch->getName());
break;
}
return implode("\n\n", $commit_message);
}
}

View file

@ -0,0 +1,90 @@
<?php
final class ConduitAPI_releephwork_getcommitmessage_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return
"Get commit message components for building ".
"a ReleephRequest commit message.";
}
public function defineParamTypes() {
return array(
'requestPHID' => 'required string',
'action' => 'required enum<"pick", "revert">',
);
}
public function defineReturnType() {
return 'dict<string, string>';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$releeph_request = id(new ReleephRequest())
->loadOneWhere('phid = %s', $request->getValue('requestPHID'));
$action = $request->getValue('action');
$title = $releeph_request->getSummaryForDisplay();
$commit_message = array();
$project = $releeph_request->loadReleephProject();
$branch = $releeph_request->loadReleephBranch();
$selector = $project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
$fields = $selector->sortFieldsForCommitMessage($fields);
foreach ($fields as $field) {
$field
->setUser($request->getUser())
->setReleephProject($project)
->setReleephBranch($branch)
->setReleephRequest($releeph_request);
$label = null;
$value = null;
switch ($action) {
case 'pick':
if ($field->shouldAppearOnCommitMessage()) {
$label = $field->renderLabelForCommitMessage();
$value = $field->renderValueForCommitMessage();
}
break;
case 'revert':
if ($field->shouldAppearOnRevertMessage()) {
$label = $field->renderLabelForRevertMessage();
$value = $field->renderValueForRevertMessage();
}
break;
}
if ($label && $value) {
if (strpos($value, "\n") !== false ||
substr($value, 0, 2) === ' ') {
$commit_message[] = "{$label}:\n{$value}";
} else {
$commit_message[] = "{$label}: {$value}";
}
}
}
return array(
'title' => $title,
'body' => implode("\n\n", $commit_message),
);
}
}

View file

@ -0,0 +1,35 @@
<?php
final class ConduitAPI_releephwork_getorigcommitmessage_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Return the original commit message for the given commit.";
}
public function defineParamTypes() {
return array(
'commitPHID' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty string';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$commit = id(new PhabricatorRepositoryCommit())
->loadOneWhere('phid = %s', $request->getValue('commitPHID'));
$commit_data = $commit->loadCommitData();
$commit_message = $commit_data->getCommitMessage();
return trim($commit_message);
}
}

View file

@ -0,0 +1,208 @@
<?php
final class ConduitAPI_releephwork_nextrequest_Method
extends ConduitAPI_releeph_Method {
private $project;
private $branch;
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return
"Return info required to cut a branch, ".
"and pick and revert ReleephRequests";
}
public function defineParamTypes() {
return array(
'branchPHID' => 'required int',
'seen' => 'required list<string, bool>',
);
}
public function defineReturnType() {
return '';
}
public function defineErrorTypes() {
return array(
'ERR-NOT-PUSHER' =>
'You are not listed as a pusher for thie Releeph project!',
);
}
protected function execute(ConduitAPIRequest $request) {
$seen = $request->getValue('seen');
$branch = id(new ReleephBranch())
->loadOneWhere('phid = %s', $request->getValue('branchPHID'));
$project = $branch->loadReleephProject();
$needs_pick = array();
$needs_revert = array();
$releeph_requests = $branch->loadReleephRequests($request->getUser());
foreach ($releeph_requests as $candidate) {
$phid = $candidate->getPHID();
if (idx($seen, $phid)) {
continue;
}
$should = $candidate->shouldBeInBranch();
$in = $candidate->getInBranch();
if ($should && !$in) {
$needs_pick[] = $candidate;
}
if (!$should && $in) {
$needs_revert[] = $candidate;
}
}
/**
* Sort both needs_pick and needs_revert in ascending commit order, as
* discovered by Phabricator (using the `id` column to perform that
* ordering).
*
* This is easy for $needs_pick as the ordinal is stored. It is hard for
* reverts, as we have to look that information up.
*/
$needs_pick = msort($needs_pick, 'getRequestCommitOrdinal');
$needs_revert = $this->sortReverts($needs_revert);
/**
* Do reverts first in reverse order, then the picks in original-commit
* order.
*
* This seems like the correct thing to do, but there may be a better
* algorithm for the releephwork.nextrequest Conduit call that orders
* things better.
*
* We could also button-mash our way through everything that failed (at the
* end of the run) to try failed things again.
*/
$releeph_request = null;
$action = null;
if ($needs_revert) {
$releeph_request = last($needs_revert);
$action = 'revert';
$commit_id = $releeph_request->getCommitIdentifier();
$commit_phid = $releeph_request->getCommitPHID();
} elseif ($needs_pick) {
$releeph_request = head($needs_pick);
$action = 'pick';
$commit_id = $releeph_request->getRequestCommitIdentifier();
$commit_phid = $releeph_request->getRequestCommitPHID();
} else {
// Return early if there's nothing to do!
return array();
}
// Build the response
$phids = array();
$phids[] = $commit_phid;
$diff_phid = null;
$diff_rev_id = null;
$diff_rev = $releeph_request->loadDifferentialRevision();
if ($diff_rev) {
$diff_phid = $diff_rev->getPHID();
$phids[] = $diff_phid;
$diff_rev_id = $diff_rev->getID();
}
$phids[] = $releeph_request->getPHID();
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($request->getUser())
->loadHandles();
$diff_name = null;
if ($diff_rev) {
$diff_name = $handles[$diff_phid]->getName();
}
// Calculate the new-author information (if any)
$new_author = null;
$new_author_phid = null;
switch ($project->getDetail('commitWithAuthor')) {
case ReleephProject::COMMIT_AUTHOR_NONE:
break;
case ReleephProject::COMMIT_AUTHOR_FROM_DIFF:
if ($diff_rev) {
$new_author_phid = $diff_rev->getAuthorPHID();
} else {
$pr_commit = $releeph_request->loadPhabricatorRepositoryCommit();
if ($pr_commit) {
$new_author_phid = $pr_commit->getAuthorPHID();
}
}
break;
case ReleephProject::COMMIT_AUTHOR_REQUESTOR:
$new_author_phid = $releeph_request->getRequestUserPHID();
break;
}
return array(
'requestID' => $releeph_request->getID(),
'requestPHID' => $releeph_request->getPHID(),
'requestName' => $handles[$releeph_request->getPHID()]->getName(),
'requestorPHID' => $releeph_request->getRequestUserPHID(),
'action' => $action,
'diffRevID' => $diff_rev_id,
'diffName' => $diff_name,
'commitIdentifier' => $commit_id,
'commitPHID' => $commit_phid,
'commitName' => $handles[$commit_phid]->getName(),
'needsRevert' => mpull($needs_revert, 'getID'),
'needsPick' => mpull($needs_pick, 'getID'),
'newAuthorPHID' => $new_author_phid,
);
}
/**
* Sort an array of ReleephRequests, that have been picked into a branch, in
* the order in which they were picked to the branch.
*/
private function sortReverts(array $releeph_requests) {
if (!$releeph_requests) {
return array();
}
// ReleephRequests, keyed by <branch-commit-id>
$releeph_requests = mpull($releeph_requests, null, 'getCommitIdentifier');
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere(
'commitIdentifier IN (%Ls)',
mpull($releeph_requests, 'getCommitIdentifier'));
// A map of <branch-commit-id> => <branch-commit-ordinal>
$surrogate = mpull($commits, 'getID', 'getCommitIdentifier');
$unparsed = array();
$result = array();
foreach ($releeph_requests as $commit_id => $releeph_request) {
$ordinal = idx($surrogate, $commit_id);
if ($ordinal) {
$result[$ordinal] = $releeph_request;
} else {
$unparsed[] = $releeph_request;
}
}
// Sort $result in ascending order
ksort($result);
// Unparsed commits we'll just have to guess, based on time
$unparsed = msort($unparsed, 'getDateModified');
return array_merge($result, $unparsed);
}
}

View file

@ -0,0 +1,42 @@
<?php
final class ConduitAPI_releephwork_record_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Wrapper to ReleephRequestEditor->recordSuccessfulCommit().";
}
public function defineParamTypes() {
return array(
'requestPHID' => 'required string',
'action' => 'required enum<"pick", "revert">',
'commitIdentifier' => 'required string',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$action = $request->getValue('action');
$new_commit_id = $request->getValue('commitIdentifier');
$releeph_request = id(new ReleephRequest())
->loadOneWhere('phid = %s', $request->getValue('requestPHID'));
id(new ReleephRequestEditor($releeph_request))
->setActor($request->getUser())
->recordSuccessfulCommit($action, $new_commit_id);
}
}

View file

@ -0,0 +1,63 @@
<?php
final class ConduitAPI_releephwork_recordpickstatus_Method
extends ConduitAPI_releeph_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Wrapper to ReleephRequestEditor->changePickStatus().";
}
public function defineParamTypes() {
return array(
'requestPHID' => 'required string',
'action' => 'required enum<"pick", "revert">',
'ok' => 'required bool',
'dryRun' => 'optional bool',
'details' => 'optional dict<string, wild>',
);
}
public function defineReturnType() {
return '';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$action = $request->getValue('action');
$ok = $request->getValue('ok');
$dry_run = $request->getValue('dryRun');
$details = $request->getValue('details', array());
switch ($request->getValue('action')) {
case 'pick':
$pick_status = $ok
? ReleephRequest::PICK_OK
: ReleephRequest::PICK_FAILED;
break;
case 'revert':
$pick_status = $ok
? ReleephRequest::REVERT_OK
: ReleephRequest::REVERT_FAILED;
break;
default:
throw new Exception("Unknown action {$action}!");
}
$releeph_request = id(new ReleephRequest())
->loadOneWhere('phid = %s', $request->getValue('requestPHID'));
id(new ReleephRequestEditor($releeph_request))
->setActor($request->getUser())
->changePickStatus($pick_status, $dry_run, $details);
}
}

View file

@ -0,0 +1,64 @@
<?php
final class PhabricatorApplicationReleephConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht("Releeph");
}
public function getDescription() {
return pht("Options for configuring Releeph, the release branch tool.");
}
public function getOptions() {
return array(
$this->newOption('releeph.installed', 'bool', false)
->setSummary(pht('Enable the Releeph application.'))
->setDescription(
pht(
"Releeph, a tool for managing release branches, will eventually ".
"fit in to the Phabricator suite as a general purpose tool. ".
"However Releeph is currently unstable in multiple ways that may ".
"not migrate properly for you: the code is still in alpha stage ".
"of design, the storage format is likely to change in unexpected ".
"ways, and the workflows presented are very specific to a core ".
"set of alpha testers at Facebook. For the time being you are ".
"strongly discouraged from relying on Releeph being at all ".
"stable.")),
$this->newOption(
'releeph.field-selector',
'class',
'ReleephDefaultFieldSelector')
->setBaseClass('ReleephFieldSelector')
->setSummary(pht('Field selector class'))
->setDescription(
pht(
"Control which fields are available when making a new Releeph ".
"request, and which are then shown in the Releeph UI.")),
$this->newOption(
'releeph.user-view',
'class',
'ReleephDefaultUserView')
->setBaseClass('ReleephUserView')
->setSummary(pht('Extra markup when rendering usernames'))
->setDescription(
pht(
"A wrapper to render Phabricator users in Releeph, with custom ".
"markup. For example, Facebook extends this to render additional ".
"information about requestors, to each Releeph project's ".
"pushers.")),
$this->newOption(
'releeph.default-branch-template',
'string',
'releases/%P/%p-%Y%m%d-%v')
->setDescription(
pht(
"The default branch template for new branches in unconfigured ".
"Releeph projects. This is also configurable on a per-project ".
"basis.")),
);
}
}

View file

@ -0,0 +1,122 @@
<?php
abstract class ReleephController extends PhabricatorController {
private $releephProject;
private $releephBranch;
private $releephRequest;
/**
* ReleephController will take care of loading any Releeph* objects
* referenced in the URL.
*/
public function willProcessRequest(array $data) {
// Project
$project = null;
$project_id = idx($data, 'projectID');
$project_name = idx($data, 'projectName');
if ($project_id) {
$project = id(new ReleephProject())->load($project_id);
if (!$project) {
throw new Exception(
"ReleephProject with id '{$project_id}' not found!");
}
} elseif ($project_name) {
$project = id(new ReleephProject())
->loadOneWhere('name = %s', $project_name);
if (!$project) {
throw new Exception(
"ReleephProject with name '{$project_name}' not found!");
}
}
// Branch
$branch = null;
$branch_id = idx($data, 'branchID');
$branch_name = idx($data, 'branchName');
if ($branch_id) {
$branch = id(new ReleephBranch())->load($branch_id);
if (!$branch) {
throw new Exception("Branch with id '{$branch_id}' not found!");
}
} elseif ($branch_name) {
if (!$project) {
throw new Exception(
"You cannot refer to a branch by name without also referring ".
"to a ReleephProject (branch names are only unique in projects).");
}
$branch = id(new ReleephBranch())->loadOneWhere(
'basename = %s AND releephProjectID = %d',
$branch_name,
$project->getID());
if (!$branch) {
throw new Exception(
"ReleephBranch with basename '{$branch_name}' not found ".
"in project '{$project->getName()}'!");
}
}
// Request
$request = null;
$request_id = idx($data, 'requestID');
if ($request_id) {
$request = id(new ReleephRequest())->load($request_id);
if (!$request) {
throw new Exception(
"ReleephRequest with id '{$request_id}' not found!");
}
}
// Fill in the gaps
if ($request && !$branch) {
$branch = $request->loadReleephBranch();
}
if ($branch && !$project) {
$project = $branch->loadReleephProject();
}
// Set!
$this->releephProject = $project;
$this->releephBranch = $branch;
$this->releephRequest = $request;
}
protected function getReleephProject() {
if (!$this->releephProject) {
throw new Exception(
'This controller did not load a ReleephProject from the URL $data.');
}
return $this->releephProject;
}
protected function getReleephBranch() {
if (!$this->releephBranch) {
throw new Exception(
'This controller did not load a ReleephBranch from the URL $data.');
}
return $this->releephBranch;
}
protected function getReleephRequest() {
if (!$this->releephRequest) {
throw new Exception(
'This controller did not load a ReleephRequest from the URL $data.');
}
return $this->releephRequest;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Releeph');
$page->setBaseURI('/releeph/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xD3\x82");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}

View file

@ -0,0 +1,61 @@
<?php
final class ReleephBranchAccessController extends ReleephController {
private $action;
public function willProcessRequest(array $data) {
$this->action = $data['action'];
parent::willProcessRequest($data);
}
public function processRequest() {
$rph_branch = $this->getReleephBranch();
$request = $this->getRequest();
$active_uri = '/releeph/project/'.$rph_branch->getReleephProjectID().'/';
$inactive_uri = $active_uri.'inactive/';
switch ($this->action) {
case 'close':
$is_active = false;
$origin_uri = $active_uri;
break;
case 're-open':
$is_active = true;
$origin_uri = $inactive_uri;
break;
default:
throw new Exception("Unknown action '{$this->action}'!");
break;
}
if ($request->isDialogFormPost()) {
id(new ReleephBranchEditor())
->setActor($request->getUser())
->setReleephBranch($rph_branch)
->changeBranchAccess($is_active ? 1 : 0);
return id(new AphrontRedirectResponse())
->setURI($origin_uri);
}
$button_text = ucfirst($this->action).' Branch';
$message = hsprintf(
'<p>Really %s the branch <i>%s</i>?</p>',
$this->action,
$rph_branch->getBasename());
$dialog = new AphrontDialogView();
$dialog
->setUser($request->getUser())
->setTitle('Confirm')
->appendChild($message)
->addSubmitButton($button_text)
->addCancelButton($origin_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -0,0 +1,105 @@
<?php
final class ReleephBranchCreateController extends ReleephController {
public function processRequest() {
$releeph_project = $this->getReleephProject();
$request = $this->getRequest();
$cut_point = $request->getStr('cutPoint');
$symbolic_name = $request->getStr('symbolicName');
if (!$cut_point) {
$repository = $releeph_project->loadPhabricatorRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$cut_point = $releeph_project->getTrunkBranch();
break;
}
}
$e_cut = true;
$errors = array();
$branch_date_control = id(new AphrontFormDateControl())
->setUser($request->getUser())
->setName('templateDate')
->setLabel('Date')
->setCaption('The date used for filling out the branch template.')
->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY);
$branch_date = $branch_date_control->readValueFromRequest($request);
if ($request->isFormPost()) {
$cut_commit = null;
if (!$cut_point) {
$e_cut = 'Required';
$errors[] = 'You must give a branch cut point';
} else {
try {
$finder = id(new ReleephCommitFinder())
->setReleephProject($releeph_project);
$cut_commit = $finder->fromPartial($cut_point);
} catch (Exception $e) {
$e_cut = 'Invalid';
$errors[] = $e->getMessage();
}
}
if (!$errors) {
$branch = id(new ReleephBranchEditor())
->setReleephProject($releeph_project)
->setActor($request->getUser())
->newBranchFromCommit(
$cut_commit,
$branch_date,
$symbolic_name);
return id(new AphrontRedirectResponse())
->setURI($branch->getURI());
}
}
$error_view = array();
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Symbolic name')
->setName('symbolicName')
->setValue($symbolic_name)
->setCaption('Mutable alternate name, for easy reference, '.
'(e.g. "LATEST")'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Cut point')
->setName('cutPoint')
->setValue($cut_point)
->setError($e_cut)
->setCaption(
'A commit ID for your repo type, or a Diffusion ID like "rE123"'))
->appendChild($branch_date_control)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Cut Branch')
->addCancelButton($releeph_project->getURI()));
$panel = id(new AphrontPanelView())
->appendChild($form)
->setHeader('Cut Branch')
->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
array($error_view, $panel),
array('title' => 'Cut new branch'));
}
}

View file

@ -0,0 +1,136 @@
<?php
final class ReleephBranchEditController extends ReleephController {
public function processRequest() {
$request = $this->getRequest();
$releeph_branch = $this->getReleephBranch();
$branch_name = $request->getStr(
'branchName',
$releeph_branch->getName());
$symbolic_name = $request->getStr(
'symbolicName',
$releeph_branch->getSymbolicName());
$e_existing_with_same_branch_name = false;
$errors = array();
if ($request->isFormPost()) {
$existing_with_same_branch_name =
id(new ReleephBranch())
->loadOneWhere(
'id != %d AND releephProjectID = %d AND name = %s',
$releeph_branch->getID(),
$releeph_branch->getReleephProjectID(),
$branch_name);
if ($existing_with_same_branch_name) {
$errors[] = sprintf(
"The branch name %s is currently taken. Please use another name. ",
$branch_name);
$e_existing_with_same_branch_name = 'Error';
}
if (!$errors) {
$existing_with_same_symbolic_name =
id(new ReleephBranch())
->loadOneWhere(
'id != %d AND releephProjectID = %d AND symbolicName = %s',
$releeph_branch->getID(),
$releeph_branch->getReleephProjectID(),
$symbolic_name);
$releeph_branch->openTransaction();
$releeph_branch
->setName($branch_name)
->setBasename(last(explode('/', $branch_name)))
->setSymbolicName($symbolic_name);
if ($existing_with_same_symbolic_name) {
$existing_with_same_symbolic_name
->setSymbolicName(null)
->save();
}
$releeph_branch->save();
$releeph_branch->saveTransaction();
return id(new AphrontRedirectResponse())
->setURI('/releeph/project/'.$releeph_branch->getReleephProjectID());
}
}
$phids = array();
$phids[] = $creator_phid = $releeph_branch->getCreatedByUserPHID();
$phids[] = $cut_commit_phid = $releeph_branch->getCutPointCommitPHID();
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($request->getUser())
->loadHandles();
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Branch name')
->setValue($branch_name))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Cut point')
->setValue($handles[$cut_commit_phid]->renderLink()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Created by')
->setValue($handles[$creator_phid]->renderLink()))
->appendChild(
id(new AphrontFormTextControl)
->setLabel('Symbolic Name')
->setName('symbolicName')
->setValue($symbolic_name)
->setCaption('Mutable alternate name, for easy reference, '.
'(e.g. "LATEST")'))
->appendChild(hsprintf(
'<br>' .
'In dire situations where the branch name is wrong, ' .
'you can edit it in the database by changing the field below. ' .
'If you do this, it is very important that you change your ' .
'branch\'s name in the VCS to reflect the new name in Releeph, ' .
'otherwise a catastrophe of previously unheard-of magnitude ' .
'will befall your project.'))
->appendChild(
id(new AphrontFormTextControl)
->setLabel('New branch name')
->setName('branchName')
->setValue($branch_name)
->setError($e_existing_with_same_branch_name))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($releeph_branch->getURI())
->setValue('Save'));
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_ERROR)
->setErrors($errors)
->setTitle('Errors');
}
$title = hsprintf(
'Edit branch %s',
$releeph_branch->getDisplayNameWithDetail());
$panel = id(new AphrontPanelView())
->setHeader($title)
->appendChild($form)
->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array('title' => $title));
}
}

View file

@ -0,0 +1,46 @@
<?php
final class ReleephBranchNamePreviewController
extends PhabricatorController {
public function processRequest() {
$request = $this->getRequest();
$is_symbolic = $request->getBool('isSymbolic');
$template = $request->getStr('template');
if (!$is_symbolic && !$template) {
$template = ReleephBranchTemplate::getDefaultTemplate();
}
$arc_project_id = $request->getInt('arcProjectID');
$fake_commit_handle =
ReleephBranchTemplate::getFakeCommitHandleFor($arc_project_id);
list($name, $errors) = id(new ReleephBranchTemplate())
->setCommitHandle($fake_commit_handle)
->setReleephProjectName($request->getStr('projectName'))
->setSymbolic($is_symbolic)
->interpolate($template);
$markup = '';
if ($name) {
$markup = phutil_tag(
'div',
array('class' => 'name'),
$name);
}
if ($errors) {
$markup .= phutil_tag(
'div',
array('class' => 'error'),
head($errors));
}
return id(new AphrontAjaxResponse())
->setContent(array('markup' => $markup));
}
}

View file

@ -0,0 +1,94 @@
<?php
final class ReleephBranchViewController extends ReleephController {
public function processRequest() {
$request = $this->getRequest();
$releeph_branch = $this->getReleephBranch();
$releeph_project = $this->getReleephProject();
$all_releeph_requests = $releeph_branch->loadReleephRequests(
$request->getUser());
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->arrangeFieldsForSelectForm(
$selector->getFieldSpecifications());
$form = id(new AphrontFormView())
->setMethod('GET')
->setUser($request->getUser());
$filtered_releeph_requests = $all_releeph_requests;
foreach ($fields as $field) {
$all_releeph_requests_without_this_field = $all_releeph_requests;
foreach ($fields as $other_field) {
if ($other_field != $field) {
$other_field->selectReleephRequestsHook(
$request,
$all_releeph_requests_without_this_field);
}
}
$field->appendSelectControlsHook(
$form,
$request,
$all_releeph_requests,
$all_releeph_requests_without_this_field);
$field->selectReleephRequestsHook(
$request,
$filtered_releeph_requests);
}
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter'));
$list = id(new ReleephRequestHeaderListView())
->setOriginType('branch')
->setUser($request->getUser())
->setAphrontRequest($this->getRequest())
->setReleephProject($releeph_project)
->setReleephBranch($releeph_branch)
->setReleephRequests($filtered_releeph_requests);
$filter = id(new AphrontListFilterView())
->appendChild($form);
$crumbs = $this->buildApplicationCrumbs()
->addCrumb(
id(new PhabricatorCrumbView())
->setName($releeph_project->getName())
->setHref($releeph_project->getURI()))
->addCrumb(
id(new PhabricatorCrumbView())
->setName($releeph_branch->getDisplayNameWithDetail())
->setHref($releeph_branch->getURI()));
// Don't show the request button for inactive (closed) branches
if ($releeph_branch->isActive()) {
$create_uri = $releeph_branch->getURI('request/');
$crumbs->addAction(
id(new PhabricatorMenuItemView())
->setHref($create_uri)
->setName('Request Pick')
->setIcon('create'));
}
return $this->buildStandardPageResponse(
array(
$crumbs,
$filter,
$list
),
array(
'title' =>
$releeph_project->getName().
' - '.
$releeph_branch->getDisplayName().
' requests'
));
}
}

View file

@ -0,0 +1,63 @@
<?php
final class ReleephProjectActionController extends ReleephController {
private $action;
public function willProcessRequest(array $data) {
parent::willProcessRequest($data);
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$action = $this->action;
$rph_project = $this->getReleephProject();
switch ($action) {
case 'deactivate':
if ($request->isDialogFormPost()) {
$rph_project->deactivate($request->getUser())->save();
return id(new AphrontRedirectResponse())->setURI('/releeph');
}
$dialog = id(new AphrontDialogView())
->setUser($request->getUser())
->setTitle('Really deactivate Releeph Project?')
->appendChild(hsprintf(
'<p>Really deactivate the Releeph project <i>%s</i>?',
$rph_project->getName()))
->appendChild(hsprintf(
'<p style="margin-top:1em">It will still exist, but '.
'will be hidden from the list of active projects.</p>'))
->addSubmitButton('Deactivate Releeph Project')
->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
case 'activate':
$rph_project->setIsActive(1)->save();
return id(new AphrontRedirectResponse())->setURI('/releeph');
case 'delete':
if ($request->isDialogFormPost()) {
$rph_project->delete();
return id(new AphrontRedirectResponse())
->setURI('/releeph/project/inactive');
}
$dialog = id(new AphrontDialogView())
->setUser($request->getUser())
->setTitle('Really delete Releeph Project?')
->appendChild(hsprintf(
'<p>Really delete the "%s" Releeph project? '.
'This cannot be undone!</p>',
$rph_project->getName()))
->addSubmitButton('Delete Releeph Project')
->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
}

View file

@ -0,0 +1,149 @@
<?php
final class ReleephProjectCreateController extends ReleephController {
public function processRequest() {
$request = $this->getRequest();
$name = trim($request->getStr('name'));
$trunk_branch = trim($request->getStr('trunkBranch'));
$arc_pr_id = $request->getInt('arcPrID');
// Only allow arc projects with repositories. Sort and re-key by ID.
$arc_projects = id(new PhabricatorRepositoryArcanistProject())->loadAll();
$arc_projects = mpull(
msort(
mfilter($arc_projects, 'getRepositoryID'),
'getName'),
null,
'getID');
$e_name = true;
$e_trunk_branch = true;
$errors = array();
if ($request->isFormPost()) {
if (!$name) {
$e_name = 'Required';
$errors[] =
'Your releeph project should have a simple descriptive name.';
}
if (!$trunk_branch) {
$e_trunk_branch = 'Required';
$errors[] =
'You must specify which branch you will be picking from.';
}
$all_names = mpull(id(new ReleephProject())->loadAll(), 'getName');
if (in_array($name, $all_names)) {
$errors[] = "Releeph project name {$name} is already taken";
}
$arc_project = $arc_projects[$arc_pr_id];
$pr_repository = $arc_project->loadRepository();
if (!$errors) {
$releeph_project = id(new ReleephProject())
->setName($name)
->setTrunkBranch($trunk_branch)
->setRepositoryID($pr_repository->getID())
->setRepositoryPHID($pr_repository->getPHID())
->setArcanistProjectID($arc_project->getID())
->setCreatedByUserPHID($request->getUser()->getPHID())
->setIsActive(1)
->save();
return id(new AphrontRedirectResponse())->setURI('/releeph/');
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
// Make our own optgroup select control
$arc_project_choices = array();
$pr_repositories = mpull(
msort(
array_filter(
// Some arc-projects don't have repositories
mpull($arc_projects, 'loadRepository')),
'getName'),
null,
'getID');
foreach ($pr_repositories as $pr_repo_id => $pr_repository) {
$options = array();
foreach ($arc_projects as $arc_project) {
if ($arc_project->getRepositoryID() == $pr_repo_id) {
$options[$arc_project->getID()] = $arc_project->getName();
}
}
$arc_project_choices[$pr_repository->getName()] = $options;
}
$project_name_input = id(new AphrontFormTextControl())
->setLabel('Name')
->setDisableAutocomplete(true)
->setName('name')
->setValue($name)
->setError($e_name)
->setCaption('A name like "Thrift" but not "Thrift releases".');
$arc_project_input = id(new AphrontFormSelectControl())
->setLabel('Arc Project')
->setName('arcPrID')
->setValue($arc_pr_id)
->setCaption(hsprintf(
"If your Arc project isn't listed, associate it with a repository %s",
phutil_tag(
'a',
array(
'href' => '/repository/',
'target' => '_blank',
),
'here')))
->setOptions($arc_project_choices);
$branch_name_preview = id(new ReleephBranchPreviewView())
->setLabel('Example Branch')
->addControl('projectName', $project_name_input)
->addControl('arcProjectID', $arc_project_input)
->addStatic('template', '')
->addStatic('isSymbolic', false);
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild($project_name_input)
->appendChild($arc_project_input)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Trunk')
->setName('trunkBranch')
->setValue($trunk_branch)
->setError($e_trunk_branch)
->setCaption('The development branch, '.
'from which requests will be picked.'))
->appendChild($branch_name_preview)
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/releeph/project/')
->setValue('Create'));
$panel = id(new AphrontPanelView())
->setHeader('Create Releeph Project')
->appendChild($form)
->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
array($error_view, $panel),
array(
'title' => 'Create new Releeph Project'
));
}
}

View file

@ -0,0 +1,388 @@
<?php
final class ReleephProjectEditController extends ReleephController {
public function processRequest() {
$request = $this->getRequest();
$e_name = true;
$e_trunk_branch = true;
$e_branch_template = false;
$errors = array();
$project_name = $request->getStr('name',
$this->getReleephProject()->getName());
$phabricator_project_id = $request->getInt('projectID',
$this->getReleephProject()->getProjectID());
$trunk_branch = $request->getStr('trunkBranch',
$this->getReleephProject()->getTrunkBranch());
$branch_template = $request->getStr('branchTemplate');
if ($branch_template === null) {
$branch_template =
$this->getReleephProject()->getDetail('branchTemplate');
}
$pick_failure_instructions = $request->getStr('pickFailureInstructions',
$this->getReleephProject()->getDetail('pick_failure_instructions'));
$commit_author = $request->getStr('commitWithAuthor',
$this->getReleephProject()->getDetail('commitWithAuthor'));
$test_paths = $request->getStr('testPaths');
if ($test_paths !== null) {
$test_paths = array_filter(explode("\n", $test_paths));
} else {
$test_paths = $this->getReleephProject()->getDetail('testPaths', array());
}
$field_selector = $request->getStr('fieldSelector',
get_class($this->getReleephProject()->getReleephFieldSelector()));
$release_counter = $request->getInt(
'releaseCounter',
$this->getReleephProject()->getCurrentReleaseNumber());
$arc_project_id = $this->getReleephProject()->getArcanistProjectID();
if ($request->isFormPost()) {
$pusher_phids = $request->getArr('pushers');
if (!$project_name) {
$e_name = 'Required';
$errors[] =
'Your releeph project should have a simple descriptive name';
}
if (!$trunk_branch) {
$e_trunk_branch = 'Required';
$errors[] =
'You must specify which branch you will be picking from.';
}
if ($release_counter && !is_int($release_counter)) {
$errors[] = "Release counter must be a positive integer!";
}
$other_releeph_projects = id(new ReleephProject())
->loadAllWhere('id <> %d', $this->getReleephProject()->getID());
$other_releeph_project_names = mpull($other_releeph_projects,
'getName', 'getID');
if (in_array($project_name, $other_releeph_project_names)) {
$errors[] = "Releeph project name {$project_name} is already taken";
}
foreach ($test_paths as $test_path) {
$result = @preg_match($test_path, '');
$is_a_valid_regexp = $result !== false;
if (!$is_a_valid_regexp) {
$errors[] = 'Please provide a valid regular expression: '.
"{$test_path} is not valid";
}
}
$project = $this->getReleephProject()
->setProjectID($phabricator_project_id)
->setTrunkBranch($trunk_branch)
->setDetail('pushers', $pusher_phids)
->setDetail('pick_failure_instructions', $pick_failure_instructions)
->setDetail('field_selector', $field_selector)
->setDetail('branchTemplate', $branch_template)
->setDetail('commitWithAuthor', $commit_author)
->setDetail('testPaths', $test_paths);
if ($release_counter) {
$project->setDetail('releaseCounter', $release_counter);
}
$fake_commit_handle =
ReleephBranchTemplate::getFakeCommitHandleFor($arc_project_id);
if ($branch_template) {
list($branch_name, $template_errors) = id(new ReleephBranchTemplate())
->setCommitHandle($fake_commit_handle)
->setReleephProjectName($project_name)
->interpolate($branch_template);
if ($template_errors) {
$e_branch_template = 'Invalid!';
foreach ($template_errors as $template_error) {
$errors[] = "Template error: {$template_error}";
}
}
}
if (!$errors) {
$project->save();
return id(new AphrontRedirectResponse())
->setURI('/releeph/project/');
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
$projects = mpull(
id(new PhabricatorProject())->loadAll(),
'getName',
'getID');
$projects[0] = '-'; // no project associated, that's ok
$pusher_phids = $request->getArr(
'pushers',
$this->getReleephProject()->getDetail('pushers', array()));
$handles = id(new PhabricatorObjectHandleData($pusher_phids))
->setViewer($request->getUser())
->loadHandles();
$pusher_tokens = array();
foreach ($pusher_phids as $phid) {
$pusher_tokens[$phid] = $handles[$phid]->getFullName();
}
$basic_inset = id(new AphrontFormInsetView())
->setTitle('Basics')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($project_name)
->setError($e_name)
->setCaption('A name like "Thrift" but not "Thrift releases".'))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Repository')
->setValue(
$this
->getReleephProject()
->loadPhabricatorRepository()
->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Arc Project')
->setValue(
$this->getReleephProject()->loadArcanistProject()->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Releeph Project PHID')
->setValue(
$this->getReleephProject()->getPHID()))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Phabricator Project')
->setValue($phabricator_project_id)
->setName('projectID')
->setOptions($projects))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Trunk')
->setValue($trunk_branch)
->setName('trunkBranch')
->setError($e_trunk_branch))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Release counter')
->setValue($release_counter)
->setName('releaseCounter')
->setCaption(
"Used by the command line branch cutter's %N field"))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Pick Instructions')
->setValue($pick_failure_instructions)
->setName('pickFailureInstructions')
->setCaption(
"Instructions for pick failures, which will be used " .
"in emails generated by failed picks"))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Tests paths')
->setValue(implode("\n", $test_paths))
->setName('testPaths')
->setCaption(
'List of strings that all test files contain in their path '.
'in this project. One string per line. '.
'Examples: \'__tests__\', \'/javatests/\'...'));
$pushers_inset = id(new AphrontFormInsetView())
->setTitle('Pushers')
->appendChild(
'Pushers are allowed to approve Releeph requests to be committed. '.
'to this project\'s branches. If you leave this blank then anyone '.
'is allowed to approve requests.')
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Pushers')
->setName('pushers')
->setDatasource('/typeahead/common/users/')
->setValue($pusher_tokens));
$field_selector_options = array();
$field_selector_symbols = id(new PhutilSymbolLoader())
->setType('class')
->setConcreteOnly(true)
->setAncestorClass('ReleephFieldSelector')
->selectAndLoadSymbols();
foreach ($field_selector_symbols as $symbol) {
$selector_name = $symbol['name'];
$field_selector_options[$selector_name] = $selector_name;
}
$field_selector_blurb = hsprintf(
"If you you have additional information to render about Releeph ".
"requests, or want to re-arrange the UI, implement a ".
"<tt>ReleephFieldSelector</tt> and select it here.");
$fields_inset = id(new AphrontFormInsetView())
->setTitle('Fields')
->appendChild($field_selector_blurb)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Selector')
->setName('fieldSelector')
->setValue($field_selector)
->setOptions($field_selector_options));
$commit_author_inset = $this->buildCommitAuthorInset($commit_author);
// Build the Template inset
$markup_engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
// From DifferentialUnitFieldSpecification...
$markup_engine->setConfig('viewer', $request->getUser());
$help_markup = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
phutil_safe_html(
$markup_engine->markupText(ReleephBranchTemplate::getHelpRemarkup())));
$branch_template_input = id(new AphrontFormTextControl())
->setName('branchTemplate')
->setValue($branch_template)
->setLabel('Template')
->setError($e_branch_template)
->setCaption(
"Leave this blank to use your installation's default.");
$branch_template_preview = id(new ReleephBranchPreviewView())
->setLabel('Preview')
->addControl('template', $branch_template_input)
->addStatic('arcProjectID', $arc_project_id)
->addStatic('isSymbolic', false)
->addStatic('projectName', $this->getReleephProject()->getName());
$template_inset = id(new AphrontFormInsetView())
->setTitle('Branch Cutting')
->appendChild(
'Provide a pattern for creating new branches.')
->appendChild($branch_template_input)
->appendChild($branch_template_preview)
->appendChild($help_markup);
// Build the form
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild($basic_inset)
->appendChild($pushers_inset)
->appendChild($fields_inset)
->appendChild($commit_author_inset)
->appendChild($template_inset);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/releeph/project/')
->setValue('Save'));
$panel = id(new AphrontPanelView())
->setHeader('Edit Releeph Project')
->appendChild($form)
->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
array($error_view, $panel),
array('title' => 'Edit Releeph Project'));
}
private function buildCommitAuthorInset($current) {
$vcs_type = $this->getReleephProject()
->loadPhabricatorRepository()
->getVersionControlSystem();
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return;
break;
}
$vcs_name = PhabricatorRepositoryType::getNameForRepositoryType($vcs_type);
$help_markup = hsprintf(<<<EOTEXT
When your project's release engineers run <tt>arc releeph</tt>, they will be
listed as the <strong>committer</strong> of the code committed to release
branches.
%s allows you to specify a separate author when committing code. Some
tools use the author of a commit (rather than the committer) when they need to
notify someone about a build or test failure.
Releeph can use one of the following to set the <strong>author</strong> of the
commits it makes:
EOTEXT
, $vcs_name);
$trunk = $this->getReleephProject()->getTrunkBranch();
$options = array(
array(
'value' => ReleephProject::COMMIT_AUTHOR_FROM_DIFF,
'label' => 'Original Author',
'caption' =>
"The author of the original commit in {$trunk}.",
),
array(
'value' => ReleephProject::COMMIT_AUTHOR_REQUESTOR,
'label' => 'Requestor',
'caption' =>
"The person who requested that this code go into the release.",
),
array(
'value' => ReleephProject::COMMIT_AUTHOR_NONE,
'label' => "None",
'caption' =>
"Only record the default committer information.",
),
);
if (!$current) {
$current = ReleephProject::COMMIT_AUTHOR_FROM_DIFF;
}
$control = id(new AphrontFormRadioButtonControl())
->setLabel('Author')
->setName('commitWithAuthor')
->setValue($current);
foreach ($options as $dict) {
$control->addButton($dict['value'], $dict['label'], $dict['caption']);
}
return id(new AphrontFormInsetView())
->setTitle('Authors')
->appendChild($help_markup)
->appendChild($control);
}
}

View file

@ -0,0 +1,70 @@
<?php
final class ReleephProjectListController extends PhabricatorController {
public function processRequest() {
$path = $this->getRequest()->getRequestURI()->getPath();
$is_active = strpos($path, 'inactive/') === false;
$releeph_projects = mfilter(
id(new ReleephProject())->loadAll(),
'getIsActive',
!$is_active);
$releeph_projects = msort($releeph_projects, 'getName');
$releeph_projects_set = new LiskDAOSet();
foreach ($releeph_projects as $releeph_project) {
$releeph_projects_set->addToSet($releeph_project);
}
$panel = new AphrontPanelView();
if ($is_active) {
$view_inactive_link = phutil_tag(
'a',
array(
'href' => '/releeph/project/inactive/',
),
'View inactive projects');
$panel
->setHeader(hsprintf(
'Active Releeph Projects &middot; %s', $view_inactive_link))
->appendChild(
id(new ReleephActiveProjectListView())
->setUser($this->getRequest()->getUser())
->setReleephProjects($releeph_projects));
} else {
$view_active_link = phutil_tag(
'a',
array(
'href' => '/releeph/project/'
),
'View active projects');
$panel
->setHeader(hsprintf(
'Inactive Releeph Projects &middot; %s', $view_active_link))
->appendChild(
id(new ReleephInactiveProjectListView())
->setUser($this->getRequest()->getUser())
->setReleephProjects($releeph_projects));
}
if ($is_active) {
$create_new_project_button = phutil_tag(
'a',
array(
'href' => '/releeph/project/create/',
'class' => 'green button',
),
'Create New Project');
$panel->addButton($create_new_project_button);
}
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'List Releeph Projects'
));
}
}

View file

@ -0,0 +1,45 @@
<?php
final class ReleephProjectViewController extends ReleephController {
public function processRequest() {
// Load all branches
$releeph_project = $this->getReleephProject();
$releeph_branches = id(new ReleephBranch())
->loadAllWhere('releephProjectID = %d',
$releeph_project->getID());
$path = $this->getRequest()->getRequestURI()->getPath();
$is_open_branches = strpos($path, 'closedbranches/') === false;
$view = id(new ReleephProjectView())
->setShowOpenBranches($is_open_branches)
->setUser($this->getRequest()->getUser())
->setReleephProject($releeph_project)
->setBranches($releeph_branches);
$crumbs = $this->buildApplicationCrumbs()
->addCrumb(
id(new PhabricatorCrumbView())
->setName($releeph_project->getName())
->setHref($releeph_project->getURI()));
if ($releeph_project->getIsActive()) {
$crumbs->addAction(
id(new PhabricatorMenuItemView())
->setHref($releeph_project->getURI('cutbranch'))
->setName('Cut New Branch')
->setIcon('create'));
}
return $this->buildStandardPageResponse(
array(
$crumbs,
$view,
),
array(
'title' => $releeph_project->getName().' Releeph Project'
));
}
}

View file

@ -0,0 +1,71 @@
<?php
final class ReleephRequestActionController extends ReleephController {
private $action;
public function willProcessRequest(array $data) {
parent::willProcessRequest($data);
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$releeph_branch = $this->getReleephBranch();
$releeph_request = $this->getReleephRequest();
$releeph_branch->populateReleephRequestHandles(
$request->getUser(), array($releeph_request));
$action = $this->action;
$user = $request->getUser();
$origin_uri = $releeph_request->loadReleephBranch()->getURI();
$editor = id(new ReleephRequestEditor($releeph_request))
->setActor($user);
switch ($action) {
case 'want':
case 'pass':
static $action_map = array(
'want' => ReleephRequest::INTENT_WANT,
'pass' => ReleephRequest::INTENT_PASS);
$intent = $action_map[$action];
$editor->changeUserIntent($user, $intent);
break;
case 'mark-manually-picked':
$editor->markManuallyActioned('pick');
break;
case 'mark-manually-reverted':
$editor->markManuallyActioned('revert');
break;
default:
throw new Exception("unknown or unimplemented action {$action}");
}
// If we're adding a new user to userIntents, we'll have to re-populate
// request handles to load that user's data.
//
// This is cheap enough to do every time.
$this->getReleephBranch()->populateReleephRequestHandles(
$user, array($releeph_request));
$list = id(new ReleephRequestHeaderListView())
->setReleephProject($this->getReleephProject())
->setReleephBranch($this->getReleephBranch())
->setReleephRequests(array($releeph_request))
->setUser($request->getUser())
->setAphrontRequest($this->getRequest())
->setOriginType('request');
return id(new AphrontAjaxResponse())->setContent(array(
'markup' => head($list->renderInner())
));
}
}

View file

@ -0,0 +1,165 @@
<?php
final class ReleephRequestCreateController extends ReleephController {
const MAX_SUMMARY_LENGTH = 70;
public function processRequest() {
$request = $this->getRequest();
// We arrived via /releeph/request/create/?branchID=$id
$releeph_branch_id = $request->getInt('branchID');
if ($releeph_branch_id) {
$releeph_branch = id(new ReleephBranch())->load($releeph_branch_id);
} else {
// We arrived via /releeph/$project/$branch/request.
//
// If this throws an Exception, then somethind weird happened.
$releeph_branch = $this->getReleephBranch();
}
$releeph_project = $releeph_branch->loadReleephProject();
$repo = $releeph_project->loadPhabricatorRepository();
$request_identifier = $request->getStr('requestIdentifierRaw');
$e_request_identifier = true;
$releeph_request = new ReleephRequest();
$errors = array();
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $field) {
$field
->setReleephProject($releeph_project)
->setReleephBranch($releeph_branch)
->setReleephRequest($releeph_request);
}
if ($request->isFormPost()) {
foreach ($fields as $field) {
if ($field->isEditable()) {
try {
$field->setValueFromAphrontRequest($request);
} catch (ReleephFieldParseException $ex) {
$errors[] = $ex->getMessage();
}
}
}
$pr_commit = null;
$finder = id(new ReleephCommitFinder())
->setReleephProject($releeph_project);
try {
$pr_commit = $finder->fromPartial($request_identifier);
} catch (Exception $e) {
$e_request_identifier = 'Invalid';
$errors[] =
"Request {$request_identifier} is probably not a valid commit";
$errors[] = $e->getMessage();
}
$pr_commit_data = null;
if (!$errors) {
$pr_commit_data = $pr_commit->loadCommitData();
if (!$pr_commit_data) {
$e_request_identifier = 'Not parsed yet';
$errors[] = "The requested commit hasn't been parsed yet.";
}
}
if (!$errors) {
$existing = id(new ReleephRequest())
->loadOneWhere('requestCommitPHID = %s AND branchID = %d',
$pr_commit->getPHID(), $releeph_branch->getID());
if ($existing) {
return id(new AphrontRedirectResponse())
->setURI('/releeph/request/edit/'.$existing->getID().
'?existing=1');
}
id(new ReleephRequestEditor($releeph_request))
->setActor($request->getUser())
->create($pr_commit, $releeph_branch);
return id(new AphrontRedirectResponse())
->setURI($releeph_branch->getURI());
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
// For the typeahead
$branch_cut_point = id(new PhabricatorRepositoryCommit())
->loadOneWhere(
'phid = %s',
$releeph_branch->getCutPointCommitPHID());
// Build the form
$form = id(new AphrontFormView())
->setUser($request->getUser());
$origin = null;
$diff_rev_id = $request->getStr('D');
if ($diff_rev_id) {
$diff_rev = id(new DifferentialRevision())->load($diff_rev_id);
$origin = '/D'.$diff_rev->getID();
$title = sprintf(
'D%d: %s',
$diff_rev_id,
$diff_rev->getTitle());
$form
->addHiddenInput('requestIdentifierRaw', 'D'.$diff_rev_id)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Diff')
->setValue($title));
} else {
$origin = $releeph_branch->getURI();
$form->appendChild(
id(new ReleephRequestTypeaheadControl())
->setName('requestIdentifierRaw')
->setLabel('Commit ID')
->setRepo($repo)
->setValue($request_identifier)
->setError($e_request_identifier)
->setStartTime($branch_cut_point->getEpoch())
->setCaption(
'Start typing to autocomplete on commit title, '.
'or give a Phabricator commit identifier like rFOO1234'));
}
// Fields
foreach ($fields as $field) {
if ($field->isEditable()) {
$control = $field->renderEditControl($request);
$form->appendChild($control);
}
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($origin)
->setValue('Request'));
$panel = id(new AphrontPanelView())
->setHeader(
'Request for '.
$releeph_branch->getDisplayNameWithDetail())
->setWidth(AphrontPanelView::WIDTH_FORM)
->appendChild($form);
return $this->buildStandardPageResponse(
array($error_view, $panel),
array('title' => 'Request pick'));
}
}

View file

@ -0,0 +1,99 @@
<?php
final class ReleephRequestDifferentialCreateController
extends ReleephController {
private $revision;
public function willProcessRequest($data) {
$diff_rev_id = $data['diffRevID'];
$diff_rev = id(new DifferentialRevision())->load($diff_rev_id);
if (!$diff_rev) {
throw new Exception(sprintf('D%d not found!', $diff_rev_id));
}
$this->revision = $diff_rev;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$arc_project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere('phid = %s', $this->revision->getArcanistProjectPHID());
$projects = id(new ReleephProject())->loadAllWhere(
'arcanistProjectID = %d AND isActive = 1',
$arc_project->getID());
if (!$projects) {
throw new ReleephRequestException(sprintf(
"D%d belongs to the '%s' Arcanist project, ".
"which is not part of any Releeph project!",
$this->revision->getID(),
$arc_project->getName()));
}
$branches = id(new ReleephBranch())->loadAllWhere(
'releephProjectID IN (%Ld) AND isActive = 1',
mpull($projects, 'getID'));
if (!$branches) {
throw new ReleephRequestException(sprintf(
"D%d could be in the Releeph project(s) %s, ".
"but this project / none of these projects have open branches.",
$this->revision->getID(),
implode(', ', mpull($projects, 'getName'))));
}
if (count($branches) === 1) {
return id(new AphrontRedirectResponse())
->setURI($this->buildReleephRequestURI(head($branches)));
}
$projects = msort(
mpull($projects, null, 'getID'),
'getName');
$branch_groups = mgroup($branches, 'getReleephProjectID');
require_celerity_resource('releeph-request-differential-create-dialog');
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle('Choose Releeph Branch')
->setClass('releeph-request-differential-create-dialog')
->addCancelButton('/D'.$request->getStr('D'));
$dialog->appendChild(
"This differential revision changes code that is associated ".
"with multiple Releeph branches. ".
"Please select the branch where you would like this code to be picked.");
foreach ($branch_groups as $project_id => $branches) {
$project = idx($projects, $project_id);
$dialog->appendChild(
phutil_tag(
'h1',
array(),
$project->getName()));
$branches = msort($branches, 'getBasename');
foreach ($branches as $branch) {
$uri = $this->buildReleephRequestURI($branch);
$dialog->appendChild(
phutil_tag(
'a',
array(
'href' => $uri,
),
$branch->getDisplayNameWithDetail()));
}
}
return id(new AphrontDialogResponse)
->setDialog($dialog);
}
private function buildReleephRequestURI(ReleephBranch $branch) {
return id(new PhutilURI('/releeph/request/create/'))
->setQueryParam('branchID', $branch->getID())
->setQueryParam('D', $this->revision->getID());
}
}

View file

@ -0,0 +1,132 @@
<?php
final class ReleephRequestEditController extends ReleephController {
public function processRequest() {
$request = $this->getRequest();
$releeph_branch = $this->getReleephBranch();
$releeph_request = $this->getReleephRequest();
$releeph_branch->populateReleephRequestHandles(
$request->getUser(), array($releeph_request));
$phids = array();
$phids[] = $releeph_request->getRequestCommitPHID();
$phids[] = $releeph_request->getRequestUserPHID();
$phids[] = $releeph_request->getCommittedByUserPHID();
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($request->getUser())
->loadHandles();
$age_string = phabricator_format_relative_time(
time() - $releeph_request->getDateCreated());
// Warn the user if we see this
$notice_view = null;
if ($request->getInt('existing')) {
$notice_messages = array(
'You are editing an existing pick request!',
hsprintf(
"Requested %s ago by %s",
$age_string,
$handles[$releeph_request->getRequestUserPHID()]->renderLink())
);
$notice_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setErrors($notice_messages);
}
// <aidehua> epriestley: Is it common to pass around a referer URL to
// return from whence one came? [...]
// <epriestley> If you only have two places, maybe consider some parameter
// rather than the full URL.
switch ($request->getStr('origin')) {
case 'request':
$origin_uri = '/RQ'.$releeph_request->getID();
break;
case 'branch':
default:
$origin_uri = $releeph_request->loadReleephBranch()->getURI();
break;
}
$errors = array();
$selector = $this->getReleephProject()->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $field) {
$field
->setReleephProject($this->getReleephProject())
->setReleephBranch($this->getReleephBranch())
->setReleephRequest($this->getReleephRequest());
}
if ($request->isFormPost()) {
foreach ($fields as $field) {
if ($field->isEditable()) {
try {
$field->setValueFromAphrontRequest($request);
} catch (ReleephFieldParseException $ex) {
$errors[] = $ex->getMessage();
}
}
}
if (!$errors) {
$releeph_request->save();
return id(new AphrontRedirectResponse())->setURI($origin_uri);
}
}
/**
* Build the rest of the page
*/
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Original Commit')
->setValue(
$handles[$releeph_request->getRequestCommitPHID()]->renderLink()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Requestor')
->setValue(hsprintf(
'%s %s ago',
$handles[$releeph_request->getRequestUserPHID()]->renderLink(),
$age_string)));
// Fields
foreach ($fields as $field) {
if ($field->isEditable()) {
$control = $field->renderEditControl($request);
$form->appendChild($control);
}
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($origin_uri, 'Cancel')
->setValue('Save'));
$panel = id(new AphrontPanelView())
->setHeader('Edit Pick Request')
->setWidth(AphrontPanelView::WIDTH_FORM)
->appendChild($form);
return $this->buildStandardPageResponse(
array($notice_view, $error_view, $panel),
array('title', 'Edit Pick Request'));
}
}

View file

@ -0,0 +1,92 @@
<?php
final class ReleephRequestTypeaheadController
extends PhabricatorTypeaheadDatasourceController {
public function processRequest() {
$request = $this->getRequest();
$query = $request->getStr('q');
$repo_id = $request->getInt('repo');
$since = $request->getInt('since');
$limit = $request->getInt('limit');
$now = time();
$data = array();
// Dummy instances used for getting connections, table names, etc.
$pr_commit = new PhabricatorRepositoryCommit();
$pr_commit_data = new PhabricatorRepositoryCommitData();
$conn = $pr_commit->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT
rc.phid as commitPHID,
rc.authorPHID,
rcd.authorName,
SUBSTRING(rcd.commitMessage, 1, 100) AS shortMessage,
rc.commitIdentifier,
rc.epoch
FROM %T rc
INNER JOIN %T rcd ON rcd.commitID = rc.id
WHERE repositoryID = %d
AND rc.epoch >= %d
AND (
rcd.commitMessage LIKE %~
OR
rc.commitIdentifier LIKE %~
)
ORDER BY rc.epoch DESC
LIMIT %d',
$pr_commit->getTableName(),
$pr_commit_data->getTableName(),
$repo_id,
$since,
$query,
$query,
$limit);
foreach ($rows as $row) {
$full_commit_id = $row['commitIdentifier'];
$short_commit_id = substr($full_commit_id, 0, 12);
$first_line = $this->getFirstLine($row['shortMessage']);
$data[] = array(
$full_commit_id,
$short_commit_id,
$row['authorName'],
phabricator_format_relative_time($now - $row['epoch']),
$first_line,
);
}
return id(new AphrontAjaxResponse())
->setContent($data);
}
/**
* Split either at the first new line, or a bunch of dashes.
*
* Really just a legacy from old Releeph Daemon commit messages where I used
* to say:
*
* Commit of FOO for BAR
* ------------
* This does X Y Z
*
*/
private function getFirstLine($commit_message_fragment) {
static $separators = array('-------', "\n");
$string = ltrim($commit_message_fragment);
$first_line = $string;
foreach ($separators as $separator) {
if ($pos = strpos($string, $separator)) {
$first_line = substr($string, 0, $pos);
break;
}
}
return $first_line;
}
}

View file

@ -0,0 +1,99 @@
<?php
final class ReleephRequestViewController extends ReleephController {
public function processRequest() {
$request = $this->getRequest();
$uri_path = $request->getRequestURI()->getPath();
$legacy_prefix = '/releeph/request/';
if (strncmp($uri_path, $legacy_prefix, strlen($legacy_prefix)) === 0) {
return id(new AphrontRedirectResponse())
->setURI('/RQ'.$this->getReleephRequest()->getID());
}
$releeph_request = $this->getReleephRequest();
$releeph_branch = $this->getReleephBranch();
$releeph_project = $this->getReleephProject();
$releeph_branch->populateReleephRequestHandles(
$request->getUser(), array($releeph_request));
$rq_view =
id(new ReleephRequestHeaderListView())
->setReleephProject($releeph_project)
->setReleephBranch($releeph_branch)
->setReleephRequests(array($releeph_request))
->setUser($request->getUser())
->setAphrontRequest($this->getRequest())
->setReloadOnStateChange(true)
->setOriginType('request');
$events = $releeph_request->loadEvents();
$phids = array_mergev(mpull($events, 'extractPHIDs'));
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($request->getUser())
->loadHandles();
$rq_event_list_view =
id(new ReleephRequestEventListView())
->setUser($request->getUser())
->setEvents($events)
->setHandles($handles);
// Handle comment submit
$origin_uri = '/RQ'.$releeph_request->getID();
if ($request->isFormPost()) {
id(new ReleephRequestEditor($releeph_request))
->setActor($request->getUser())
->addComment($request->getStr('comment'));
return id(new AphrontRedirectResponse())->setURI($origin_uri);
}
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextAreaControl())
->setName('comment'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($origin_uri, 'Cancel')
->setValue("Submit"));
$rq_comment_form = id(new AphrontPanelView())
->setHeader('Add a comment')
->setWidth(AphrontPanelView::WIDTH_FULL)
->appendChild($form);
$title = hsprintf("RQ%d: %s",
$releeph_request->getID(),
$releeph_request->getSummaryForDisplay());
$crumbs = $this->buildApplicationCrumbs()
->addCrumb(
id(new PhabricatorCrumbView())
->setName($releeph_project->getName())
->setHref($releeph_project->getURI()))
->addCrumb(
id(new PhabricatorCrumbView())
->setName($releeph_branch->getDisplayNameWithDetail())
->setHref($releeph_branch->getURI()))
->addCrumb(
id(new PhabricatorCrumbView())
->setName('RQ'.$releeph_request->getID())
->setHref('/RQ'.$releeph_request->getID()));
return $this->buildStandardPageResponse(
array(
$crumbs,
array(
$rq_view,
$rq_event_list_view,
$rq_comment_form
)
),
array(
'title' => $title
));
}
}

View file

@ -0,0 +1,348 @@
<?php
/**
* This DifferentialFieldSpecification exists for two reason:
*
* 1: To parse "Releeph: picks RQ<nn>" headers in commits created by
* arc-releeph so that RQs committed by arc-releeph have real
* PhabricatorRepositoryCommits associated with them (instaed of just the SHA
* of the commit, as seen by the pusher).
*
* 2: If requestors want to commit directly to their release branch, they can
* use this header to (i) indicate on a differential revision that this
* differential revision is for the release branch, and (ii) when they land
* their diff on to the release branch manually, the ReleephRequest is
* automatically updated (instead of having to use the "Mark Manually Picked"
* button.)
*
*/
final class DifferentialReleephRequestFieldSpecification
extends DifferentialFieldSpecification {
const ACTION_PICKS = 'picks';
const ACTION_REVERTS = 'reverts';
private $releephAction;
private $releephPHIDs = array();
public function getStorageKey() {
return 'releeph:actions';
}
public function getValueForStorage() {
return json_encode(array(
'releephAction' => $this->releephAction,
'releephPHIDs' => $this->releephPHIDs,
));
}
public function setValueFromStorage($json) {
if ($json) {
$dict = json_decode($json, true);
$this->releephAction = idx($dict, 'releephAction');
$this->releephPHIDs = idx($dict, 'releephPHIDs');
}
return $this;
}
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Releeph';
}
public function getRequiredHandlePHIDs() {
return mpull($this->loadReleephRequests(), 'getPHID');
}
public function renderValueForRevisionView() {
static $tense = array(
self::ACTION_PICKS => array(
'future' => 'Will pick',
'past' => 'Picked',
),
self::ACTION_REVERTS => array(
'future' => 'Will revert',
'past' => 'Reverted',
),
);
$releeph_requests = $this->loadReleephRequests();
if (!$releeph_requests) {
return null;
}
$status = $this->getRevision()->getStatus();
if ($status == ArcanistDifferentialRevisionStatus::CLOSED) {
$verb = $tense[$this->releephAction]['past'];
} else {
$verb = $tense[$this->releephAction]['future'];
}
$parts = hsprintf('%s...', $verb);
foreach ($releeph_requests as $releeph_request) {
$parts->appendHTML(phutil_tag('br'));
$parts->appendHTML(
$this->getHandle($releeph_request->getPHID())->renderLink());
}
return $parts;
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'releephActions';
}
public function setValueFromParsedCommitMessage($dict) {
$this->releephAction = $dict['releephAction'];
$this->releephPHIDs = $dict['releephPHIDs'];
return $this;
}
public function renderValueForCommitMessage($is_edit) {
$releeph_requests = $this->loadReleephRequests();
if (!$releeph_requests) {
return null;
}
$parts = array($this->releephAction);
foreach ($releeph_requests as $releeph_request) {
$parts[] = 'RQ'.$releeph_request->getID();
}
return implode(' ', $parts);
}
/**
* Releeph fields should look like:
*
* Releeph: picks RQ1 RQ2, RQ3
* Releeph: reverts RQ1
*/
public function parseValueFromCommitMessage($value) {
/**
* Releeph commit messages look like this (but with more blank lines,
* omitted here):
*
* Make CaptainHaddock more reasonable
* Releeph: picks RQ1
* Requested By: edward
* Approved By: edward (requestor)
* Request Reason: x
* Summary: Make the Haddock implementation more reasonable.
* Test Plan: none
* Reviewers: user1
*
* Some of these fields are recognized by Differential (e.g. "Requested
* By"). They are folded up into the "Releeph" field, parsed by this
* class. As such $value includes more than just the first-line:
*
* "picks RQ1\n\nRequested By: edward\n\nApproved By: edward (requestor)"
*
* To hack around this, just consider the first line of $value when
* determining what Releeph actions the parsed commit is performing.
*/
$first_line = head(array_filter(explode("\n", $value)));
$tokens = preg_split('/\s*,?\s+/', $first_line);
$raw_action = array_shift($tokens);
$action = strtolower($raw_action);
if (!$action) {
return null;
}
switch ($action) {
case self::ACTION_REVERTS:
case self::ACTION_PICKS:
break;
default:
throw new DifferentialFieldParseException(
"Commit message contains unknown Releeph action '{$raw_action}'!");
break;
}
$releeph_requests = array();
foreach ($tokens as $token) {
$match = array();
if (!preg_match('/^(?:RQ)?(\d+)$/i', $token, $match)) {
$label = $this->renderLabelForCommitMessage();
throw new DifferentialFieldParseException(
"Commit message contains unparseable ".
"Releeph request token '{$token}'!");
}
$id = (int) $match[1];
$releeph_request = id(new ReleephRequest())->load($id);
if (!$releeph_request) {
throw new DifferentialFieldParseException(
"Commit message references non existent releeph request: {$value}!");
}
$releeph_requests[] = $releeph_request;
}
if (count($releeph_requests) > 1) {
$rqs_seen = array();
$groups = array();
foreach ($releeph_requests as $releeph_request) {
$releeph_branch = $releeph_request->loadReleephBranch();
$branch_name = $releeph_branch->getName();
$rq_id = 'RQ'.$releeph_request->getID();
if (idx($rqs_seen, $rq_id)) {
throw new DifferentialFieldParseException(
"Commit message refers to {$rq_id} multiple times!");
}
$rqs_seen[$rq_id] = true;
if (!isset($groups[$branch_name])) {
$groups[$branch_name] = array();
}
$groups[$branch_name][] = $rq_id;
}
if (count($groups) > 1) {
$lists = array();
foreach ($groups as $branch_name => $rq_ids) {
$lists[] = implode(', ', $rq_ids).' in '.$branch_name;
}
throw new DifferentialFieldParseException(
"Commit message references multiple Releeph requests, ".
"but the requests are in different branches: ".
implode('; ', $lists));
}
}
$phids = mpull($releeph_requests, 'getPHID');
$data = array(
'releephAction' => $action,
'releephPHIDs' => $phids,
);
return $data;
}
public function renderLabelForCommitMessage() {
return 'Releeph';
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
public function didParseCommit(PhabricatorRepository $repo,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$releeph_requests = $this->loadReleephRequests();
if (!$releeph_requests) {
return;
}
$releeph_branch = head($releeph_requests)->loadReleephBranch();
if (!$this->isCommitOnBranch($repo, $commit, $releeph_branch)) {
return;
}
foreach ($releeph_requests as $releeph_request) {
if ($this->releephAction === self::ACTION_PICKS) {
$action = 'pick';
} else {
$action = 'revert';
}
$actor_phid = coalesce(
$data->getCommitDetail('committerPHID'),
$data->getCommitDetail('authorPHID'));
$actor = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $actor_phid);
id(new ReleephRequestEditor($releeph_request))
->setActor($actor)
->discoverCommit($action, $commit, $data);
}
}
private function loadReleephRequests() {
if (!$this->releephPHIDs) {
return array();
} else {
return id(new ReleephRequest())
->loadAllWhere('phid IN (%Ls)', $this->releephPHIDs);
}
}
private function isCommitOnBranch(PhabricatorRepository $repo,
PhabricatorRepositoryCommit $commit,
ReleephBranch $releeph_branch) {
switch ($repo->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
list($output) = $repo->execxLocalCommand(
'branch --all --no-color --contains %s',
$commit->getCommitIdentifier());
$remote_prefix = 'remotes/origin/';
$branches = array();
foreach (array_filter(explode("\n", $output)) as $line) {
$tokens = explode(' ', $line);
$ref = last($tokens);
if (strncmp($ref, $remote_prefix, strlen($remote_prefix)) === 0) {
$branch = substr($ref, strlen($remote_prefix));
$branches[$branch] = $branch;
}
}
return idx($branches, $releeph_branch->getName());
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
DiffusionRequest::newFromDictionary(array(
'repository' => $repo,
'commit' => $commit->getCommitIdentifier(),
)));
$path_changes = $change_query->loadChanges();
$commit_paths = mpull($path_changes, 'getPath');
$branch_path = $releeph_branch->getName();
$in_branch = array();
$ex_branch = array();
foreach ($commit_paths as $path) {
if (strncmp($path, $branch_path, strlen($branch_path)) === 0) {
$in_branch[] = $path;
} else {
$ex_branch[] = $path;
}
}
if ($in_branch && $ex_branch) {
$error = sprintf(
"CONFUSION: commit %s in %s contains %d path change(s) that were ".
"part of a Releeph branch, but also has %d path change(s) not ".
"part of a Releeph branch!",
$commit->getCommitIdentifier(),
$repo->getCallsign(),
count($in_branch),
count($ex_branch));
phlog($error);
}
return !empty($in_branch);
break;
}
}
}

View file

@ -0,0 +1,39 @@
<?php
final class ReleephDifferentialRevisionDetailRenderer {
public static function generateActionLink(DifferentialRevision $revision,
DifferentialDiff $diff) {
$arc_project = $diff->loadArcanistProject(); // 93us
if (!$arc_project) {
return;
}
$releeph_projects = id(new ReleephProject())->loadAllWhere(
'arcanistProjectID = %d AND isActive = 1',
$arc_project->getID());
if (!$releeph_projects) {
return;
}
$releeph_branches = id(new ReleephBranch())->loadAllWhere(
'releephProjectID IN (%Ld) AND isActive = 1',
mpull($releeph_projects, 'getID'));
if (!$releeph_branches) {
return;
}
$uri = new PhutilURI(
'/releeph/request/differentialcreate/D'.$revision->getID());
return array(
'name' => 'Releeph Request',
'sigil' => 'workflow',
'href' => $uri,
'icon' => 'fork',
);
}
}

View file

@ -0,0 +1,106 @@
<?php
final class ReleephBranchEditor extends PhabricatorEditor {
private $releephProject;
private $releephBranch;
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function setReleephBranch(ReleephBranch $branch) {
$this->releephBranch = $branch;
return $this;
}
public function newBranchFromCommit(PhabricatorRepositoryCommit $cut_point,
$branch_date,
$symbolic_name = null) {
$template = $this->releephProject->getDetail('branchTemplate');
if (!$template) {
$template = ReleephBranchTemplate::getRequiredDefaultTemplate();
}
$cut_point_handle = head(
id(new PhabricatorObjectHandleData(array($cut_point->getPHID())))
// We'll assume that whoever found the $cut_point has passed privacy
// checks.
->setViewer($this->requireActor())
->loadHandles());
list($name, $errors) = id(new ReleephBranchTemplate())
->setCommitHandle($cut_point_handle)
->setBranchDate($branch_date)
->setReleephProjectName($this->releephProject->getName())
->interpolate($template);
$basename = last(explode('/', $name));
$table = id(new ReleephBranch());
$transaction = $table->openTransaction();
$branch = id(new ReleephBranch())
->setName($name)
->setBasename($basename)
->setReleephProjectID($this->releephProject->getID())
->setCreatedByUserPHID($this->requireActor()->getPHID())
->setCutPointCommitIdentifier($cut_point->getCommitIdentifier())
->setCutPointCommitPHID($cut_point->getPHID())
->setIsActive(1)
->setDetail('branchDate', $branch_date)
->save();
/**
* Steal the symbolic name from any other branch that has it (in this
* project).
*/
if ($symbolic_name) {
$others = id(new ReleephBranch())->loadAllWhere(
'releephProjectID = %d',
$this->releephProject->getID());
foreach ($others as $other) {
if ($other->getSymbolicName() == $symbolic_name) {
$other
->setSymbolicName(null)
->save();
}
}
$branch
->setSymbolicName($symbolic_name)
->save();
}
id(new ReleephEvent())
->setType(ReleephEvent::TYPE_BRANCH_CREATE)
->setActorPHID($this->requireActor()->getPHID())
->setReleephProjectID($this->releephProject->getID())
->setReleephBranchID($branch->getID())
->save();
$table->saveTransaction();
return $branch;
}
// aka "close" and "reopen"
public function changeBranchAccess($is_active) {
$branch = $this->releephBranch;
$branch->openTransaction();
$branch
->setIsActive((int)$is_active)
->save();
id(new ReleephEvent())
->setType(ReleephEvent::TYPE_BRANCH_ACCESS)
->setActorPHID($this->requireActor()->getPHID())
->setReleephProjectID($branch->getReleephProjectID())
->setReleephBranchID($branch->getID())
->setDetail('isActive', $is_active)
->save();
$branch->saveTransaction();
}
}

View file

@ -0,0 +1,408 @@
<?php
/**
* Provide methods for the common ways of creating and mutating a
* ReleephRequest, sending email when something interesting happens.
*
* This class generates ReleephRequestEvents, and each type of event
* (ReleephRequestEvent::TYPE_*) corresponds to one of the editor methods.
*
* The editor methods (except for create() use newEvent() and commit() to save
* some code duplication.
*/
final class ReleephRequestEditor extends PhabricatorEditor {
private $releephRequest;
private $event;
private $silentUpdate;
public function __construct(ReleephRequest $rq) {
$this->releephRequest = $rq;
}
public function setSilentUpdate($silent) {
$this->silentUpdate = $silent;
return $this;
}
/* -( ReleephRequest edit methods )---------------------------------------- */
/**
* Request a PhabricatorRepositoryCommit to be committed to the given
* ReleephBranch.
*/
public function create(PhabricatorRepositoryCommit $commit,
ReleephBranch $branch) {
// We can't use newEvent() / commit() abstractions, so do what those
// helpers do manually.
$requestor = $this->requireActor();
$rq = $this->releephRequest;
$rq->openTransaction();
$rq
->setBranchID($branch->getID())
->setRequestCommitIdentifier($commit->getCommitIdentifier())
->setRequestCommitPHID($commit->getPHID())
->setRequestCommitOrdinal($commit->getID())
->setInBranch(0)
->setRequestUserPHID($requestor->getPHID())
->setUserIntent($requestor, ReleephRequest::INTENT_WANT)
->save();
$event = id(new ReleephRequestEvent())
->setType(ReleephRequestEvent::TYPE_CREATE)
->setActorPHID($requestor->getPHID())
->setStatusBefore(null)
->setStatusAfter($rq->getStatus())
->setReleephRequestID($rq->getID())
->setDetail('commitPHID', $commit->getPHID())
->save();
$rq->saveTransaction();
// Mail
if (!$this->silentUpdate) {
$project = $this->releephRequest->loadReleephProject();
$mail = id(new ReleephRequestMail())
->setReleephRequest($this->releephRequest)
->setReleephProject($project)
->setEvents(array($event))
->setSenderAndRecipientPHID($requestor->getPHID())
->addTos(ReleephRequestMail::ENT_ALL_PUSHERS)
->addCCs(ReleephRequestMail::ENT_REQUESTOR)
->send();
}
}
/**
* Record whether the PhabricatorUser wants or passes on this request.
*/
public function changeUserIntent(PhabricatorUser $user, $intent) {
$project = $this->releephRequest->loadReleephProject();
$is_pusher = $project->isPusher($user);
$event = $this->newEvent()
->setType(ReleephRequestEvent::TYPE_USER_INTENT)
->setDetail('userPHID', $user->getPHID())
->setDetail('wasPusher', $is_pusher)
->setDetail('newIntent', $intent);
$this->releephRequest
->setUserIntent($user, $intent);
$this->commit();
// Mail if this is 'interesting'
if (!$this->silentUpdate &&
$event->getStatusBefore() != $event->getStatusAfter()) {
$project = $this->releephRequest->loadReleephProject();
$mail = id(new ReleephRequestMail())
->setReleephRequest($this->releephRequest)
->setReleephProject($project)
->setEvents(array($event))
->setSenderAndRecipientPHID($this->requireActor()->getPHID())
->addTos(ReleephRequestMail::ENT_REQUESTOR)
->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS)
->send();
}
}
/**
* Record the results of someone trying to pick or revert a request in their
* local repository, to give advance warning that something doesn't pick or
* revert cleanly.
*/
public function changePickStatus($pick_status, $dry_run, $details) {
$event = $this->newEvent()
->setType(ReleephRequestEvent::TYPE_PICK_STATUS)
->setDetail('newPickStatus', $pick_status)
->setDetail('commitDetails', $details);
$this->releephRequest->setPickStatus($pick_status);
$this->commit();
// Failures should generate an email
if (!$this->silentUpdate &&
!$dry_run &&
($pick_status == ReleephRequest::PICK_FAILED ||
$pick_status == ReleephRequest::REVERT_FAILED)) {
$project = $this->releephRequest->loadReleephProject();
$mail = id(new ReleephRequestMail())
->setReleephRequest($this->releephRequest)
->setReleephProject($project)
->setEvents(array($event))
->setSenderAndRecipientPHID($this->requireActor()->getPHID())
->addTos(ReleephRequestMail::ENT_REQUESTOR)
->addCCs(ReleephRequestMail::ENT_ACTORS)
->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS)
->send();
}
}
/**
* Record that a request was committed locally, and is about to be pushed to
* the remote repository.
*
* This lets us mark a ReleephRequest as being in a branch in real time so
* that no one else tries to pick it.
*
* When the daemons discover this commit in the repository with
* DifferentialReleephRequestFieldSpecification, we'll be able to recrod the
* commit's PHID as well. That process is slow though, and
* we don't want to wait a whole minute before marking something as cleanly
* picked or reverted.
*/
public function recordSuccessfulCommit($action, $new_commit_id) {
$table = $this->releephRequest;
$table->openTransaction();
$actor = $this->requireActor();
$event = id(new ReleephRequestEvent())
->setReleephRequestID($this->releephRequest->getID())
->setActorPHID($actor->getPHID())
->setType(ReleephRequestEvent::TYPE_COMMIT)
->setDetail('action', $action)
->setDetail('newCommitIdentifier', $new_commit_id)
->save();
switch ($action) {
case 'pick':
$this->releephRequest
->setInBranch(1)
->setPickStatus(ReleephRequest::PICK_OK)
->setCommitIdentifier($new_commit_id)
->setCommitPHID(null)
->setCommittedByUserPHID($actor->getPHID())
->save();
break;
case 'revert':
$this->releephRequest
->setInBranch(0)
->setPickStatus(ReleephRequest::REVERT_OK)
->setCommitIdentifier(null)
->setCommitPHID(null)
->setCommittedByUserPHID(null)
->save();
break;
default:
$table->killTransaction();
throw new Exception("Unknown action {$action}!");
break;
}
$table->saveTransaction();
// Don't spam people about local commits -- we'll do that with
// discoverCommit() instead!
}
/**
* Mark this request as picked or reverted based on discovering it in the
* branch. We have a PhabricatorRepositoryCommit, so we're able to
* setCommitPHID on the ReleephRequest (unlike recordSuccessfulCommit()).
*/
public function discoverCommit(
$action,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$table = $this->releephRequest;
$table->openTransaction();
$table->beginWriteLocking();
$past_events = id(new ReleephRequestEvent())->loadAllWhere(
'releephRequestID = %d AND type = %s',
$this->releephRequest->getID(),
ReleephRequestEvent::TYPE_DISCOVERY);
foreach ($past_events as $past_event) {
if ($past_event->getDetail('newCommitIdentifier')
== $commit->getCommitIdentifier()) {
// Avoid re-discovery if reparsing!
$table->endWriteLocking();
$table->killTransaction();
return;
}
}
$actor = $this->requireActor();
$event = id(new ReleephRequestEvent())
->setReleephRequestID($this->releephRequest->getID())
->setActorPHID($actor->getPHID())
->setType(ReleephRequestEvent::TYPE_DISCOVERY)
->setDateCreated($commit->getEpoch())
->setDetail('action', $action)
->setDetail('newCommitIdentifier', $commit->getCommitIdentifier())
->setDetail('newCommitPHID', $commit->getPHID())
->setDetail('authorPHID', $data->getCommitDetail('authorPHID'))
->setDetail('committerPHID', $data->getCommitDetail('committerPHID'))
->save();
switch ($action) {
case 'pick':
$this->releephRequest
->setInBranch(1)
->setPickStatus(ReleephRequest::PICK_OK)
->setCommitIdentifier($commit->getCommitIdentifier())
->setCommitPHID($commit->getPHID())
->setCommittedByUserPHID($actor->getPHID())
->save();
break;
case 'revert':
$this->releephRequest
->setInBranch(0)
->setPickStatus(ReleephRequest::REVERT_OK)
->setCommitIdentifier(null)
->setCommitPHID(null)
->setCommittedByUserPHID(null)
->save();
break;
default:
$table->killTransaction();
throw new Exception("Unknown action {$action}!");
break;
}
$table->endWriteLocking();
$table->saveTransaction();
// Mail
if (!$this->silentUpdate) {
$project = $this->releephRequest->loadReleephProject();
$mail = id(new ReleephRequestMail())
->setReleephRequest($this->releephRequest)
->setReleephProject($project)
->setEvents(array($event))
->setSenderAndRecipientPHID($this->requireActor()->getPHID())
->addTos(ReleephRequestMail::ENT_REQUESTOR)
->addCCs(ReleephRequestMail::ENT_ACTORS)
->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS)
->send();
}
}
public function addComment($comment) {
$event = $this->newEvent()
->setType(ReleephRequestEvent::TYPE_COMMENT)
->setDetail('comment', $comment);
$this->commit();
// Mail
if (!$this->silentUpdate) {
$project = $this->releephRequest->loadReleephProject();
$mail = id(new ReleephRequestMail())
->setReleephRequest($this->releephRequest)
->setReleephProject($project)
->setEvents(array($event))
->setSenderAndRecipientPHID($this->requireActor()->getPHID())
->addTos(ReleephRequestMail::ENT_REQUESTOR)
->addCCs(ReleephRequestMail::ENT_ACTORS)
->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS)
->send();
}
}
public function markManuallyActioned($action) {
$event = $this->newEvent()
->setType(ReleephRequestEvent::TYPE_MANUAL_ACTION)
->setDetail('action', $action);
$actor = $this->requireActor();
$project = $this->releephRequest->loadReleephProject();
$requestor_phid = $this->releephRequest->getRequestUserPHID();
if (!$project->isPusher($actor) &&
$actor->getPHID() !== $requestor_phid) {
throw new Exception(
"Only pushers or requestors can mark requests as ".
"manually picked or reverted!");
}
switch ($action) {
case 'pick':
$in_branch = true;
$intent = ReleephRequest::INTENT_WANT;
break;
case 'revert':
$in_branch = false;
$intent = ReleephRequest::INTENT_PASS;
break;
default:
throw new Exception("Unknown action {$action}!");
break;
}
$this->releephRequest
->setInBranch((int)$in_branch)
->setUserIntent($this->getActor(), $intent);
$this->commit();
// Mail
if (!$this->silentUpdate) {
$project = $this->releephRequest->loadReleephProject();
$mail = id(new ReleephRequestMail())
->setReleephRequest($this->releephRequest)
->setReleephProject($project)
->setEvents(array($event))
->setSenderAndRecipientPHID($this->requireActor()->getPHID())
->addTos(ReleephRequestMail::ENT_REQUESTOR)
->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS)
->send();
}
}
/* -( Implementation )----------------------------------------------------- */
/**
* Create and return a new ReleephRequestEvent bound to the editor's
* ReleephRequest, inside a transaction.
*
* When you call commit(), the event and this editor's ReleephRequest (along
* with any changes you made to the ReleephRequest) are saved and the
* transaction committed.
*/
private function newEvent() {
$actor = $this->requireActor();
if ($this->event) {
throw new Exception("You have already called newEvent()!");
}
$rq = $this->releephRequest;
$rq->openTransaction();
$this->event = id(new ReleephRequestEvent())
->setReleephRequestID($rq->getID())
->setActorPHID($actor->getPHID())
->setStatusBefore($rq->getStatus());
return $this->event;
}
private function commit() {
if (!$this->event) {
throw new Exception("You must call newEvent first!");
}
$rq = $this->releephRequest;
$this->event
->setStatusAfter($rq->getStatus())
->save();
$rq->save();
$rq->saveTransaction();
$this->event = null;
}
}

View file

@ -0,0 +1,213 @@
<?php
/**
* Build an email that renders a group of events with and appends some standard
* Releeph things (a URI for this request, and this branch).
*
* Also includes some helper stuff for adding groups of people to the To: and
* Cc: headers.
*/
final class ReleephRequestMail {
const ENT_REQUESTOR = 'requestor';
const ENT_DIFF = 'diff';
const ENT_ALL_PUSHERS = 'pushers';
const ENT_ACTORS = 'actors';
const ENT_INTERESTED_PUSHERS = 'interested-pushers';
private $sender;
private $tos = array();
private $ccs = array();
private $events;
private $releephRequest;
private $releephProject;
public function setReleephRequest(ReleephRequest $rq) {
$this->releephRequest = $rq;
return $this;
}
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function setEvents(array $events) {
assert_instances_of($events, 'ReleephRequestEvent');
$this->events = $events;
return $this;
}
public function setSenderAndRecipientPHID($sender_phid) {
$this->sender = $sender_phid;
$this->tos[] = $sender_phid;
return $this;
}
public function addTos($entity) {
$this->tos = array_merge(
$this->tos,
$this->getEntityPHIDs($entity));
return $this;
}
public function addCcs($entity) {
$this->ccs = array_merge(
$this->tos,
$this->getEntityPHIDs($entity));
return $this;
}
public function send() {
$this->buildMail()->save();
}
public function buildMail() {
return id(new PhabricatorMetaMTAMail())
->setSubject($this->renderSubject())
->setBody($this->buildBody()->render())
->setFrom($this->sender)
->addTos($this->tos)
->addCCs($this->ccs);
}
private function getEntityPHIDs($entity) {
$phids = array();
switch ($entity) {
// The requestor
case self::ENT_REQUESTOR:
$phids[] = $this->releephRequest->getRequestUserPHID();
break;
// People on the original diff
case self::ENT_DIFF:
$commit = $this->releephRequest->loadPhabricatorRepositoryCommit();
$commit_data = $commit->loadCommitData();
if ($commit_data) {
$phids[] = $commit_data->getCommitDetail('reviewerPHID');
$phids[] = $commit_data->getCommitDetail('authorPHID');
}
break;
// All pushers for this project
case self::ENT_ALL_PUSHERS:
$phids = array_merge(
$phids,
$this->releephProject->getPushers());
break;
// Pushers who have explicitly wanted or passed on this request
case self::ENT_INTERESTED_PUSHERS:
$all_pushers = $this->releephProject->getPushers();
$intents = $this->releephRequest->getUserIntents();
foreach ($all_pushers as $pusher) {
if (idx($intents, $pusher)) {
$phids[] = $pusher;
}
}
break;
// Anyone who created our list of events
case self::ENT_ACTORS:
$phids = array_merge(
$phids,
mpull($this->events, 'getActorPHID'));
break;
default:
throw new Exception(
"Unknown entity type {$entity}!");
break;
}
return array_filter($phids);
}
private function buildBody() {
$body = new PhabricatorMetaMTAMailBody();
$rq = $this->releephRequest;
// Events and comments
$phids = array(
$rq->getPHID(),
);
foreach ($this->events as $event) {
$phids = array_merge($phids, $event->extractPHIDs());
}
$handles = id(new PhabricatorObjectHandleData($phids))
// By the time we're generating email, we can assume that whichever
// entitties are receving the email are authorized to see the loaded
// handles!
->setViewer(PhabricatorUser::getOmnipotentUser())
->loadHandles();
$raw_events = id(new ReleephRequestEventListView())
->setUser(PhabricatorUser::getOmnipotentUser())
->setHandles($handles)
->setEvents($this->events)
->renderForEmail();
$body->addRawSection($raw_events);
$project = $rq->loadReleephProject();
$branch = $rq->loadReleephBranch();
/**
* If any of the events we are emailing about were TYPE_PICK_STATUS where
* the newPickStatus was a pick failure (and/or a revert failure?), include
* pick failure instructions.
*/
$pick_failure_events = array();
foreach ($this->events as $event) {
if ($event->getType() == ReleephRequestEvent::TYPE_PICK_STATUS &&
$event->getDetail('newPickStatus') == ReleephRequest::PICK_FAILED) {
$pick_failure_events[] = $event;
}
}
if ($pick_failure_events) {
$instructions = $project->getDetail('pick_failure_instructions');
if ($instructions) {
$body->addTextSection('PICK FAILURE INSTRUCTIONS', $instructions);
}
}
// Common stuff at the end
$body->addTextSection(
'RELEEPH REQUEST',
$handles[$rq->getPHID()]->getFullName()."\n".
PhabricatorEnv::getProductionURI('/RQ'.$rq->getID()));
$project_and_branch = sprintf(
'%s - %s',
$project->getName(),
$branch->getDisplayNameWithDetail());
$body->addTextSection(
'RELEEPH BRANCH',
$project_and_branch."\n".
$branch->getURI());
// But verbose stuff at the *very* end!
foreach ($pick_failure_events as $event) {
$failure_details = $event->getDetail('commitDetails');
if ($failure_details) {
$body->addRawSection('PICK FAILURE DETAILS');
foreach ($failure_details as $heading => $data) {
$body->addTextSection($heading, $data);
}
}
}
return $body;
}
private function renderSubject() {
$rq = $this->releephRequest;
$id = $rq->getID();
$summary = $rq->getSummaryForDisplay();
return "RQ{$id}: {$summary}";
}
}

View file

@ -0,0 +1,12 @@
<?php
final class ReleephFieldParseException extends Exception {
public function __construct(ReleephFieldSpecification $field,
$message) {
$name = $field->getName();
parent::__construct("{$name}: {$message}");
}
}

View file

@ -0,0 +1,11 @@
<?php
final class ReleephFieldSpecificationIncompleteException extends Exception {
public function __construct(ReleephFieldSpecification $field) {
$class = get_class($field);
parent::__construct(
"Releeph field class {$class} is incompletely implemented.");
}
}

View file

@ -0,0 +1,73 @@
<?php
final class ReleephDefaultFieldSelector extends ReleephFieldSelector {
public function getFieldSpecifications() {
return array(
new ReleephCommitMessageFieldSpecification(),
new ReleephSummaryFieldSpecification(),
new ReleephReasonFieldSpecification(),
new ReleephAuthorFieldSpecification(),
new ReleephRevisionFieldSpecification(),
new ReleephRequestorFieldSpecification(),
new ReleephSeverityFieldSpecification(),
new ReleephOriginalCommitFieldSpecification(),
new ReleephDiffMessageFieldSpecification(),
new ReleephStatusFieldSpecification(),
new ReleephIntentFieldSpecification(),
new ReleephBranchCommitFieldSpecification(),
new ReleephDiffSizeFieldSpecification(),
new ReleephDiffChurnFieldSpecification(),
);
}
public function arrangeFieldsForHeaderView(array $fields) {
return array(
// Top group
array(
'left' => self::selectFields($fields, array(
'ReleephAuthorFieldSpecification',
'ReleephRevisionFieldSpecification',
'ReleephOriginalCommitFieldSpecification',
'ReleephDiffSizeFieldSpecification',
'ReleephDiffChurnFieldSpecification',
)),
'right' => self::selectFields($fields, array(
'ReleephRequestorFieldSpecification',
'ReleephSeverityFieldSpecification',
'ReleephStatusFieldSpecification',
'ReleephIntentFieldSpecification',
'ReleephBranchCommitFieldSpecification',
))
),
// Bottom group
array(
'left' => self::selectFields($fields, array(
'ReleephDiffMessageFieldSpecification',
)),
'right' => self::selectFields($fields, array(
'ReleephReasonFieldSpecification',
))
)
);
}
public function arrangeFieldsForSelectForm(array $fields) {
self::selectFields($fields, array(
'ReleephStatusFieldSpecification',
'ReleephSeverityFieldSpecification',
'ReleephRequestorFieldSpecification',
));
}
public function sortFieldsForCommitMessage(array $fields) {
self::selectFields($fields, array(
'ReleephCommitMessageFieldSpecification',
'ReleephRequestorFieldSpecification',
'ReleephIntentFieldSpecification',
'ReleephReasonFieldSpecification',
));
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* Control the rendering of ReleephRequestHeaderView, and the layout of the
* ReleephRequest search dialog (in ReleephBranchViewController.)
*/
abstract class ReleephFieldSelector {
final public function __construct() {
// <empty>
}
abstract public function getFieldSpecifications();
abstract public function arrangeFieldsForHeaderView(array $fields);
abstract public function arrangeFieldsForSelectForm(array $fields);
public function sortFieldsForCommitMessage(array $fields) {
assert_instances_of($fields, 'ReleephFieldSpecification');
return $fields;
}
protected static function selectFields(array $fields, array $classes) {
assert_instances_of($fields, 'ReleephFieldSpecification');
$map = array();
foreach ($fields as $field) {
$map[get_class($field)] = $field;
}
$result = array();
foreach ($classes as $class) {
$field = idx($map, $class);
if (!$field) {
throw new Exception(
"Tried to select a in instance of '{$class}' but that field ".
"is not configured for this project!");
}
if (idx($result, $class)) {
throw new Exception(
"You have asked to select the field '{$class}' ".
"more than once!");
}
$result[$class] = $field;
}
return $result;
}
}

View file

@ -0,0 +1,39 @@
<?php
final class ReleephAuthorFieldSpecification
extends ReleephFieldSpecification {
private static $authorMap = array();
public function bulkLoad(array $releeph_requests) {
foreach ($releeph_requests as $releeph_request) {
$commit = $releeph_request->loadPhabricatorRepositoryCommit();
if ($commit) {
$author_phid = $commit->getAuthorPHID();
self::$authorMap[$releeph_request->getPHID()] = $author_phid;
}
}
ReleephUserView::getNewInstance()
->setUser($this->getUser())
->setReleephProject($this->getReleephProject())
->load(self::$authorMap);
}
public function getName() {
return 'Author';
}
public function renderValueForHeaderView() {
$rr = $this->getReleephRequest();
$author_phid = idx(self::$authorMap, $rr->getPHID());
if ($author_phid) {
return ReleephUserView::getNewInstance()
->setRenderUserPHID($author_phid)
->render();
} else {
return 'Unknown Author';
}
}
}

View file

@ -0,0 +1,30 @@
<?php
final class ReleephBranchCommitFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Commit';
}
public function renderValueForHeaderView() {
$rr = $this->getReleephRequest();
if (!$rr->getInBranch()) {
return null;
}
$c_phid = $rr->getCommitPHID();
$c_id = $rr->getCommitIdentifier();
if ($c_phid) {
$handles = $rr->getHandles();
$val = $handles[$c_phid]->renderLink();
} else if ($c_id) {
$val = $c_id;
} else {
$val = '???';
}
return $val;
}
}

View file

@ -0,0 +1,46 @@
<?php
final class ReleephCommitMessageFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return '__only_for_commit_message!';
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function renderLabelForCommitMessage() {
return $this->renderCommonLabel();
}
public function renderValueForCommitMessage() {
return $this->renderCommonValue(
DifferentialReleephRequestFieldSpecification::ACTION_PICKS);
}
public function shouldAppearOnRevertMessage() {
return true;
}
public function renderLabelForRevertMessage() {
return $this->renderCommonLabel();
}
public function renderValueForRevertMessage() {
return $this->renderCommonValue(
DifferentialReleephRequestFieldSpecification::ACTION_REVERTS);
}
private function renderCommonLabel() {
return id(new DifferentialReleephRequestFieldSpecification())
->renderLabelForCommitMessage();
}
private function renderCommonValue($action) {
$rq = 'RQ'.$this->getReleephRequest()->getID();
return "{$action} {$rq}";
}
}

View file

@ -0,0 +1,77 @@
<?php
final class ReleephDiffChurnFieldSpecification
extends ReleephFieldSpecification {
const REJECTIONS_WEIGHT = 30;
const COMMENTS_WEIGHT = 7;
const UPDATES_WEIGHT = 10;
const MAX_POINTS = 100;
public function getName() {
return 'Churn';
}
public function renderValueForHeaderView() {
$diff_rev = $this->getReleephRequest()->loadDifferentialRevision();
if (!$diff_rev) {
return null;
}
$diff_rev = $this->getReleephRequest()->loadDifferentialRevision();
$comments = $diff_rev->loadRelatives(
new DifferentialComment(),
'revisionID');
$counts = array();
foreach ($comments as $comment) {
$action = $comment->getAction();
if (!isset($counts[$action])) {
$counts[$action] = 0;
}
$counts[$action] += 1;
}
// 'none' action just means a plain comment
$comments = idx($counts, 'none', 0);
$rejections = idx($counts, 'reject', 0);
$updates = idx($counts, 'update', 0);
$points =
self::REJECTIONS_WEIGHT * $rejections +
self::COMMENTS_WEIGHT * $comments +
self::UPDATES_WEIGHT * $updates;
if ($points === 0) {
$points = 0.15 * self::MAX_POINTS;
$blurb = 'Silent diff';
} else {
$parts = array();
if ($rejections) {
$parts[] = pht('%d rejection(s)', $rejections);
}
if ($comments) {
$parts[] = pht('%d comment(s)', $comments);
}
if ($updates) {
$parts[] = pht('%d update(s)', $updates);
}
if (count($parts) === 0) {
$blurb = '';
} else if (count($parts) === 1) {
$blurb = head($parts);
} else {
$last = array_pop($parts);
$blurb = implode(', ', $parts).' and '.$last;
}
}
return id(new AphrontProgressBarView())
->setValue($points)
->setMax(self::MAX_POINTS)
->setCaption($blurb)
->render();
}
}

View file

@ -0,0 +1,37 @@
<?php
final class ReleephDiffMessageFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Message';
}
public function renderLabelForHeaderView() {
return null;
}
public function renderValueForHeaderView() {
$commit_data = $this
->getReleephRequest()
->loadPhabricatorRepositoryCommitData();
if (!$commit_data) {
return '';
}
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $this->getUser());
$markup = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->markupText($commit_data->getCommitMessage()));
return id(new AphrontNoteView())
->setTitle('Commit Message')
->appendChild($markup)
->render();
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* While this class could take advantage of bulkLoad(), in practice
* loadRelatives fixes all that for us.
*/
final class ReleephDiffSizeFieldSpecification
extends ReleephFieldSpecification {
const LINES_WEIGHT = 1;
const PATHS_WEIGHT = 30;
const MAX_POINTS = 1000;
public function getName() {
return 'Size';
}
public function renderValueForHeaderView() {
$diff_rev = $this->getReleephRequest()->loadDifferentialRevision();
if (!$diff_rev) {
return '';
}
$diffs = $diff_rev->loadRelatives(
new DifferentialDiff(),
'revisionID',
'getID',
'creationMethod <> "commit"');
$all_changesets = array();
$most_recent_changesets = null;
foreach ($diffs as $diff) {
$changesets = $diff->loadRelatives(new DifferentialChangeset(), 'diffID');
$all_changesets += $changesets;
$most_recent_changesets = $changesets;
}
// The score is based on all changesets for all versions of this diff
$all_changes = $this->countLinesAndPaths($all_changesets);
$points =
self::LINES_WEIGHT * $all_changes['code']['lines'] +
self::PATHS_WEIGHT * count($all_changes['code']['paths']);
// The blurb is just based on the most recent version of the diff
$mr_changes = $this->countLinesAndPaths($most_recent_changesets);
$test_tag = '';
if ($mr_changes['tests']['paths']) {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
$test_blurb =
pht('%d line(s)', $mr_changes['tests']['lines']).' and '.
pht('%d path(s)', count($mr_changes['tests']['paths'])).
" contain changes to test code:\n";
foreach ($mr_changes['tests']['paths'] as $mr_test_path) {
$test_blurb .= pht("%s\n", $mr_test_path);
}
$test_tag = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $test_blurb,
'align' => 'E',
'size' => 'auto'),
'style' => ''),
' + tests');
}
$blurb = hsprintf("%s%s.",
pht('%d line(s)', $mr_changes['code']['lines']).' and '.
pht('%d path(s)', count($mr_changes['code']['paths'])).' over '.
pht('%d diff(s)', count($diffs)),
$test_tag);
return id(new AphrontProgressBarView())
->setValue($points)
->setMax(self::MAX_POINTS)
->setCaption($blurb)
->render();
}
private function countLinesAndPaths(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$lines = 0;
$paths_touched = array();
$test_lines = 0;
$test_paths_touched = array();
foreach ($changesets as $ch) {
if ($this->getReleephProject()->isTestFile($ch->getFilename())) {
$test_lines += $ch->getAddLines() + $ch->getDelLines();
$test_paths_touched[] = $ch->getFilename();
} else {
$lines += $ch->getAddLines() + $ch->getDelLines();
$paths_touched[] = $ch->getFilename();
}
}
return array(
'code' => array(
'lines' => $lines,
'paths' => array_unique($paths_touched),
),
'tests' => array(
'lines' => $test_lines,
'paths' => array_unique($test_paths_touched),
)
);
}
}

View file

@ -0,0 +1,359 @@
<?php
abstract class ReleephFieldSpecification {
abstract public function getName();
/* -( Storage )------------------------------------------------------------ */
public function getStorageKey() {
return null;
}
final public function isEditable() {
return $this->getStorageKey() !== null;
}
/**
* This will be called many times if you are using **Selecting**. In
* particular, for N selecting fields, selectReleephRequests() is called
* N-squared times, each time for R ReleephRequests.
*/
final public function getValue() {
$key = $this->getRequiredStorageKey();
return $this->getReleephRequest()->getDetail($key);
}
final public function setValue($value) {
$key = $this->getRequiredStorageKey();
return $this->getReleephRequest()->setDetail($key, $value);
}
/**
* @throws ReleephFieldParseException, to show an error.
*/
public function validate($value) {
return;
}
/* -( Header View )-------------------------------------------------------- */
/**
* Return a label for use in rendering the fields table. If you return null,
* the renderLabelForHeaderView data will span both columns.
*/
public function renderLabelForHeaderView() {
return $this->getName();
}
public function renderValueForHeaderView() {
$key = $this->getRequiredStorageKey();
return $this->getReleephRequest()->getDetail($key);
}
/* -( Edit View )---------------------------------------------------------- */
public function renderEditControl(AphrontRequest $request) {
throw new ReleephFieldSpecificationIncompleteException($this);
}
public function setValueFromAphrontRequest(AphrontRequest $request) {
$data = $request->getRequestData();
$value = idx($data, $this->getRequiredStorageKey());
$this->validate($value);
$this->setValue($value);
}
/* -( Conduit )------------------------------------------------------------ */
public function getKeyForConduit() {
return $this->getRequiredStorageKey();
}
public function getValueForConduit() {
return $this->getValue();
}
public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) {
$value = idx(
$request->getValue('fields', array()),
$this->getRequiredStorageKey());
$this->validate($value);
$this->setValue($value);
}
/* -( Arcanist )----------------------------------------------------------- */
public function renderHelpForArcanist() {
return '';
}
/* -( Context )------------------------------------------------------------ */
private $releephProject;
private $releephBranch;
private $releephRequest;
private $user;
final public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
final public function setReleephBranch(ReleephBranch $rb) {
$this->releephRequest = $rb;
return $this;
}
final public function setReleephRequest(ReleephRequest $rr) {
$this->releephRequest = $rr;
return $this;
}
final public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
final public function getReleephProject() {
return $this->releephProject;
}
final public function getReleephBranch() {
return $this->releephBranch;
}
final public function getReleephRequest() {
return $this->releephRequest;
}
final public function getUser() {
return $this->user;
}
/* -( Bulk loading )------------------------------------------------------- */
public function bulkLoad(array $releeph_requests) {
}
/* -( Selecting )---------------------------------------------------------- */
/**
* Append select controls to the given form.
*
* You are given:
*
* - the AphrontFormView to append to;
*
* - the AphrontRequest, so you can make use of the value currently selected
* in the form;
*
* - $all_releeph_requests: an array of all the ReleephRequests without any
* selection based filtering; and
*
* - $all_releeph_requests_without_this_field: an array of ReleephRequests
* that have been selected by all the other select controls on this page.
*
* The example in ReleephLevelFieldSpecification shows how to use these.
* $all_releeph_requests lets you find out all the values of a field in all
* ReleephRequests, so you can render controls for every known value.
*
* $all_releeph_requests_without_this_field lets you count how many
* ReleephRequests could be affected by this field's select control, after
* all the other fields have made their selections.
* ReleephLevelFieldSpecification uses this to render a preview count for
* each select button, and disables the button completely (but still renders
* it) if it couldn't possibly select anything.
*/
protected function appendSelectControls(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
return null;
}
/**
* Filter the $releeph_requests using the data you set with your form
* controls, and which is now available in the provided AphrontRequest.
*/
protected function selectReleephRequests(AphrontRequest $request,
array &$releeph_requests) {
return null;
}
/**
* If you have PHIDs that can be used in an AphrontFormTokenizerControl,
* return true here, return the PHIDs in getSelectablePHIDs(), and return the
* URL the Tokenizer should use for the form control in
* getSelectTokenizerDatasource().
*
* This is a cheap alternative to implementing appendSelectControls() and
* selectReleephRequests() in full.
*/
protected function hasSelectablePHIDs() {
return false;
}
protected function getSelectablePHIDs() {
throw new ReleephFieldSpecificationIncompleteException($this);
}
protected function getSelectTokenizerDatasource() {
throw new ReleephFieldSpecificationIncompleteException($this);
}
/* -( Commit Messages )---------------------------------------------------- */
public function shouldAppearOnCommitMessage() {
return false;
}
public function renderLabelForCommitMessage() {
throw new ReleephFieldSpecificationIncompleteException($this);
}
public function renderValueForCommitMessage() {
throw new ReleephFieldSpecificationIncompleteException($this);
}
public function shouldAppearOnRevertMessage() {
return false;
}
public function renderLabelForRevertMessage() {
return $this->renderLabelForCommitMessage();
}
public function renderValueForRevertMessage() {
return $this->renderValueForCommitMessage();
}
/* -( Implementation )----------------------------------------------------- */
protected function getRequiredStorageKey() {
$key = $this->getStorageKey();
if ($key === null) {
throw new ReleephFieldSpecificationIncompleteException($this);
}
if (strpos($key, '.') !== false) {
/**
* Storage keys are reused for form controls, and periods in form control
* names break HTML forms.
*/
throw new Exception(
"You can't use '.' in storage keys!");
}
return $key;
}
/**
* The "hook" functions ##appendSelectControlsHook()## and
* ##selectReleephRequestsHook()## are used with ##hasSelectablePHIDs()##, to
* use the tokenizing helpers if ##hasSelectablePHIDs()## returns true.
*/
public function appendSelectControlsHook(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
if ($this->hasSelectablePHIDs()) {
$this->appendTokenizingSelectControl(
$form,
$request,
$all_releeph_requests,
$all_releeph_requests_without_this_field);
} else {
$this->appendSelectControls(
$form,
$request,
$all_releeph_requests,
$all_releeph_requests_without_this_field);
}
}
// See above
public function selectReleephRequestsHook(AphrontRequest $request,
array &$releeph_requests) {
if ($this->hasSelectablePHIDs()) {
$this->selectReleephRequestsFromTokens(
$request,
$releeph_requests);
} else {
$this->selectReleephRequests(
$request,
$releeph_requests);
}
}
private function appendTokenizingSelectControl(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
$key = urlencode(strtolower($this->getName()));
$selected_phids = $request->getArr($key);
$handles = id(new PhabricatorObjectHandleData($selected_phids))
->setViewer($request->getUser())
->loadHandles();
$tokens = array();
foreach ($selected_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$datasource = $this->getSelectTokenizerDatasource();
$control =
id(new AphrontFormTokenizerControl())
->setDatasource($datasource)
->setName($key)
->setLabel($this->getName())
->setValue($tokens);
$form->appendChild($control);
}
private function selectReleephRequestsFromTokens(AphrontRequest $request,
array &$releeph_requests) {
$key = urlencode(strtolower($this->getName()));
$selected_phids = $request->getArr($key);
if (!$selected_phids) {
return;
}
$selected_phid_lookup = array();
foreach ($selected_phids as $phid) {
$selected_phid_lookup[$phid] = $phid;
}
$filtered = array();
foreach ($releeph_requests as $releeph_request) {
$rq_phids = $this
->setReleephRequest($releeph_request)
->getSelectablePHIDs();
foreach ($rq_phids as $rq_phid) {
if (idx($selected_phid_lookup, $rq_phid)) {
$filtered[] = $releeph_request;
break;
}
}
}
$releeph_requests = $filtered;
}
}

View file

@ -0,0 +1,81 @@
<?php
final class ReleephIntentFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Intent';
}
public function renderValueForHeaderView() {
return id(new ReleephRequestIntentsView())
->setReleephRequest($this->getReleephRequest())
->setReleephProject($this->getReleephProject())
->render();
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnRevertMessage() {
return true;
}
public function renderLabelForCommitMessage() {
return "Approved By";
}
public function renderLabelForRevertMessage() {
return "Rejected By";
}
public function renderValueForCommitMessage() {
return $this->renderIntentsForCommitMessage(ReleephRequest::INTENT_WANT);
}
public function renderValueForRevertMessage() {
return $this->renderIntentsForCommitMessage(ReleephRequest::INTENT_PASS);
}
private function renderIntentsForCommitMessage($print_intent) {
$intents = $this->getReleephRequest()->getUserIntents();
$requestor = $this->getReleephRequest()->getRequestUserPHID();
$pusher_phids = $this->getReleephProject()->getPushers();
$phids = array_unique($pusher_phids + array_keys($intents));
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($this->getUser())
->loadHandles();
$tokens = array();
foreach ($phids as $phid) {
$intent = idx($intents, $phid);
if ($intent == $print_intent) {
$name = $handles[$phid]->getName();
$is_pusher = in_array($phid, $pusher_phids);
$is_requestor = $phid == $requestor;
if ($is_pusher) {
if ($is_requestor) {
$token = "{$name} (pusher and requestor)";
} else {
$token = "{$name} (pusher)";
}
} else {
if ($is_requestor) {
$token = "{$name} (requestor)";
} else {
$token = $name;
}
}
$tokens[] = $token;
}
}
return implode(', ', $tokens);
}
}

View file

@ -0,0 +1,228 @@
<?php
/**
* Provides a convenient field for storing a set of levels that you can use to
* filter requests on.
*
* Levels are rendered with names and descriptions in the edit UI, and are
* automatically documented via the "arc request" interface.
*
* See ReleephSeverityFieldSpecification for an example.
*/
abstract class ReleephLevelFieldSpecification
extends ReleephFieldSpecification {
private $error;
abstract public function getLevels();
abstract public function getDefaultLevel();
abstract public function getNameForLevel($level);
abstract public function getDescriptionForLevel($level);
/**
* Use getCanonicalLevel() to convert old, unsupported levels to new ones.
*/
protected function getCanonicalLevel($misc_level) {
return $misc_level;
}
public function getStorageKey() {
$class = get_class($this);
throw new ReleephFieldSpecificationIncompleteException(
$this,
"You must implement getStorageKey() for children of {$class}!");
}
public function renderValueForHeaderView() {
$raw_level = $this->getValue();
$level = $this->getCanonicalLevel($raw_level);
return $this->getNameForLevel($level);
}
public function renderEditControl(AphrontRequest $request) {
$control_name = $this->getRequiredStorageKey();
$all_levels = $this->getLevels();
$level = $request->getStr($control_name);
if (!$level) {
$level = $this->getCanonicalLevel($this->getValue());
}
if (!$level) {
$level = $this->getDefaultLevel();
}
$control = id(new AphrontFormRadioButtonControl())
->setLabel('Level')
->setName($control_name)
->setValue($level);
if ($this->error) {
$control->setError($this->error);
} elseif ($this->getDefaultLevel()) {
$control->setError(true);
}
foreach ($all_levels as $level) {
$name = $this->getNameForLevel($level);
$description = $this->getDescriptionForLevel($level);
$control->addButton($level, $name, $description);
}
return $control;
}
public function renderHelpForArcanist() {
$text = '';
$levels = $this->getLevels();
$default = $this->getDefaultLevel();
foreach ($levels as $level) {
$name = $this->getNameForLevel($level);
$description = $this->getDescriptionForLevel($level);
$default_marker = ' ';
if ($level === $default) {
$default_marker = '*';
}
$text .= " {$default_marker} **{$name}**\n";
$text .= phutil_console_wrap($description."\n", 8);
}
return $text;
}
public function validate($value) {
if ($value === null) {
$this->error = 'Required';
$label = $this->getName();
throw new ReleephFieldParseException(
$this,
"You must provide a {$label} level");
}
$levels = $this->getLevels();
if (!in_array($value, $levels)) {
$label = $this->getName();
throw new ReleephFieldParseException(
$this,
"Level '{$value}' is not a valid {$label} level in this project.");
}
}
public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) {
$key = $this->getRequiredStorageKey();
$label = $this->getName();
$name = idx($request->getValue('fields', array()), $key);
if (!$name) {
$level = $this->getDefaultLevel();
if (!$level) {
throw new ReleephFieldParseException(
$this,
"No value given for {$label}, ".
"and no default is given for this level!");
}
} else {
$level = $this->getLevelByName($name);
}
if (!$level) {
throw new ReleephFieldParseException(
$this,
"Unknown {$label} level name '{$name}'");
}
$this->setValue($level);
}
private $nameMap = array();
public function getLevelByName($name) {
// Build this once
if (!$this->nameMap) {
foreach ($this->getLevels() as $level) {
$level_name = $this->getNameForLevel($level);
$this->nameMap[$level_name] = $level;
}
}
return idx($this->nameMap, $name);
}
protected function appendSelectControls(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
$buttons = array(null => 'All');
// Add in known level/names
foreach ($this->getLevels() as $level) {
$name = $this->getNameForLevel($level);
$buttons[$name] = $name;
}
// Add in any names we've seen in the wild, as well.
foreach ($all_releeph_requests as $releeph_request) {
$raw_level = $this->setReleephRequest($releeph_request)->getValue();
if (!$raw_level) {
// The ReleephRequest might not have a level set
continue;
}
$level = $this->getCanonicalLevel($raw_level);
$name = $this->getNameForLevel($level);
$buttons[$name] = $name;
}
$key = $this->getRequiredStorageKey();
$current = $request->getStr($key);
$counters = array(null => count($all_releeph_requests_without_this_field));
foreach ($all_releeph_requests_without_this_field as $releeph_request) {
$raw_level = $this->setReleephRequest($releeph_request)->getValue();
if (!$raw_level) {
// The ReleephRequest might not have a level set
continue;
}
$level = $this->getCanonicalLevel($raw_level);
$name = $this->getNameForLevel($level);
if (!isset($counters[$name])) {
$counters[$name] = 0;
}
$counters[$name]++;
}
$control = id(new AphrontFormCountedToggleButtonsControl())
->setLabel($this->getName())
->setValue($current)
->setBaseURI($request->getRequestURI(), $key)
->setButtons($buttons)
->setCounters($counters);
$form
->appendChild($control)
->addHiddenInput($key, $current);
}
protected function selectReleephRequests(AphrontRequest $request,
array &$releeph_requests) {
$key = $this->getRequiredStorageKey();
$current = $request->getStr($key);
if (!$current) {
return;
}
$filtered = array();
foreach ($releeph_requests as $releeph_request) {
$raw_level = $this->setReleephRequest($releeph_request)->getValue();
$level = $this->getCanonicalLevel($raw_level);
$name = $this->getNameForLevel($level);
if ($name == $current) {
$filtered[] = $releeph_request;
}
}
$releeph_requests = $filtered;
}
}

View file

@ -0,0 +1,16 @@
<?php
final class ReleephOriginalCommitFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Commit';
}
public function renderValueForHeaderView() {
$rr = $this->getReleephRequest();
$handles = $rr->getHandles();
return $handles[$rr->getRequestCommitPHID()]->renderLink();
}
}

View file

@ -0,0 +1,78 @@
<?php
final class ReleephReasonFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Reason';
}
public function getStorageKey() {
return 'reason';
}
public function renderLabelForHeaderView() {
return null;
}
public function renderValueForHeaderView() {
$reason = $this->getValue();
if (!$reason) {
return '';
}
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $this->getUser());
$markup = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->markupText($reason));
return id(new AphrontNoteView())
->setTitle('Reason')
->appendChild($markup)
->render();
}
private $error = true;
public function renderEditControl(AphrontRequest $request) {
$reason = $request->getStr('reason', $this->getValue());
return id(new AphrontFormTextAreaControl())
->setLabel('Reason')
->setName('reason')
->setError($this->error)
->setValue($reason);
}
public function validate($reason) {
if (!$reason) {
$this->error = 'Required';
throw new ReleephFieldParseException(
$this,
"You must give a reason for your request.");
}
}
public function renderHelpForArcanist() {
$text =
"Fully explain why you are requesting this code be included ".
"in the next release.\n";
return phutil_console_wrap($text, 8);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Request Reason';
}
public function renderValueForCommitMessage() {
return $this->getValue();
}
}

View file

@ -0,0 +1,59 @@
<?php
final class ReleephRequestorFieldSpecification
extends ReleephFieldSpecification {
public function bulkLoad(array $releeph_requests) {
$phids = mpull($releeph_requests, 'getRequestUserPHID');
ReleephUserView::getNewInstance()
->setUser($this->getUser())
->setReleephProject($this->getReleephProject())
->load($phids);
}
public function getName() {
return 'Requestor';
}
public function renderValueForHeaderView() {
$phid = $this->getReleephRequest()->getRequestUserPHID();
return ReleephUserView::getNewInstance()
->setRenderUserPHID($phid)
->render();
}
public function hasSelectablePHIDs() {
return true;
}
public function getSelectTokenizerDatasource() {
return '/typeahead/common/users/';
}
public function getSelectablePHIDs() {
return array(
$this->getReleephRequest()->getRequestUserPHID(),
);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnRevertMessage() {
return true;
}
public function renderLabelForCommitMessage() {
return "Requested By";
}
public function renderValueForCommitMessage() {
$phid = $this->getReleephRequest()->getRequestUserPHID();
$handles = id(new PhabricatorObjectHandleData(array($phid)))
->setViewer($this->getUser())
->loadHandles();
return $handles[$phid]->getName();
}
}

View file

@ -0,0 +1,37 @@
<?php
final class ReleephRevisionFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Revision';
}
public function renderValueForHeaderView() {
$data = $this
->getReleephRequest()
->loadPhabricatorRepositoryCommitData();
if (!$data) {
return null;
}
$phid = $data->getCommitDetail('differential.revisionPHID');
if (!$phid) {
return null;
}
$handles = $this->getReleephRequest()->getHandles();
$handle = $handles[$phid];
$link = $handle
// Hack to remove the strike-through rendering of diff links
->setStatus(null)
->renderLink();
return phutil_tag(
'div',
array(
'class' => 'releeph-header-text-truncated',
),
$link);
}
}

View file

@ -0,0 +1,63 @@
<?php
final class ReleephRiskFieldSpecification
extends ReleephFieldSpecification {
static $defaultRisks = array(
'NONE' => 'Completely safe to pick this request.',
'SOME' => 'There is some risk this could break things, but not much.',
'HIGH' => 'This is pretty risky, but is also very important.',
);
public function getName() {
return 'Riskiness';
}
public function getStorageKey() {
return 'risk';
}
public function renderLabelForHeaderView() {
return 'Riskiness';
}
private $error = true;
public function renderEditControl(AphrontRequest $request) {
$value = $request->getStr('risk', $this->getValue());
$buttons = id(new AphrontFormRadioButtonControl())
->setLabel('Riskiness')
->setName('risk')
->setError($this->error)
->setValue($value);
foreach (self::$defaultRisks as $value => $description) {
$buttons->addButton($value, $value, $description);
}
return $buttons;
}
public function validate($risk) {
if (!$risk) {
$this->error = 'Required';
throw new ReleephFieldParseException(
$this,
"No risk was given, which probably means we've changed the set ".
"of valid risks since you made this request. Please pick one.");
}
if (!idx(self::$defaultRisks, $risk)) {
throw new ReleephFieldParseException(
$this,
"Unknown risk '{$risk}'.");
}
}
public function renderHelpForArcanist() {
$help = '';
foreach (self::$defaultRisks as $name => $description) {
$help .= " **{$name}**\n";
$help .= phutil_console_wrap($description."\n", 8);
}
return $help;
}
}

View file

@ -0,0 +1,46 @@
<?php
final class ReleephSeverityFieldSpecification
extends ReleephLevelFieldSpecification {
const HOTFIX = 'HOTFIX';
const RELEASE = 'RELEASE';
public function getName() {
return 'Severity';
}
public function getStorageKey() {
return 'releeph:severity';
}
public function getLevels() {
return array(
self::HOTFIX,
self::RELEASE,
);
}
public function getDefaultLevel() {
return self::RELEASE;
}
public function getNameForLevel($level) {
static $names = array(
self::HOTFIX => 'HOTFIX',
self::RELEASE => 'RELEASE',
);
return idx($names, $level, $level);
}
public function getDescriptionForLevel($level) {
static $descriptions = array(
self::HOTFIX =>
'Needs merging and fixing right now.',
self::RELEASE =>
'Required for the currently rolling release.',
);
return idx($descriptions, $level);
}
}

View file

@ -0,0 +1,89 @@
<?php
final class ReleephStatusFieldSpecification
extends ReleephFieldSpecification {
public function getName() {
return 'Status';
}
public function renderValueForHeaderView() {
return id(new ReleephRequestStatusView())
->setReleephRequest($this->getReleephRequest())
->render();
}
private static $filters = array(
'req' => ReleephRequest::STATUS_REQUESTED,
'app' => ReleephRequest::STATUS_NEEDS_PICK,
'rej' => ReleephRequest::STATUS_REJECTED,
'abn' => ReleephRequest::STATUS_ABANDONED,
'mer' => ReleephRequest::STATUS_PICKED,
'rrq' => ReleephRequest::STATUS_NEEDS_REVERT,
'rev' => ReleephRequest::STATUS_REVERTED,
);
protected function appendSelectControls(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
$filter_names = array(
null => 'All',
);
foreach (self::$filters as $code => $status) {
$name = ReleephRequest::getStatusDescriptionFor($status);
$filter_names[$code] = $name;
}
$key = 'status';
$code = $request->getStr($key);
$current_status = idx(self::$filters, $code);
$codes = array_flip(self::$filters);
$counters = array(null => count($all_releeph_requests_without_this_field));
foreach ($all_releeph_requests_without_this_field as $releeph_request) {
$this_status = $releeph_request->getStatus();
$this_code = idx($codes, $this_status);
if (!isset($counters[$this_code])) {
$counters[$this_code] = 0;
}
$counters[$this_code]++;
}
$control = id(new AphrontFormCountedToggleButtonsControl())
->setLabel($this->getName())
->setValue($code)
->setBaseURI($request->getRequestURI(), $key)
->setButtons($filter_names)
->setCounters($counters);
$form
->appendChild($control)
->addHiddenInput($key, $code);
}
protected function selectReleephRequests(AphrontRequest $request,
array &$releeph_requests) {
$key = 'status';
$code = $request->getStr($key);
if (!$code) {
return;
}
$current_status = idx(self::$filters, $code);
$filtered = array();
foreach ($releeph_requests as $releeph_request) {
if ($releeph_request->getStatus() == $current_status) {
$filtered[] = $releeph_request;
}
}
$releeph_requests = $filtered;
}
}

View file

@ -0,0 +1,46 @@
<?php
final class ReleephSummaryFieldSpecification
extends ReleephFieldSpecification {
const MAX_SUMMARY_LENGTH = 60;
public function getName() {
return 'Summary';
}
public function getStorageKey() {
return 'summary';
}
private $error = false;
public function renderEditControl(AphrontRequest $request) {
$summary = $request->getStr('summary', $this->getValue());
return id(new AphrontFormTextControl())
->setLabel('Summary')
->setName('summary')
->setError($this->error)
->setValue($summary)
->setCaption(
'Leave this blank to use the original commit title');
}
public function renderHelpForArcanist() {
$text =
"A one-line title summarizing this request. ".
"Leave blank to use the original commit title.\n";
return phutil_console_wrap($text, 8);
}
public function validate($summary) {
if ($summary && strlen($summary) > self::MAX_SUMMARY_LENGTH) {
$this->error = 'Too long!';
throw new ReleephFieldParseException(
$this, sprintf(
'Please keep your summary to under %d characters.',
self::MAX_SUMMARY_LENGTH));
}
}
}

View file

@ -0,0 +1,154 @@
<?php
final class ReleephBranch extends ReleephDAO {
protected $phid;
protected $releephProjectID;
protected $isActive;
protected $createdByUserPHID;
// The immutable name of this branch ('releases/foo-2013.01.24')
protected $name;
protected $basename;
// The symbolic name of this branch (LATEST, PRODUCTION, RC, ...)
// See SYMBOLIC_NAME_NOTE below
protected $symbolicName;
// Where to cut the branch
protected $cutPointCommitIdentifier;
protected $cutPointCommitPHID;
protected $details = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
ReleephPHIDConstants::PHID_TYPE_REBR);
}
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function willWriteData(array &$data) {
// If symbolicName is omitted, set it to the basename.
//
// This means that we can enforce symbolicName as a UNIQUE column in the
// DB. We'll interpret symbolicName === basename as meaning "no symbolic
// name".
//
// SYMBOLIC_NAME_NOTE
if (!$data['symbolicName']) {
$data['symbolicName'] = $data['basename'];
}
parent::willWriteData($data);
}
public function getSymbolicName() {
// See SYMBOLIC_NAME_NOTE above for why this is needed
if ($this->symbolicName == $this->getBasename()) {
return '';
}
return $this->symbolicName;
}
public function setSymbolicName($name) {
if ($name) {
parent::setSymbolicName($name);
} else {
parent::setSymbolicName($this->getBasename());
}
return $this;
}
public function getDisplayName() {
if ($sn = $this->getSymbolicName()) {
return $sn;
}
return $this->getBasename();
}
public function getDisplayNameWithDetail() {
$n = $this->getBasename();
if ($sn = $this->getSymbolicName()) {
return "{$sn} ({$n})";
} else {
return $n;
}
}
public function getURI($path = null) {
$components = array(
'/releeph',
rawurlencode($this->loadReleephProject()->getName()),
rawurlencode($this->getBasename()),
$path
);
return PhabricatorEnv::getProductionURI(implode('/', $components));
}
public function loadReleephProject() {
return $this->loadOneRelative(
new ReleephProject(),
'id',
'getReleephProjectID');
}
private function loadReleephRequestHandles(PhabricatorUser $user, $reqs) {
$phids_to_phetch = array();
foreach ($reqs as $rr) {
$phids_to_phetch[] = $rr->getRequestCommitPHID();
$phids_to_phetch[] = $rr->getRequestUserPHID();
$phids_to_phetch[] = $rr->getCommitPHID();
$intents = $rr->getUserIntents();
if ($intents) {
foreach ($intents as $user_phid => $intent) {
$phids_to_phetch[] = $user_phid;
}
}
$request_commit = $rr->loadPhabricatorRepositoryCommit();
if ($request_commit) {
$phids_to_phetch[] = $request_commit->getAuthorPHID();
$phids_to_phetch[] = $rr->loadRequestCommitDiffPHID();
}
}
$handles = id(new PhabricatorObjectHandleData($phids_to_phetch))
->setViewer($user)
->loadHandles();
return $handles;
}
public function populateReleephRequestHandles(PhabricatorUser $user, $reqs) {
$handles = $this->loadReleephRequestHandles($user, $reqs);
foreach ($reqs as $req) {
$req->setHandles($handles);
}
}
public function loadReleephRequests(PhabricatorUser $user) {
$reqs = $this->loadRelatives(new ReleephRequest(), 'branchID');
$this->populateReleephRequestHandles($user, $reqs);
return $reqs;
}
public function isActive() {
return $this->getIsActive();
}
}

View file

@ -0,0 +1,9 @@
<?php
abstract class ReleephDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'releeph';
}
}

View file

@ -0,0 +1,176 @@
<?php
final class ReleephProject extends ReleephDAO {
const DEFAULT_BRANCH_NAMESPACE = 'releeph-releases';
const SYSTEM_AGENT_USERNAME_PREFIX = 'releeph-agent-';
const COMMIT_AUTHOR_NONE = 'commit-author-none';
const COMMIT_AUTHOR_FROM_DIFF = 'commit-author-is-from-diff';
const COMMIT_AUTHOR_REQUESTOR = 'commit-author-is-requestor';
protected $phid;
protected $name;
// Specifying the place to pick from is a requirement for svn, though not
// for git. It's always useful though for reasoning about what revs have
// been picked and which haven't.
protected $trunkBranch;
protected $repositoryID;
protected $repositoryPHID;
protected $isActive;
protected $createdByUserPHID;
protected $arcanistProjectID;
protected $projectID;
protected $details = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
ReleephPHIDConstants::PHID_TYPE_REPR);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function getURI($path = null) {
$components = array(
'/releeph/project',
$this->getID(),
$path
);
return PhabricatorEnv::getProductionURI(implode('/', $components));
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function willSaveObject() {
// Do this first, to generate the PHID
parent::willSaveObject();
$banned_names = $this->getBannedNames();
if (in_array($this->name, $banned_names)) {
throw new Exception(sprintf(
"The name '%s' is in the list of banned project names!",
$this->name,
implode(', ', $banned_names)));
}
if (!$this->getDetail('releaseCounter')) {
$this->setDetail('releaseCounter', 0);
}
}
public function loadPhabricatorProject() {
if ($id = $this->getProjectID()) {
return id(new PhabricatorProject())->load($id);
}
return id(new PhabricatorProject())->makeEphemeral(); // dummy
}
public function loadArcanistProject() {
return $this->loadOneRelative(
new PhabricatorRepositoryArcanistProject(),
'id',
'getArcanistProjectID');
}
public function getPushers() {
return $this->getDetail('pushers', array());
}
public function isPusherPHID($phid) {
$pusher_phids = $this->getDetail('pushers', array());
return in_array($phid, $pusher_phids);
}
public function isPusher(PhabricatorUser $user) {
return $this->isPusherPHID($user->getPHID());
}
public function loadPhabricatorRepository() {
return $this->loadOneRelative(
new PhabricatorRepository(),
'id',
'getRepositoryID');
}
public function getCurrentReleaseNumber() {
$current_release_numbers = array();
// From the project...
$current_release_numbers[] = $this->getDetail('releaseCounter', 0);
// From any branches...
$branches = id(new ReleephBranch())->loadAllWhere(
'releephProjectID = %d', $this->getID());
if ($branches) {
$release_numbers = array();
foreach ($branches as $branch) {
$current_release_numbers[] = $branch->getDetail('releaseNumber', 0);
}
}
return max($current_release_numbers);
}
public function getReleephFieldSelector() {
$class = $this->getDetail('field_selector');
if (!$class) {
$key = 'releeph.field-selector';
$class = PhabricatorEnv::getEnvConfig($key);
}
if ($class) {
return newv($class, array());
} else {
return new ReleephDefaultFieldSelector();
}
}
/**
* Wrapper to setIsActive() that logs who deactivated a project
*/
public function deactivate(PhabricatorUser $actor) {
return $this
->setIsActive(0)
->setDetail('last_deactivated_user', $actor->getPHID())
->setDetail('last_deactivated_time', time());
}
// Hide this from the public
private function setIsActive($v) {
return parent::setIsActive($v);
}
private function getBannedNames() {
return array(
'branch', // no one's tried this... yet!
);
}
public function isTestFile($filename) {
$test_paths = $this->getDetail('testPaths', array());
foreach ($test_paths as $test_path) {
if (preg_match($test_path, $filename)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,309 @@
<?php
final class ReleephRequest extends ReleephDAO {
protected $phid;
protected $branchID;
protected $requestUserPHID;
protected $details = array();
protected $userIntents = array();
protected $inBranch;
protected $pickStatus;
// Information about the thing being requested
protected $requestCommitIdentifier;
protected $requestCommitPHID;
protected $requestCommitOrdinal;
// Information about the last commit to the releeph branch
protected $commitIdentifier;
protected $committedByUserPHID;
protected $commitPHID;
// Pre-populated handles that we'll bulk load in ReleephBranch
private $handles;
/* -( Constants and helper methods )--------------------------------------- */
const INTENT_WANT = 'want';
const INTENT_PASS = 'pass';
const PICK_PENDING = 1; // old
const PICK_FAILED = 2;
const PICK_OK = 3;
const PICK_MANUAL = 4; // old
const REVERT_OK = 5;
const REVERT_FAILED = 6;
const STATUS_REQUESTED = 1;
const STATUS_NEEDS_PICK = 2; // aka approved
const STATUS_REJECTED = 3;
const STATUS_ABANDONED = 4;
const STATUS_PICKED = 5;
const STATUS_REVERTED = 6;
const STATUS_NEEDS_REVERT = 7; // aka revert requested
public function shouldBeInBranch() {
return
$this->getPusherIntent() == self::INTENT_WANT &&
/**
* We use "!= pass" instead of "== want" in case the requestor intent is
* not present. In other words, only revert if the requestor explicitly
* passed.
*/
$this->getRequestorIntent() != self::INTENT_PASS;
}
/**
* Will return INTENT_WANT if any pusher wants this request, and no pusher
* passes on this request.
*/
public function getPusherIntent() {
$project = $this->loadReleephProject();
if (!$project->getPushers()) {
return self::INTENT_WANT;
}
$found_pusher_want = false;
foreach ($this->userIntents as $phid => $intent) {
if ($project->isPusherPHID($phid)) {
if ($intent == self::INTENT_PASS) {
return self::INTENT_PASS;
}
$found_pusher_want = true;
}
}
if ($found_pusher_want) {
return self::INTENT_WANT;
} else {
return null;
}
}
public function getRequestorIntent() {
return idx($this->userIntents, $this->requestUserPHID);
}
public function getStatus() {
return $this->calculateStatus();
}
private function calculateStatus() {
if ($this->shouldBeInBranch()) {
if ($this->getInBranch()) {
return self::STATUS_PICKED;
} else {
return self::STATUS_NEEDS_PICK;
}
} else {
if ($this->getInBranch()) {
return self::STATUS_NEEDS_REVERT;
} else {
$has_been_in_branch = $this->getCommitIdentifier();
// Regardless of why we reverted something, always say reverted if it
// was once in the branch.
if ($has_been_in_branch) {
return self::STATUS_REVERTED;
} elseif ($this->getPusherIntent() === ReleephRequest::INTENT_PASS) {
// Otherwise, if it has never been in the branch, explicitly say why:
return self::STATUS_REJECTED;
} elseif ($this->getRequestorIntent() === ReleephRequest::INTENT_WANT) {
return self::STATUS_REQUESTED;
} else {
return self::STATUS_ABANDONED;
}
}
}
}
public static function getStatusDescriptionFor($status) {
static $descriptions = array(
self::STATUS_REQUESTED => 'Requested',
self::STATUS_REJECTED => 'Rejected',
self::STATUS_ABANDONED => 'Abandoned',
self::STATUS_PICKED => 'Picked',
self::STATUS_REVERTED => 'Reverted',
self::STATUS_NEEDS_PICK => 'Needs Pick',
self::STATUS_NEEDS_REVERT => 'Needs Revert',
);
return idx($descriptions, $status, '??');
}
public static function getStatusClassSuffixFor($status) {
$description = self::getStatusDescriptionFor($status);
$class = str_replace(' ', '-', strtolower($description));
return $class;
}
/* -( Lisk mechanics )----------------------------------------------------- */
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
'userIntents' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
ReleephPHIDConstants::PHID_TYPE_RERQ);
}
/* -( Helpful accessors )--------------------------------------------------- */
public function setHandles($handles) {
$this->handles = $handles;
return $this;
}
public function getHandles() {
if (!$this->handles) {
throw new Exception(
"You must call ReleephBranch::populateReleephRequestHandles() first");
}
return $this->handles;
}
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getReason() {
// Backward compatibility: reason used to be called comments
$reason = $this->getDetail('reason');
if (!$reason) {
return $this->getDetail('comments');
}
return $reason;
}
public function getSummary() {
/**
* Instead, you can use:
* - getDetail('summary') // the actual user-chosen summary
* - getSummaryForDisplay() // falls back to the original commit title
*
* Or for the fastidious:
* - id(new ReleephSummaryFieldSpecification())
* ->setReleephRequest($rr)
* ->getValue() // programmatic equivalent to getDetail()
*/
throw new Exception(
"getSummary() has been deprecated!");
}
/**
* Allow a null summary, and fall back to the title of the commit.
*/
public function getSummaryForDisplay() {
$summary = $this->getDetail('summary');
if (!$summary) {
$pr_commit_data = $this->loadPhabricatorRepositoryCommitData();
if ($pr_commit_data) {
$message_lines = explode("\n", $pr_commit_data->getCommitMessage());
$message_lines = array_filter($message_lines);
$summary = head($message_lines);
}
}
if (!$summary) {
$summary = '(no summary given and commit message empty or unparsed)';
}
return $summary;
}
public function loadRequestCommitDiffPHID() {
$commit_data = $this->loadPhabricatorRepositoryCommitData();
if (!$commit_data) {
return null;
}
return $commit_data->getCommitDetail('differential.revisionPHID');
}
/* -( Loading external objects )------------------------------------------- */
public function loadReleephBranch() {
return $this->loadOneRelative(
new ReleephBranch(),
'id',
'getBranchID');
}
public function loadReleephProject() {
return $this->loadReleephBranch()->loadReleephProject();
}
public function loadEvents() {
return $this->loadRelatives(
new ReleephRequestEvent(),
'releephRequestID',
'getID',
'(1 = 1) ORDER BY dateCreated, id');
}
public function loadPhabricatorRepositoryCommit() {
return $this->loadOneRelative(
new PhabricatorRepositoryCommit(),
'phid',
'getRequestCommitPHID');
}
public function loadPhabricatorRepositoryCommitData() {
return $this->loadOneRelative(
new PhabricatorRepositoryCommitData(),
'commitID',
'getRequestCommitOrdinal');
}
public function loadDifferentialRevision() {
return $this->loadOneRelative(
new DifferentialRevision(),
'phid',
'loadRequestCommitDiffPHID');
}
/* -( State change helpers )----------------------------------------------- */
public function setUserIntent(PhabricatorUser $user, $intent) {
$this->userIntents[$user->getPHID()] = $intent;
return $this;
}
/* -( Migrating to status-less ReleephRequests )--------------------------- */
protected function didReadData() {
if ($this->userIntents === null) {
$this->userIntents = array();
}
}
public function setStatus($value) {
throw new Exception('`status` is now deprecated!');
}
/* -( Make magic Lisk methods private )------------------------------------ */
private function setUserIntents(array $ar) {
return parent::setUserIntents($ar);
}
}

View file

@ -0,0 +1,39 @@
<?php
final class ReleephEvent extends ReleephDAO {
const TYPE_BRANCH_CREATE = 'branch-create';
const TYPE_BRANCH_ACCESS = 'branch-access-change';
protected $releephProjectID;
protected $releephBranchID;
protected $type;
protected $epoch;
protected $actorPHID;
protected $details = array();
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
protected function willSaveObject() {
parent::willSaveObject();
if (!$this->epoch) {
$this->epoch = $this->dateCreated;
}
}
}

View file

@ -0,0 +1,94 @@
<?php
final class ReleephRequestEvent extends ReleephDAO {
const TYPE_CREATE = 'create';
const TYPE_STATUS = 'status'; // old events
const TYPE_USER_INTENT = 'user-intent';
const TYPE_PICK_STATUS = 'pick-status';
const TYPE_COMMIT = 'commit';
const TYPE_MANUAL_ACTION = 'manual-action';
const TYPE_DISCOVERY = 'discovery';
const TYPE_COMMENT = 'comment';
protected $releephRequestID;
protected $type;
protected $actorPHID;
protected $details = array();
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
private function setDetails(array $details) {
throw new Exception('Use setDetail()!');
}
public function setStatusBefore($status) {
return $this->setDetail('oldStatus', $status);
}
public function setStatusAfter($status) {
return $this->setDetail('newStatus', $status);
}
public function getStatusBefore() {
return $this->getDetail('oldStatus');
}
public function getStatusAfter() {
return $this->getDetail('newStatus');
}
public function getComment() {
return $this->getDetail('comment');
}
public function extractPHIDs() {
$phids = array();
$phids[] = $this->actorPHID;
foreach ($this->details as $key => $value) {
if (strpos($key, 'PHID') !== false || strpos($key, 'phid') !== false) {
$phids[] = $value;
}
}
return $phids;
}
public function canGroupWith(ReleephRequestEvent $next) {
if ($this->getActorPHID() != $next->getActorPHID()) {
return false;
}
if ($this->getComment() && $next->getComment()) {
return false;
}
// Break the chain if the next event changes the status
if ($next->getStatusBefore() != $next->getStatusAfter()) {
return false;
}
// Don't group if the next event starts off with a different status to the
// one we ended with. This probably shouldn't ever happen.
if ($this->getStatusAfter() != $next->getStatusBefore()) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,3 @@
<?php
final class ReleephRequestException extends Exception {}

View file

@ -0,0 +1,155 @@
<?php
final class ReleephProjectView extends AphrontView {
private $showOpenBranches = true;
private $releephProject;
private $releephBranches;
public function setShowOpenBranches($active) {
$this->showOpenBranches = $active;
return $this;
}
public function setReleephProject($releeph_project) {
$this->releephProject = $releeph_project;
return $this;
}
public function setBranches($branches) {
$this->releephBranches = $branches;
return $this;
}
public function render() {
$releeph_project = $this->releephProject;
if ($this->showOpenBranches) {
$releeph_branches = mfilter($this->releephBranches, 'getIsActive');
} else {
$releeph_branches = mfilter($this->releephBranches, 'getIsActive', true);
}
// Load all relevant PHID handles
$phids = array_merge(
array(
$this->releephProject->getPHID(),
$this->releephProject->getRepositoryPHID(),
),
mpull($releeph_branches, 'getCreatedByUserPHID'),
mpull($releeph_branches, 'getCutPointCommitPHID'),
$releeph_project->getPushers());
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($this->getUser())
->loadHandles();
// Sort branches, which requires the handles above
$releeph_branches = self::sortBranches($releeph_branches, $handles);
// The header
$repository_phid = $releeph_project->getRepositoryPHID();
$header = hsprintf(
'%s in %s repository',
$releeph_project->getName(),
$handles[$repository_phid]->renderLink());
if ($this->showOpenBranches) {
$view_other_link = phutil_tag(
'a',
array(
'href' => $releeph_project->getURI('closedbranches/'),
),
'View closed branches');
} else {
$view_other_link = phutil_tag(
'a',
array(
'href' => $releeph_project->getURI(),
),
'View open branches');
}
$header = hsprintf("%s &middot; %s", $header, $view_other_link);
// The "create branch" button
$create_branch_url = $releeph_project->getURI('cutbranch/');
// Pushers info
$pushers_info = array();
$pushers = $releeph_project->getPushers();
require_celerity_resource('releeph-project');
if ($pushers) {
$pushers_info[] = phutil_tag('h2', array(), 'Pushers');
foreach ($pushers as $user_phid) {
$handle = $handles[$user_phid];
$div = phutil_tag(
'div',
array(
'class' => 'releeph-pusher',
'style' => 'background-image: url('.$handle->getImageURI().');',
),
phutil_tag(
'div',
array(
'class' => 'releeph-pusher-body',
),
$handles[$user_phid]->renderLink()));
$pushers_info[] = $div;
}
$pushers_info[] = hsprintf('<div style="clear: both;"></div>');
}
// Put it all together
$panel = id(new AphrontPanelView())
->setHeader($header)
->appendChild(phutil_implode_html('', $pushers_info));
foreach ($releeph_branches as $ii => $releeph_branch) {
$box = id(new ReleephBranchBoxView())
->setUser($this->user)
->setHandles($handles)
->setReleephBranch($releeph_branch)
->setNamed();
if ($ii === 0) {
$box->setLatest();
}
$panel->appendChild($box);
}
return $panel->render();
}
/**
* Sort branches by the point at which they were cut, newest cut points
* first.
*
* If branches share a cut point, sort newest branch first.
*/
private static function sortBranches($branches, $handles) {
// Group by commit phid
$groups = mgroup($branches, 'getCutPointCommitPHID');
// Convert commit phid to a commit timestamp
$ar = array();
foreach ($groups as $cut_phid => $group) {
$handle = $handles[$cut_phid];
// Pack (timestamp, group-with-this-timestamp) pairs into $ar
$ar[] = array(
$handle->getTimestamp(),
msort($group, 'getDateCreated')
);
}
$branches = array();
// Sort by timestamp, pull groups, and flatten into one big group
foreach (ipull(isort($ar, 0), 1) as $group) {
$branches = array_merge($branches, $group);
}
return array_reverse($branches);
}
}

View file

@ -0,0 +1,225 @@
<?php
final class ReleephBranchBoxView extends AphrontView {
private $releephBranch;
private $isLatest = false;
private $isNamed = false;
private $handles;
public function setReleephBranch(ReleephBranch $br) {
$this->releephBranch = $br;
return $this;
}
// Primary highlighted branch
public function setLatest() {
$this->isLatest = true;
return $this;
}
// Secondary highlighted branch(es)
public function setNamed() {
$this->isNamed = true;
return $this;
}
public function setHandles($handles) {
$this->handles = $handles;
return $this;
}
public function render() {
$br = $this->releephBranch;
require_celerity_resource('releeph-branch');
return phutil_tag(
'div',
array(
'class' => 'releeph-branch-box'.
($this->isNamed ? ' releeph-branch-box-named' : '').
($this->isLatest ? ' releeph-branch-box-latest' : ''),
),
array(
$this->renderNames(),
$this->renderDatesTable(),
// "float: right" means the ordering here is weird
$this->renderButtons(),
$this->renderStatisticsTable(),
phutil_tag(
'div',
array(
'style' => 'clear:both;',
),
'')));
}
private function renderNames() {
$br = $this->releephBranch;
return phutil_tag(
'div',
array(
'class' => 'names',
),
array(
phutil_tag(
'h1',
array(),
$br->getDisplayName()),
phutil_tag(
'h2',
array(),
$br->getName())));
}
private function renderDatesTable() {
$br = $this->releephBranch;
$branch_commit_handle = $this->handles[$br->getCutPointCommitPHID()];
$properties = array();
$properties['Created by'] =
$cut_age = phabricator_format_relative_time(
time() - $branch_commit_handle->getTimestamp());
return phutil_tag(
'div',
array(
'class' => 'date-info',
),
array(
$this->handles[$br->getCreatedByUserPHID()]->renderLink(),
phutil_tag('br'),
phutil_tag(
'a',
array(
'href' => $branch_commit_handle->getURI(),
),
$cut_age.' old')));
}
private function renderStatisticsTable() {
$statistics = array();
$requests = $this->releephBranch->loadReleephRequests($this->getUser());
foreach ($requests as $request) {
$status = $request->getStatus();
if (!isset($statistics[$status])) {
$statistics[$status] = 0;
}
$statistics[$status]++;
}
static $col_groups = 3;
$cells = array();
foreach ($statistics as $status => $count) {
$description = ReleephRequest::getStatusDescriptionFor($status);
$cells[] = phutil_tag('th', array(), $count);
$cells[] = phutil_tag('td', array(), $description);
}
$rows = array();
while ($cells) {
$row_cells = array();
for ($ii = 0; $ii < 2 * $col_groups; $ii++) {
$row_cells[] = array_shift($cells);
}
$rows[] = phutil_tag('tr', array(), $row_cells);
}
if (!$rows) {
$rows = hsprintf('<tr><th></th><td>%s</td></tr>', 'none');
}
return phutil_tag(
'div',
array(
'class' => 'request-statistics',
),
phutil_tag(
'table',
array(),
$rows));
}
private function renderButtons() {
$br = $this->releephBranch;
$buttons = array();
$buttons[] = phutil_tag(
'a',
array(
'class' => 'small grey button',
'href' => $br->getURI(),
),
'View Requests');
$repo = $br->loadReleephProject()->loadPhabricatorRepository();
if (!$repo) {
$buttons[] = phutil_tag(
'a',
array(
'class' => 'small button disabled',
),
"Diffusion \xE2\x86\x97");
} else {
$diffusion_request = DiffusionRequest::newFromDictionary(array(
'repository' => $repo,
));
$diffusion_branch_uri = $diffusion_request->generateURI(array(
'action' => 'branch',
'branch' => $br->getName(),
));
$diffusion_button_class = 'small grey button';
$buttons[] = phutil_tag(
'a',
array(
'class' => $diffusion_button_class,
'target' => '_blank',
'href' => $diffusion_branch_uri,
),
"Diffusion \xE2\x86\x97");
}
$releeph_project = $br->loadReleephProject();
if (!$releeph_project->getPushers() ||
$releeph_project->isPusher($this->user)) {
$buttons[] = phutil_tag(
'a',
array(
'class' => 'small blue button',
'href' => $br->getURI('edit/'),
),
'Edit');
if ($br->isActive()) {
$button_text = "Close";
$href = $br->getURI('close/');
} else {
$button_text = "Re-open";
$href = $br->getURI('re-open/');
}
$buttons[] = javelin_tag(
'a',
array(
'class' => 'small blue button',
'href' => $href,
'sigil' => 'workflow',
),
$button_text);
}
return phutil_tag(
'div',
array(
'class' => 'buttons',
),
$buttons);
}
}

View file

@ -0,0 +1,60 @@
<?php
final class ReleephBranchPreviewView extends AphrontFormControl {
private $statics = array();
private $dynamics = array();
public function addControl($param_name, AphrontFormControl $control) {
$celerity_id = celerity_generate_unique_node_id();
$control->setID($celerity_id);
$this->dynamics[$param_name] = $celerity_id;
return $this;
}
public function addStatic($param_name, $value) {
$this->statics[$param_name] = $value;
return $this;
}
public function getCustomControlClass() {
require_celerity_resource('releeph-preview-branch');
return 'releeph-preview-branch';
}
public function renderInput() {
static $required_params = array(
'arcProjectID',
'projectName',
'isSymbolic',
'template',
);
$all_params = array_merge($this->statics, $this->dynamics);
foreach ($required_params as $param_name) {
if (idx($all_params, $param_name) === null) {
throw new Exception(
"'{$param_name}' is not set as either a static or dynamic!");
}
}
$output_id = celerity_generate_unique_node_id();
Javelin::initBehavior('releeph-preview-branch', array(
'uri' => '/releeph/branch/preview/',
'outputID' => $output_id,
'params' => array(
'static' => $this->statics,
'dynamic' => $this->dynamics,
)
));
return phutil_tag(
'div',
array(
'id' => $output_id,
),
'');
}
}

View file

@ -0,0 +1,241 @@
<?php
final class ReleephBranchTemplate {
const KEY = 'releeph.default-branch-template';
public static function getDefaultTemplate() {
return PhabricatorEnv::getEnvConfig(self::KEY);
}
public static function getRequiredDefaultTemplate() {
$template = self::getDefaultTemplate();
if (!$template) {
throw new Exception(sprintf(
"Config setting '%s' must be set, ".
"or you must provide a branch-template for each project!",
self::KEY));
}
return $template;
}
public static function getFakeCommitHandleFor($arc_project_id) {
$arc_project = id(new PhabricatorRepositoryArcanistProject())
->load($arc_project_id);
if (!$arc_project) {
throw new Exception(
"No Arc project found with id '{$arc_project_id}'!");
}
$repository = $arc_project->loadRepository();
return id(new PhabricatorObjectHandle())
->setName($repository->formatCommitName('100000000000'));
}
private $commitHandle;
private $branchDate = null;
private $projectName;
private $isSymbolic;
public function setCommitHandle(PhabricatorObjectHandle $handle) {
$this->commitHandle = $handle;
return $this;
}
public function setBranchDate($branch_date) {
$this->branchDate = $branch_date;
return $this;
}
public function setReleephProjectName($project_name) {
$this->projectName = $project_name;
return $this;
}
public function setSymbolic($is_symbolic) {
$this->isSymbolic = $is_symbolic;
return $this;
}
public function interpolate($template) {
if (!$this->projectName) {
return array('', array());
}
list($name, $name_errors) = $this->interpolateInner(
$template,
$this->isSymbolic);
if ($this->isSymbolic) {
return array($name, $name_errors);
} else {
$validate_errors = $this->validateAsBranchName($name);
$errors = array_merge($name_errors, $validate_errors);
return array($name, $errors);
}
}
public static function getHelpRemarkup() {
return <<<EOTEXT
==== Interpolations ====
| Code | Meaning
| ----- | -------
| `%P` | The name of your project, with spaces changed to "-".
| `%p` | Like %P, but all lowercase.
| `%Y` | The four digit year associated with the branch date.
| `%m` | The two digit month.
| `%d` | The two digit day.
| `%v` | The handle of the commit where the branch was cut ("rXYZa4b3c2d1").
| `%V` | The abbreviated commit id where the branch was cut ("a4b3c2d1").
| `%..` | Any other sequence interpreted by `strftime()`.
| `%%` | A literal percent sign.
==== Tips for Branch Templates ====
Use a directory to separate your release branches from other branches:
lang=none
releases/%Y-%M-%d-%v
=> releases/2012-30-16-rHERGE32cd512a52b7
Include a second hierarchy if you share your repository with other projects:
lang=none
releases/%P/%p-release-%Y%m%d-%V
=> releases/Tintin/tintin-release-20121116-32cd512a52b7
Keep your branch names simple, avoiding strange punctuation, most of which is
forbidden or escaped anyway:
lang=none, counterexample
releases//..clown-releases..//`date --iso=seconds`-$(sudo halt)
Include the date early in your template, in an order which sorts properly:
lang=none
releases/%Y%m%d-%v
=> releases/20121116-rHERGE32cd512a52b7 (good!)
releases/%V-%m.%d.%Y
=> releases/32cd512a52b7-11.16.2012 (awful!)
EOTEXT
;
}
/*
* xsprintf() would be useful here, but that's for formatting concrete lists
* of things in a certain way...
*
* animal_printf('%A %A %A', $dog1, $dog2, $dog3);
*
* ...rather than interpolating percent-control-strings like strftime does.
*/
private function interpolateInner($template, $is_symbolic) {
$name = $template;
$errors = array();
$safe_project_name = str_replace(' ', '-', $this->projectName);
$short_commit_id = last(
preg_split('/r[A-Z]+/', $this->commitHandle->getName()));
$interpolations = array();
for ($ii = 0; $ii < strlen($name); $ii++) {
$char = substr($name, $ii, 1);
$prev = null;
if ($ii > 0) {
$prev = substr($name, $ii - 1, 1);
}
$next = substr($name, $ii + 1, 1);
if ($next && $char == '%' && $prev != '%') {
$interpolations[$ii] = $next;
}
}
$variable_interpolations = array();
$reverse_interpolations = $interpolations;
krsort($reverse_interpolations);
if ($this->branchDate) {
$branch_date = $this->branchDate;
} else {
$branch_date = $this->commitHandle->getTimestamp();
}
foreach ($reverse_interpolations as $position => $code) {
$replacement = null;
switch ($code) {
case 'v':
$replacement = $this->commitHandle->getName();
$is_variable = true;
break;
case 'V':
$replacement = $short_commit_id;
$is_variable = true;
break;
case 'P':
$replacement = $safe_project_name;
$is_variable = false;
break;
case 'p':
$replacement = strtolower($safe_project_name);
$is_variable = false;
break;
default:
// Format anything else using strftime()
$replacement = strftime("%{$code}", $branch_date);
$is_variable = true;
break;
}
if ($is_variable) {
$variable_interpolations[] = $code;
}
$name = substr_replace($name, $replacement, $position, 2);
}
if (!$is_symbolic && !$variable_interpolations) {
$errors[] = "Include additional interpolations that aren't static!";
}
return array($name, $errors);
}
private function validateAsBranchName($name) {
$errors = array();
if (preg_match('{^/}', $name) || preg_match('{/$}', $name)) {
$errors[] = "Branches cannot begin or end with '/'";
}
if (preg_match('{//+}', $name)) {
$errors[] = "Branches cannot contain multiple consective '/'";
}
$parts = array_filter(explode('/', $name));
foreach ($parts as $index => $part) {
$part_error = null;
if (preg_match('{^\.}', $part) || preg_match('{\.$}', $part)) {
$errors[] = "Path components cannot begin or end with '.'";
} elseif (preg_match('{^(?!\w)}', $part)) {
$errors[] = "Path components must begin with an alphanumeric";
} elseif (!preg_match('{^\w ([\w-_%\.]* [\w-_%])?$}x', $part)) {
$errors[] =
"Path components may only contain alphanumerics ".
"or '-', '_', or '.'";
}
}
return $errors;
}
}

View file

@ -0,0 +1,102 @@
<?php
final class ReleephActiveProjectListView extends AphrontView {
private $releephProjects;
public function setReleephProjects(array $releeph_projects) {
$this->releephProjects = $releeph_projects;
return $this;
}
public function render() {
$rows = array();
foreach ($this->releephProjects as $releeph_project) {
$project_uri = $releeph_project->getURI();
$name_link = phutil_tag(
'a',
array(
'href' => $project_uri,
'style' => 'font-weight: bold;',
),
$releeph_project->getName());
$edit_button = phutil_tag(
'a',
array(
'href' => $releeph_project->getURI('edit/'),
'class' => 'small grey button',
),
'Edit');
$deactivate_button = javelin_tag(
'a',
array(
'href' => $releeph_project->getURI('action/deactivate/'),
'class' => 'small grey button',
'sigil' => 'workflow',
),
'Remove');
$arc_project = $releeph_project->loadArcanistProject();
if ($arc_project) {
$arc_project_name = $arc_project->getName();
} else {
$arc_project_name = phutil_tag(
'i',
array(),
'Deleted Arcanist Project');
}
$repo = $releeph_project->loadPhabricatorRepository();
if ($repo) {
$vcs_type =
PhabricatorRepositoryType::getNameForRepositoryType(
$repo->getVersionControlSystem());
$rows[] = array(
$name_link,
$repo->getName(),
$arc_project_name,
$vcs_type,
$edit_button,
$deactivate_button,
);
} else {
$rows[] = array(
$name_link,
phutil_tag('i', array(), 'Deleted Repository'),
$arc_project_name,
null,
null,
$deactivate_button,
);
}
}
$table = new AphrontTableView($rows);
$table->setHeaders(array(
'Name',
'Repository',
'Arcanist Project',
'Type',
'',
''
));
$table->setColumnClasses(array(
null,
null,
'wide',
null,
'action',
'action'
));
return $table->render();
}
}

View file

@ -0,0 +1,112 @@
<?php
final class ReleephInactiveProjectListView extends AphrontView {
private $releephProjects;
public function setReleephProjects(array $releeph_projects) {
$this->releephProjects = $releeph_projects;
return $this;
}
public function render() {
$rows = array();
$phids = array();
foreach ($this->releephProjects as $releeph_project) {
$phids[] = $releeph_project->getCreatedByUserPHID();
if ($phid = $releeph_project->getDetail('last_deactivated_user')) {
$phids[] = $phid;
}
}
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($this->getUser())
->loadHandles();
foreach ($this->releephProjects as $releeph_project) {
$repository = $releeph_project->loadPhabricatorRepository();
if (!$repository) {
// Ignore projects referring to repositories that have been deleted.
continue;
}
$activate_link = javelin_tag(
'a',
array(
'href' => $releeph_project->getURI('action/activate/'),
'class' => 'small grey button',
'sigil' => 'workflow',
),
'Revive');
$delete_link = javelin_tag(
'a',
array(
'href' => $releeph_project->getURI('action/delete/'),
'class' => 'small grey button',
'sigil' => 'workflow',
),
'Delete');
$rows[] = array(
$releeph_project->getName(),
$repository->getName(),
$this->renderCreationInfo($releeph_project, $handles),
$this->renderDeletionInfo($releeph_project, $handles),
$activate_link,
$delete_link,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(array(
'Name',
'Repository',
'Created',
'Deleted',
'',
'',
));
$table->setColumnClasses(array(
null,
null,
null,
'wide',
'action',
'action',
));
return $table->render();
}
private function renderCreationInfo($releeph_project, $handles) {
$creator = $handles[$releeph_project->getCreatedByUserPHID()];
$when = $releeph_project->getDateCreated();
return hsprintf(
'%s by %s',
phabricator_relative_date($when, $this->user),
$creator->getName());
}
private function renderDeletionInfo($releeph_project, $handles) {
$deleted_on = $releeph_project->getDetail('last_deactivated_time');
$deleted_by_name = null;
$deleted_by_phid = $releeph_project->getDetail('last_deactivated_user');
if ($deleted_by_phid) {
$deleted_by_name = $handles[$deleted_by_phid]->getName();
} else {
$deleted_by_name = 'unknown';
}
return hsprintf(
'%s by %s',
phabricator_relative_date($deleted_on, $this->user),
$deleted_by_name);
}
}

View file

@ -0,0 +1,104 @@
<?php
final class ReleephRequestIntentsView extends AphrontView {
private $releephRequest;
private $releephProject;
public function setReleephRequest(ReleephRequest $rq) {
$this->releephRequest = $rq;
return $this;
}
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function render() {
require_celerity_resource('releeph-intents');
return phutil_tag(
'div',
array(
'class' => 'releeph-intents',
),
array(
$this->renderIntentList(ReleephRequest::INTENT_WANT),
$this->renderIntentList(ReleephRequest::INTENT_PASS)
));
}
private function renderIntentList($render_intent) {
if (!$this->releephProject) {
throw new Exception("Must call setReleephProject() first!");
}
$project = $this->releephProject;
$request = $this->releephRequest;
$handles = $request->getHandles();
$is_want = $render_intent == ReleephRequest::INTENT_WANT;
$should = $request->shouldBeInBranch();
$pusher_links = array();
$user_links = array();
$intents = $request->getUserIntents();
foreach ($intents as $user_phid => $user_intent) {
if ($user_intent == $render_intent) {
$is_pusher = $project->isPusherPHID($user_phid);
if ($is_pusher) {
$pusher_links[] = phutil_tag(
'span',
array(
'class' => 'pusher'
),
$handles[$user_phid]->renderLink());
} else {
$class = 'bystander';
if ($request->getRequestUserPHID() == $user_phid) {
$class = 'requestor';
}
$user_links[] = phutil_tag(
'span',
array(
'class' => $class,
),
$handles[$user_phid]->renderLink());
}
}
}
// Don't render anything
if (!$pusher_links && !$user_links) {
return null;
}
$links = array_merge($pusher_links, $user_links);
if ($links) {
$markup = $links;
} else {
$markup = array('&nbsp;');
}
// Stick an arrow up front
$arrow_class = 'arrow '.$render_intent;
array_unshift($markup, phutil_tag(
'div',
array(
'class' => $arrow_class,
),
''));
return phutil_tag(
'div',
array(
'class' => 'intents',
),
$markup);
}
}

View file

@ -0,0 +1,53 @@
<?php
final class ReleephRequestStatusView extends AphrontView {
private $releephRequest;
public function setReleephRequest(ReleephRequest $rq) {
$this->releephRequest = $rq;
return $this;
}
public function render() {
require_celerity_resource('releeph-status');
$request = $this->releephRequest;
$status = $request->getStatus();
$pick_status = $request->getPickStatus();
$description = ReleephRequest::getStatusDescriptionFor($status);
$warning = null;
if ($status == ReleephRequest::STATUS_NEEDS_PICK) {
if ($pick_status == ReleephRequest::PICK_FAILED) {
$warning = 'Last pick failed!';
}
} elseif ($status == ReleephRequest::STATUS_NEEDS_REVERT) {
if ($pick_status == ReleephRequest::REVERT_FAILED) {
$warning = 'Last revert failed!';
}
}
return phutil_tag(
'div',
array(
'class' => 'releeph-status',
),
array(
phutil_tag(
'div',
array(
'class' => 'description',
),
$description),
phutil_tag(
'div',
array(
'class' => 'warning',
),
$warning)));
}
}

View file

@ -0,0 +1,58 @@
<?php
final class ReleephRequestTypeaheadControl extends AphrontFormControl {
private $repo;
private $startTime;
public function setRepo(PhabricatorRepository $repo) {
$this->repo = $repo;
return $this;
}
public function setStartTime($epoch) {
$this->startTime = $epoch;
return $this;
}
public function getCustomControlClass() {
return 'releeph-request-typeahead';
}
public function renderInput() {
$id = celerity_generate_unique_node_id();
$div = phutil_tag(
'div',
array(
'style' => 'position: relative;',
'id' => $id,
),
phutil_tag(
'input',
array(
'autocomplete' => 'off',
'type' => 'text',
'name' => $this->getName(),
),
''));
require_celerity_resource('releeph-request-typeahead-css');
Javelin::initBehavior('releeph-request-typeahead', array(
'id' => $id,
'src' => '/releeph/request/typeahead/',
'placeholder' => 'Type a commit id or first line of commit message...',
'value' => $this->getValue(),
'aux' => array(
'repo' => $this->repo->getID(),
'callsign' => $this->repo->getCallsign(),
'since' => $this->startTime,
'limit' => 16,
)
));
return $div;
}
}

View file

@ -0,0 +1,113 @@
<?php
final class ReleephRequestHeaderListView
extends AphrontView {
private $originType;
private $releephProject;
private $releephBranch;
private $releephRequests;
private $aphrontRequest;
private $reload = false;
private $errors = array();
public function setOriginType($origin) {
$this->originType = $origin;
return $this;
}
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function setReleephBranch(ReleephBranch $rb) {
$this->releephBranch = $rb;
return $this;
}
public function setReleephRequests(array $requests) {
assert_instances_of($requests, 'ReleephRequest');
$this->releephRequests = $requests;
return $this;
}
public function setAphrontRequest(AphrontRequest $request) {
$this->aphrontRequest = $request;
return $this;
}
public function setReloadOnStateChange($bool) {
$this->reload = $bool;
return $this;
}
public function render() {
$views = $this->renderInner();
require_celerity_resource('phabricator-notification-css');
Javelin::initBehavior('releeph-request-state-change', array(
'reload' => $this->reload,
));
$error_view = null;
if ($this->errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Bulk load errors')
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setErrors($this->errors)
->render();
}
$list = phutil_tag(
'div',
array(
'data-sigil' => 'releeph-request-header-list',
),
$views);
return $this->renderSingleView(array(
$error_view,
$list));
}
/**
* Required for generating markup for ReleephRequestActionController.
*
* That controller just needs the markup, and doesn't need to start the
* javelin behavior.
*/
public function renderInner() {
$selector = $this->releephProject->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $field) {
$field
->setReleephProject($this->releephProject)
->setReleephBranch($this->releephBranch)
->setUser($this->user);
try {
$field->bulkLoad($this->releephRequests);
} catch (Exception $ex) {
$this->errors[] = $ex;
}
}
$field_groups = $selector->arrangeFieldsForHeaderView($fields);
$views = array();
foreach ($this->releephRequests as $releeph_request) {
$views[] = id(new ReleephRequestHeaderView())
->setUser($this->user)
->setAphrontRequest($this->aphrontRequest)
->setOriginType($this->originType)
->setReleephProject($this->releephProject)
->setReleephBranch($this->releephBranch)
->setReleephRequest($releeph_request)
->setReleephFieldGroups($field_groups)
->render();
}
return $views;
}
}

View file

@ -0,0 +1,334 @@
<?php
final class ReleephRequestHeaderView extends AphrontView {
const THROW_PARAM = '__releeph_throw';
private $aphrontRequest;
private $releephRequest;
private $releephBranch;
private $releephProject;
private $originType;
private $fieldGroups;
public function setAphrontRequest(AphrontRequest $request) {
$this->aphrontRequest = $request;
return $this;
}
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function setReleephBranch(ReleephBranch $rb) {
$this->releephBranch = $rb;
return $this;
}
public function setReleephRequest(ReleephRequest $rr) {
$this->releephRequest = $rr;
return $this;
}
public function setOriginType($origin) {
// For the Edit controller
$this->originType = $origin;
return $this;
}
public function setReleephFieldGroups(array $field_groups) {
$this->fieldGroups = $field_groups;
return $this;
}
protected function getOrigin() {
return $this->originType;
}
public function render() {
require_celerity_resource('releeph-core');
$all_properties_table = $this->renderFields();
require_celerity_resource('releeph-colors');
$status = $this->releephRequest->getStatus();
$rr_div_class =
'releeph-request-header '.
'releeph-request-header-border '.
'releeph-border-color-'.ReleephRequest::getStatusClassSuffixFor($status);
$hidden_link = phutil_tag(
'a',
array(
'href' => '/RQ'.$this->releephRequest->getID(),
'target' => '_blank',
'data-sigil' => 'hidden-link',
),
'');
$focus_char = phutil_tag(
'div',
array(
'class' => 'focus-char',
'data-sigil' => 'focus-char',
),
"\xE2\x98\x86");
$rr_div = phutil_tag(
'div',
array(
'data-sigil' => 'releeph-request-header',
'class' => $rr_div_class,
),
array(
phutil_tag(
'div',
array(),
array(
phutil_tag(
'h1',
array(),
array(
$focus_char,
$this->renderTitleLink(),
$hidden_link
)),
$all_properties_table,
)),
phutil_tag(
'div',
array(
'class' => 'button-divider',
),
$this->renderActionButtonsTable())));
return $rr_div;
}
private function renderFields() {
$field_row_groups = $this->fieldGroups;
$trs = array();
foreach ($field_row_groups as $field_column_group) {
$tds = array();
foreach ($field_column_group as $side => $fields) {
$rows = array();
foreach ($fields as $field) {
$rows[] = $this->renderOneField($field);
}
$pane = phutil_tag(
'table',
array(
'class' => 'fields',
),
$rows);
$tds[] = phutil_tag(
'td',
array(
'class' => 'side '.$side,
),
$pane);
}
$trs[] = phutil_tag(
'tr',
array(),
$tds);
}
return phutil_tag(
'table',
array(
'class' => 'panes',
),
$trs);
}
private function renderOneField(ReleephFieldSpecification $field) {
$field
->setUser($this->user)
->setReleephProject($this->releephProject)
->setReleephBranch($this->releephBranch)
->setReleephRequest($this->releephRequest);
$label = $field->renderLabelForHeaderView();
try {
$value = $field->renderValueForHeaderView();
} catch (Exception $ex) {
if ($this->aphrontRequest->getInt(self::THROW_PARAM)) {
throw $ex;
} else {
$value = $this->renderExceptionIcon($ex);
}
}
if ($value) {
if (!$label) {
return phutil_tag(
'tr',
array(),
phutil_tag('td', array('colspan' => 2), $value));
} else {
return phutil_tag(
'tr',
array(),
array(
phutil_tag('th', array(), $label),
phutil_tag('td', array(), $value)));
}
}
}
private function renderExceptionIcon(Exception $ex) {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
$throw_uri = $this
->aphrontRequest
->getRequestURI()
->setQueryParam(self::THROW_PARAM, 1);
$message = $ex->getMessage();
if (!$message) {
$message = get_class($ex).' with no message.';
}
return javelin_tag(
'a',
array(
'class' => 'releeph-field-error',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $message,
'size' => 400,
'align' => 'E',
),
'href' => $throw_uri,
),
'!!!');
}
private function renderTitleLink() {
$rq_id = $this->releephRequest->getID();
$summary = $this->releephRequest->getSummaryForDisplay();
return phutil_tag(
'a',
array(
'href' => '/RQ'.$rq_id,
),
hsprintf(
'RQ%d: %s',
$rq_id,
$summary));
}
private function renderActionButtonsTable() {
$left_buttons = array();
$right_buttons = array();
$user_phid = $this->user->getPHID();
$is_pusher = $this->releephProject->isPusherPHID($user_phid);
$is_requestor = $this->releephRequest->getRequestUserPHID() === $user_phid;
$current_intent = idx(
$this->releephRequest->getUserIntents(),
$this->user->getPHID());
if ($is_pusher) {
$left_buttons[] = $this->renderIntentButton(true, 'Approve', 'green');
$left_buttons[] = $this->renderIntentButton(false, 'Reject');
} else {
if ($is_requestor) {
$right_buttons[] = $this->renderIntentButton(true, 'Request');
$right_buttons[] = $this->renderIntentButton(false, 'Remove');
} else {
$right_buttons[] = $this->renderIntentButton(true, 'Want');
$right_buttons[] = $this->renderIntentButton(false, 'Pass');
}
}
// Allow the pusher to mark a request as manually picked or reverted.
if ($is_pusher || $is_requestor) {
if ($this->releephRequest->getInBranch()) {
$left_buttons[] = $this->renderActionButton(
'Mark Manually Reverted',
'mark-manually-reverted');
} else {
$left_buttons[] = $this->renderActionButton(
'Mark Manually Picked',
'mark-manually-picked');
}
}
$right_buttons[] = phutil_tag(
'a',
array(
'href' => '/releeph/request/edit/'.$this->releephRequest->getID().
'?origin='.$this->originType,
'class' => 'small blue button',
),
'Edit');
if (!$left_buttons && !$right_buttons) {
return;
}
$cells = array();
foreach ($left_buttons as $button) {
$cells[] = phutil_tag('td', array('align' => 'left'), $button);
}
$cells[] = phutil_tag('td', array('class' => 'wide'), '');
foreach ($right_buttons as $button) {
$cells[] = phutil_tag('td', array('align' => 'right'), $button);
}
$table = phutil_tag(
'table',
array(
'class' => 'buttons',
),
phutil_tag(
'tr',
array(),
$cells));
return $table;
}
private function renderIntentButton($want, $name, $class = null) {
$current_intent = idx(
$this->releephRequest->getUserIntents(),
$this->user->getPHID());
if ($current_intent) {
// If this is a "want" button, and they already want it, disable the
// button (and vice versa for the "pass" case.)
if (($want && $current_intent == ReleephRequest::INTENT_WANT) ||
(!$want && $current_intent == ReleephRequest::INTENT_PASS)) {
$class .= ' disabled';
}
}
$action = $want ? 'want' : 'pass';
return $this->renderActionButton($name, $action, $class);
}
private function renderActionButton($name, $action, $class=null) {
$attributes = array(
'class' => 'small button '.$class,
'sigil' => 'releeph-request-state-change '.$action,
'meta' => null,
);
if ($class != 'disabled') {
// NB the trailing slash on $uri is critical, otherwise the URI will
// redirect to one with a slash, which will turn our GET into a POST.
$attributes['meta'] = sprintf(
'/releeph/request/action/%s/%d/',
$action,
$this->releephRequest->getID());
}
return javelin_tag('a', $attributes, $name);
}
}

View file

@ -0,0 +1,266 @@
<?php
final class ReleephRequestEventListView extends AphrontView {
private $events;
private $handles;
public function setEvents(array $events) {
assert_instances_of($events, 'ReleephRequestEvent');
$this->events = $events;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function render() {
$views = array();
$discovered_commits = array();
foreach ($this->events as $event) {
$commit_id = $event->getDetail('newCommitIdentifier');
switch ($event->getType()) {
case ReleephRequestEvent::TYPE_DISCOVERY:
$discovered_commits[$commit_id] = true;
break;
}
}
$markup_engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$markup_engine->setConfig('viewer', $this->getUser());
foreach ($this->events as $event) {
$description = $this->describeEvent($event);
if (!$description) {
continue;
}
if ($event->getType() === ReleephRequestEvent::TYPE_COMMIT) {
$commit_id = $event->getDetail('newCommitIdentifier');
if (idx($discovered_commits, $commit_id)) {
continue;
}
}
$actor_handle = $this->handles[$event->getActorPHID()];
$description = $this->describeEvent($event);
$action = phutil_tag(
'div',
array(),
array(
$actor_handle->renderLink(),
' ',
$description));
$view = id(new PhabricatorTransactionView())
->setUser($this->user)
->setImageURI($actor_handle->getImageURI())
->setEpoch($event->getDateCreated())
->setActions(array($action))
->addClass($this->getTransactionClass($event));
$comment = $this->getEventComment($event);
if ($comment) {
$markup = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
phutil_safe_html(
$markup_engine->markupText($comment)));
$view->appendChild($markup);
}
$views[] = $view;
}
return phutil_tag(
'div',
array(
'class' => 'releeph-request-event-list',
),
$views);
}
public function renderForEmail() {
$items = array();
foreach ($this->events as $event) {
$description = $this->describeEvent($event);
if (!$description) {
continue;
}
$actor = $this->handles[$event->getActorPHID()]->getName();
$items[] = $actor.' '.$description;
$comment = $this->getEventComment($event);
if ($comment) {
$items[] = preg_replace('/^/m', ' ', $comment);
}
}
return implode("\n\n", $items);
}
private function describeEvent(ReleephRequestEvent $event) {
$type = $event->getType();
switch ($type) {
case ReleephRequestEvent::TYPE_CREATE:
return "created this request.";
break;
case ReleephRequestEvent::TYPE_STATUS:
$status = $event->getStatusAfter();
return sprintf(
"updated status to %s.",
ReleephRequest::getStatusDescriptionFor($status));
break;
case ReleephRequestEvent::TYPE_USER_INTENT:
$intent = $event->getDetail('newIntent');
$was_pusher = $event->getDetail('wasPusher');
if ($intent == ReleephRequest::INTENT_WANT) {
if ($was_pusher) {
$verb = "approved";
} else {
$verb = "wanted";
}
} else {
if ($was_pusher) {
$verb = "rejected";
} else {
$verb = "passed on";
}
}
return "{$verb} this request.";
break;
case ReleephRequestEvent::TYPE_PICK_STATUS:
$pick_status = $event->getDetail('newPickStatus');
switch ($pick_status) {
case ReleephRequest::PICK_FAILED:
return "found a conflict when picking.";
break;
case ReleephRequest::REVERT_FAILED:
return "found a conflict when reverting.";
break;
case ReleephRequest::PICK_OK:
case ReleephRequest::REVERT_OK:
// (nothing)
break;
default:
return "changed pick-status to {$pick_status}.";
break;
}
break;
case ReleephRequestEvent::TYPE_MANUAL_ACTION:
$action = $event->getDetail('action');
return "claimed to have manually {$action}ed this request.";
break;
case ReleephRequestEvent::TYPE_COMMIT:
$action = $event->getDetail('action');
if ($action) {
return "{$action}ed this request.";
} else {
return "did something with this request.";
}
break;
case ReleephRequestEvent::TYPE_DISCOVERY:
$action = $event->getDetail('action');
if ($action) {
return "{$action}ed this request.";
} else {
// It's unlikely we'll have action-less TYPE_DISCOVERY events, but I
// used this during testing and I guess it's a useful safety net.
return "discovered this request in the branch.";
}
break;
case ReleephRequestEvent::TYPE_COMMENT:
return "commented on this request.";
break;
default:
return "did event of type {$type}.";
break;
}
}
private function getEventComment(ReleephRequestEvent $event) {
switch ($event->getType()) {
case ReleephRequestEvent::TYPE_CREATE:
$commit_phid = $event->getDetail('commitPHID');
return sprintf(
"Commit %s was requested.",
$this->handles[$commit_phid]->getName());
break;
case ReleephRequestEvent::TYPE_STATUS:
case ReleephRequestEvent::TYPE_USER_INTENT:
case ReleephRequestEvent::TYPE_PICK_STATUS:
case ReleephRequestEvent::TYPE_MANUAL_ACTION:
// no comment!
break;
case ReleephRequestEvent::TYPE_COMMIT:
return sprintf(
"Closed by commit %s.",
$event->getDetail('newCommitIdentifier'));
break;
case ReleephRequestEvent::TYPE_DISCOVERY:
$author_phid = $event->getDetail('authorPHID');
$commit_phid = $event->getDetail('newCommitPHID');
if ($author_phid && $author_phid != $event->getActorPHID()) {
return sprintf(
"Closed by commit %s (with author set to @%s).",
$this->handles[$commit_phid]->getName(),
$this->handles[$author_phid]->getName());
} else {
return sprintf(
'Closed by commit %s.',
$this->handles[$commit_phid]->getName());
}
break;
case ReleephRequestEvent::TYPE_COMMENT:
return $event->getComment();
break;
}
}
private function getTransactionClass($event) {
switch ($event->getType()) {
case ReleephRequestEvent::TYPE_COMMIT:
case ReleephRequestEvent::TYPE_DISCOVERY:
$action = $event->getDetail('action');
if ($action == 'pick') {
return 'releeph-border-color-picked';
} else {
return 'releeph-border-color-abandoned';
}
break;
case ReleephRequestEvent::TYPE_COMMENT:
return 'releeph-border-color-comment';
break;
default:
$status_after = $event->getStatusAfter();
$class_suffix = ReleephRequest::getStatusClassSuffixFor($status_after);
return ' releeph-border-color-'.$class_suffix;
break;
}
}
}

View file

@ -0,0 +1,9 @@
<?php
final class ReleephDefaultUserView extends ReleephUserView {
public function render() {
return $this->getHandle()->renderLink();
}
}

View file

@ -0,0 +1,74 @@
<?php
abstract class ReleephUserView extends AphrontView {
/**
* This function should bulk load everything you need to render all the given
* user phids.
*
* Many parts of Releeph load users for rendering. Accordingly, this
* function will be called multiple times for each part of the UI that
* renders users, so you should accumulate your results on each call.
*
* You should also implement render() (from AphrontView) to render each
* user's PHID.
*/
protected function loadInner(array $phids) {
// This is a hook!
}
final public static function getNewInstance() {
$key = 'releeph.user-view';
$class = PhabricatorEnv::getEnvConfig($key);
return newv($class, array());
}
private static $handles = array();
private static $seen = array();
final public function load(array $phids) {
$todo = array();
foreach ($phids as $key => $phid) {
if (!idx(self::$seen, $phid)) {
$todo[$key] = $phid;
self::$seen[$phid] = true;
}
}
if ($todo) {
self::$handles = array_merge(
self::$handles,
id(new PhabricatorObjectHandleData($todo))
->setViewer($this->getUser())
->loadHandles());
$this->loadInner($todo);
}
}
private $phid;
private $releephProject;
final public function setRenderUserPHID($phid) {
$this->phid = $phid;
return $this;
}
final public function setReleephProject(ReleephProject $project) {
$this->releephProject = $project;
return $this;
}
final protected function getRenderUserPHID() {
return $this->phid;
}
final protected function getReleephProject() {
return $this->releephProject;
}
final protected function getHandle() {
return self::$handles[$this->phid];
}
}

View file

@ -32,6 +32,8 @@ abstract class PhabricatorBaseEnglishTranslation
'COMMIT(S)' => array('COMMIT', 'COMMITS'),
'%d line(s)' => array('%d line', '%d lines'),
'%d path(s)' => array('%d path', '%d paths'),
'%d diff(s)' => array('%d diff', '%d diffs'),
'added %d commit(s): %s' => array(
'added commit: %2$s',
@ -286,6 +288,10 @@ abstract class PhabricatorBaseEnglishTranslation
),
),
'%d comment(s)' => array('%d comment', '%d comments'),
'%d rejection(s)' => array('%d rejection', '%d rejections'),
'%d update(s)' => array('%d update', '%d updates'),
);
}

View file

@ -171,6 +171,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'db',
'name' => 'token',
),
'db.releeph' => array(
'type' => 'db',
'name' => 'releeph',
),
'0000.legacy.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('0000.legacy.sql'),
@ -1165,6 +1169,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20130304.lintauthor.sql'),
),
'releeph.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('releeph.sql'),
),
);
}

View file

@ -0,0 +1,79 @@
/**
* @provides releeph-branch
*/
.releeph-branch-box {
margin-bottom: .5em;
padding: .5em .5em .5em;
border: 2px solid #d5d5d5;
/*border-top-color: #D5D5D5;
border-right-color: #BBB;
border-bottom-color: #A4A4A4;
border-left-color: #BBB;*/
background: #bbb;
}
/* Types of branch */
.releeph-branch-box-named {
background: #ddd;
}
.releeph-branch-box-latest {
background: #ffd;
}
/* Branch symbolic name and full name */
.releeph-branch-box .names {
width: 25em;
float: left;
margin-bottom: 1em;
}
.releeph-branch-box .names h1 {
font-size: 125%;
padding: 0px;
}
.releeph-branch-box .names h2 {
font-weight: normal;
font-size: 85%;
}
/* Date info */
.releeph-branch-box .date-info {
width: 10%;
float: left;
color: #555;
margin-bottom: .3em;
}
/* Statistics table */
.releeph-branch-box .request-statistics {
float: right;
padding-right: 2em;
font-size: 85%;
}
.releeph-branch-box .request-statistics th {
width: 1em;
text-align: right;
padding-right: .4em;
padding-left: .4em;
}
.releeph-branch-box .request-statistics td {
white-space: nowrap;
font-style: italic;
}
/* Buttons */
.releeph-branch-box .buttons {
float: right;
}

View file

@ -0,0 +1,35 @@
/**
* @provides releeph-colors
*/
.releeph-border-color-failed {
border-color: #d2d;
}
.releeph-border-color-requested {
border-color: #ddd;
}
.releeph-border-color-comment {
border-color: #ddd;
}
.releeph-border-color-needs-pick {
border-color: #096;
}
.releeph-border-color-rejected {
border-color: #d00;
}
.releeph-border-color-needs-revert {
border-color: #d00;
}
.releeph-border-color-abandoned {
border-color: #222;
}
.releeph-border-color-picked {
border-color: #069;
}

View file

@ -0,0 +1,199 @@
/**
* @provides releeph-core
*/
.releeph-request-header {
margin: .5em 2em 3em;
/**
* Copied from the old .differential-panel, present in commit
* f04d8ab1a747dc9719d378d9286088b677ce224c
*
* (As is the <h1> code below)
*/
max-width: 1120px;
border: 1px solid #666622;
background: #efefdf;
padding: 15px 20px;
font-size: 13px;
}
.releeph-request-header h1 {
width: 100%;
border-bottom: 1px solid #aaaa99;
padding-bottom: 8px;
margin-bottom: 8px;
position: relative;
}
.releeph-request-header .focus-char {
left: -10px;
display: none;
float: left;
position: absolute;
top: 0px;
left: -1em;
font-weight: bold;
color: #880;
font-family: "Hiragino Kaku Gothic Pro", "Osaka", "Zapf Dingbats";
}
.releeph-request-header.focus .focus-char {
display: block;
}
.releeph-request-header-border {
border-width: 1px 10px 1px;
border-color: #ddd;
}
/* Laying out properties / fields */
.releeph-request-header table.panes {
width: 100%;
}
.releeph-request-header table.panes td.side {
width: 50%;
max-width: 1em;
}
.releeph-request-header table.panes td.side.left {
padding-right: 20px;
border-right: 3px solid #bbb;
}
.releeph-request-header table.panes td.side.right {
padding-left: 20px;
}
.releeph-request-header table.panes td.side table.fields {
width: 100%;
}
.releeph-request-header table.panes td.side table.fields tr {
vertical-align: middle;
}
.releeph-request-header table.panes td.side table.fields th {
font-weight: bold;
text-align: right;
padding-right: 1em;
white-space: nowrap;
}
.releeph-request-header table.panes td.side table.fields td {
width: 100%; /* wide! */
max-width: 1em;
}
/* Buttons */
.releeph-request-header .button-divider {
clear: both;
margin-top: 1.5em;
border-top: 1px solid #bbb;
}
.releeph-request-header .buttons {
width: 100%;
}
.releeph-request-header .buttons tr {
padding: 1em;
margin: 3em;
}
.releeph-request-header .buttons td {
padding: 1em .5em 0.2em;
}
.releeph-request-header .buttons td.wide {
width: 100%;
}
/* Colors: match differential colors */
.releeph-request-comment {
border-color: #ddd;
}
.releeph-request-comment-pusher {
background: #8DEE8D;
border-color: #096;
}
.releeph-request-comment-pusher div {
background: #8DEE8D;
}
/* The diff size bar */
.releeph-request-header .diff-bar {
border: 0px;
}
.releeph-request-header .diff-bar div {
width: 100px;
border: 1px solid;
border-top-color: #A4A4A4;
border-right-color: #BBB;
border-bottom-color: #D5D5D5;
border-left-color: #BBB;
background: white;
float: left;
margin-right: 1em;
}
.releeph-request-header .diff-bar div div {
height: 10px;
}
.releeph-request-header .diff-bar span {
color: #555;
}
/* Rendering pick / commit errors, etc. */
.releeph-request-pick-failed-event h1:before {
content: '\2014 ';
}
.releeph-request-pick-failed-event h1:after {
content: ' \2014';
}
.releeph-request-pick-failed-event h1 {
padding: 3px 10px 3px;
margin-bottom: 0.5em;
background: #ffb;
font-size: small;
}
.releeph-request-pick-failed-event div {
font-family: monospace;
margin-bottom: 1.5em;
padding-left: 1em;
width: 70em;
}
/* History view of request */
.releeph-request-event-list {
margin: .5em 2em .5em;
}
/* Shorten long header-text */
.releeph-header-text-truncated {
width: 100%;
float: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,37 @@
/**
* @provides releeph-intents
*/
.releeph-intents .intents {
clear: left;
width: 100%;
margin-top: 3px;
}
.releeph-intents .arrow {
float: left;
clear: left;
margin-right: 0.4em;
padding: 8px;
background: transparent 0 0 no-repeat;
}
.releeph-intents .arrow.want {
background-image: url('/rsrc/custom/image/icon/tango/go-next.png');
}
.releeph-intents .arrow.pass {
background-image: url('/rsrc/custom/image/icon/tango/go-previous-gray.png');
}
.releeph-intents a {
margin-right: 0.4em;
}
.releeph-intents .pusher {
font-weight: bold;
}
.releeph-intents .requestor {
font-weight: normal;
}

View file

@ -0,0 +1,29 @@
/**
* @provides releeph-preview-branch
*/
.releeph-preview-branch {
min-height: 4em;
position: relative;
}
.releeph-preview-branch .error {
padding-left: 22px;
background-repeat: no-repeat;
background-size: 16px auto;
background-image: url(/rsrc/custom/image/releeph/releeph_warning.png);
float: left;
position: absolute;
top: 2.5em;
}
.releeph-preview-branch .name {
clear: both;
float: left;
position: absolute;
font-family: monospace;
font-size: 9pt !important;
background: white;
top: 0.7em;
padding: 2px;
}

View file

@ -0,0 +1,25 @@
/**
* @provides releeph-project
*/
/**
* ...from aphront-transaction.css
*/
.releeph-pusher {
background: 2px 2px no-repeat;
margin-top: 1em;
margin-bottom: 1.25em;
margin-right: 1em;
min-height: 50px;
padding: 2px 0px;
background-color: white;
border: 2px solid gray;
float: left;
}
.releeph-pusher-body {
margin-left: 54px;
padding: 1em;
}

View file

@ -0,0 +1,17 @@
/**
* @provides releeph-request-differential-create-dialog
*/
.releeph-request-differential-create-dialog h1 {
color: gray;
font-style: italic;
font-size: 16px;
margin-top: 0.8em;
}
.releeph-request-differential-create-dialog a {
font-weight: bold;
margin-left: 2em;
display: block;
margin-top: 1em;
}

View file

@ -0,0 +1,27 @@
/**
* @provides releeph-request-typeahead-css
*/
.releeph-request-typeahead .commit-id {
color: #aaf; /* blue... */
font-family: monospace;
font-size: 100%;
display: block;
float: left;
}
.releeph-request-typeahead .author-info {
color: #080; /* ...and green, for search results! */
text-align: right;
display: block;
float: right;
padding-left: 1em;
}
.releeph-request-typeahead .focused .author-info {
color: #8b8;
}
.releeph-request-typeahead .summary {
clear: both;
}

View file

@ -0,0 +1,26 @@
/**
* @provides releeph-status
*/
.releeph-status .description {
background: #d3d3d3;
padding: 2px 6px 3px;
margin-right: 4px;
margin-bottom: 5px;
display: block;
float: left;
border-radius: 8px;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
text-decoration: none;
}
.releeph-status .warning {
margin-top: 2px;
margin-left: 0.8em;
float: left;
padding-left: 22px;
background-repeat: no-repeat;
background-size: 16px auto;
background-image: url(/rsrc/custom/image/releeph/releeph_warning.png);
}

View file

@ -0,0 +1,49 @@
/**
* @provides javelin-behavior-releeph-preview-branch
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* javelin-uri
* javelin-util
*/
JX.behavior('releeph-preview-branch', function(config) {
var uri = JX.$U(config.uri);
for (param_name in config.params.static) {
var value = config.params.static[param_name];
uri.setQueryParam(param_name, value);
}
var output = JX.$(config.outputID);
var dynamics = config.params.dynamic;
function renderPreview() {
for (param_name in dynamics) {
var node_id = dynamics[param_name];
var input = JX.$(node_id);
uri.setQueryParam(param_name, input.value);
}
var request = new JX.Request(uri, function(response) {
JX.DOM.setContent(output, JX.$H(response.markup));
});
request.send();
}
renderPreview();
for (ii in dynamics) {
var node_id = dynamics[ii];
var input = JX.$(node_id);
JX.DOM.listen(
input,
['keyup', 'click', 'change'],
null,
function(e) {
renderPreview();
}
);
}
});

View file

@ -0,0 +1,145 @@
/**
* @provides javelin-behavior-releeph-request-state-change
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* javelin-util
* phabricator-keyboard-shortcut
* phabricator-notification
*/
JX.behavior('releeph-request-state-change', function(config) {
var root = JX.DOM.find(document, 'div', 'releeph-request-header-list');
function getRequestHeaderNodes() {
return JX.DOM.scry(root, 'div', 'releeph-request-header');
}
/**
* Keyboard navigation
*/
var keynav_cursor = -1;
var notification = new JX.Notification();
function keynavJump(manager, delta) {
// Calculate this everytime, because the DOM changes.
var headers = getRequestHeaderNodes();
keynav_cursor += delta;
if (keynav_cursor < 0) {
keynav_cursor = -1;
window.scrollTo(0);
keynavMarkup();
return;
}
if (keynav_cursor >= headers.length) {
keynav_cursor = headers.length - 1;
}
var focus = headers[keynav_cursor];
manager.scrollTo(focus);
keynavMarkup();
}
function keynavMarkup() {
var headers = getRequestHeaderNodes();
for (ii in headers) {
JX.DOM.alterClass(headers[ii], 'focus', ii == keynav_cursor);
}
}
function keynavAction(manager, action_name) {
var headers = getRequestHeaderNodes();
var header = headers[keynav_cursor];
if (keynav_cursor < 0) {
return;
}
var sigil = action_name;
var button = JX.DOM.find(header, 'a', sigil);
if (button) {
button.click();
}
}
function keynavNavigateToRequestPage() {
var headers = getRequestHeaderNodes();
var header = headers[keynav_cursor];
JX.DOM.find(header, 'a', 'hidden-link').click();
}
new JX.KeyboardShortcut('j', 'Jump to next request.')
.setHandler(function(manager) {
keynavJump(manager, +1);
})
.register();
new JX.KeyboardShortcut('k', 'Jump to previous request.')
.setHandler(function(manager) {
keynavJump(manager, -1);
})
.register();
new JX.KeyboardShortcut('a', 'Approve the selected request.')
.setHandler(function(manager) {
keynavAction(manager, 'want');
})
.register();
new JX.KeyboardShortcut('r', 'Reject the selected request.')
.setHandler(function(manager) {
keynavAction(manager, 'pass');
})
.register();
new JX.KeyboardShortcut('g', "Open selected request's page in a new tab.")
.setHandler(function(manager) {
keynavNavigateToRequestPage();
})
.register();
/**
* AJAXy state changes for request buttons.
*/
function request_action(node, url) {
var request = new JX.Request(url, function(response) {
if (config.reload) {
window.location.reload();
} else {
var markup = JX.$H(response.markup);
JX.DOM.replace(node, markup);
keynavMarkup();
}
});
request.send();
}
JX.Stratcom.listen(
'click',
'releeph-request-state-change',
function(e) {
var button = e.getNode('releeph-request-state-change');
var node = e.getNode('releeph-request-header');
var url = e.getNodeData('releeph-request-state-change');
// If this button has no action, or we've already responded to the first
// click...
if (!url || button.disabled) {
return;
}
// There's a race condition here though :(
JX.DOM.alterClass(button, 'disabled', true);
button.disabled = true;
e.prevent();
request_action(node, url);
}
);
});

View file

@ -0,0 +1,84 @@
/**
* @provides javelin-behavior-releeph-request-typeahead
* @requires javelin-behavior
* javelin-util
* javelin-dom
* javelin-typeahead
* javelin-tokenizer
* javelin-typeahead-preloaded-source
* javelin-typeahead-ondemand-source
* javelin-dom
* javelin-stratcom
* javelin-util
*/
JX.behavior('releeph-request-typeahead', function(config) {
var root = JX.$(config.id);
var datasource = new JX.TypeaheadOnDemandSource(config.src);
var callsign = config.aux.callsign;
datasource.setAuxiliaryData(config.aux);
datasource.setTransformer(
function(object) {
var full_commit_id = object[0];
var short_commit_id = object[1];
var author = object[2];
var ago = object[3];
var summary = object[4];
var callsign_commit_id = 'r' + callsign + short_commit_id;
var box =
JX.$N(
'div',
{},
[
JX.$N(
'div',
{ className: 'commit-id' },
callsign_commit_id
),
JX.$N(
'div',
{ className: 'author-info' },
ago + ' ago by ' + author
),
JX.$N(
'div',
{ className: 'summary' },
summary
),
]
);
return {
name: callsign_commit_id,
tokenizable: callsign_commit_id + ' '+ short_commit_id + ' ' + summary,
display: box,
uri: null,
id: full_commit_id
};
});
/**
* The default normalizer removes useful control characters that would help
* out search. For example, I was just trying to search for a commit with
* the string "a_file" in the message, which was normalized to "afile".
*/
datasource.setNormalizer(function(query) {
return query;
});
datasource.setMaximumResultCount(config.aux.limit);
var typeahead = new JX.Typeahead(root);
typeahead.setDatasource(datasource);
var placeholder = config.value || config.placeholder;
if (placeholder) {
typeahead.setPlaceholder(placeholder);
}
typeahead.start();
});