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:
parent
daed35e36c
commit
2497e5b5ed
100 changed files with 9774 additions and 4 deletions
92
resources/sql/patches/releeph.sql
Normal file
92
resources/sql/patches/releeph.sql
Normal 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;
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
));
|
||||
|
|
92
src/applications/releeph/ReleephObjectHandleLoader.php
Normal file
92
src/applications/releeph/ReleephObjectHandleLoader.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
9
src/applications/releeph/ReleephPHIDConstants.php
Normal file
9
src/applications/releeph/ReleephPHIDConstants.php
Normal 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';
|
||||
}
|
|
@ -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',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class ReleephCommitFinderException extends Exception {}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
abstract class ConduitAPI_releeph_Method extends ConduitAPIMethod {
|
||||
|
||||
public function getApplication() {
|
||||
return PhabricatorApplication::getByClass('PhabricatorApplicationReleeph');
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.")),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
122
src/applications/releeph/controller/ReleephController.php
Normal file
122
src/applications/releeph/controller/ReleephController.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 · %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 · %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'
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
106
src/applications/releeph/editor/ReleephBranchEditor.php
Normal file
106
src/applications/releeph/editor/ReleephBranchEditor.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
408
src/applications/releeph/editor/ReleephRequestEditor.php
Normal file
408
src/applications/releeph/editor/ReleephRequestEditor.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
213
src/applications/releeph/editor/mail/ReleephRequestMail.php
Normal file
213
src/applications/releeph/editor/mail/ReleephRequestMail.php
Normal 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}";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class ReleephFieldParseException extends Exception {
|
||||
|
||||
public function __construct(ReleephFieldSpecification $field,
|
||||
$message) {
|
||||
|
||||
$name = $field->getName();
|
||||
parent::__construct("{$name}: {$message}");
|
||||
}
|
||||
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
|
@ -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',
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
154
src/applications/releeph/storage/ReleephBranch.php
Normal file
154
src/applications/releeph/storage/ReleephBranch.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
9
src/applications/releeph/storage/ReleephDAO.php
Normal file
9
src/applications/releeph/storage/ReleephDAO.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
abstract class ReleephDAO extends PhabricatorLiskDAO {
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'releeph';
|
||||
}
|
||||
|
||||
}
|
176
src/applications/releeph/storage/ReleephProject.php
Normal file
176
src/applications/releeph/storage/ReleephProject.php
Normal 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;
|
||||
}
|
||||
}
|
309
src/applications/releeph/storage/ReleephRequest.php
Normal file
309
src/applications/releeph/storage/ReleephRequest.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
39
src/applications/releeph/storage/event/ReleephEvent.php
Normal file
39
src/applications/releeph/storage/event/ReleephEvent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class ReleephRequestException extends Exception {}
|
155
src/applications/releeph/view/ReleephProjectView.php
Normal file
155
src/applications/releeph/view/ReleephProjectView.php
Normal 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 · %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);
|
||||
}
|
||||
|
||||
}
|
225
src/applications/releeph/view/branch/ReleephBranchBoxView.php
Normal file
225
src/applications/releeph/view/branch/ReleephBranchBoxView.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
'');
|
||||
}
|
||||
|
||||
}
|
241
src/applications/releeph/view/branch/ReleephBranchTemplate.php
Normal file
241
src/applications/releeph/view/branch/ReleephBranchTemplate.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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(' ');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
final class ReleephDefaultUserView extends ReleephUserView {
|
||||
|
||||
public function render() {
|
||||
return $this->getHandle()->renderLink();
|
||||
}
|
||||
|
||||
}
|
74
src/applications/releeph/view/user/ReleephUserView.php
Normal file
74
src/applications/releeph/view/user/ReleephUserView.php
Normal 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];
|
||||
}
|
||||
|
||||
}
|
|
@ -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'),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
79
webroot/rsrc/css/application/releeph/releeph-branch.css
Normal file
79
webroot/rsrc/css/application/releeph/releeph-branch.css
Normal 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;
|
||||
}
|
35
webroot/rsrc/css/application/releeph/releeph-colors.css
Normal file
35
webroot/rsrc/css/application/releeph/releeph-colors.css
Normal 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;
|
||||
}
|
199
webroot/rsrc/css/application/releeph/releeph-core.css
Normal file
199
webroot/rsrc/css/application/releeph/releeph-core.css
Normal 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;
|
||||
}
|
37
webroot/rsrc/css/application/releeph/releeph-intents.css
Normal file
37
webroot/rsrc/css/application/releeph/releeph-intents.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
25
webroot/rsrc/css/application/releeph/releeph-project.css
Normal file
25
webroot/rsrc/css/application/releeph/releeph-project.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
26
webroot/rsrc/css/application/releeph/releeph-status.css
Normal file
26
webroot/rsrc/css/application/releeph/releeph-status.css
Normal 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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -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();
|
||||
});
|
Loading…
Reference in a new issue