@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+9774 -4
+92
resources/sql/patches/releeph.sql
··· 1 + CREATE TABLE {$NAMESPACE}_releeph.`releeph_project` ( 2 + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 + `dateCreated` int(10) unsigned NOT NULL, 4 + `dateModified` int(10) unsigned NOT NULL, 5 + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 6 + `name` varchar(255) NOT NULL, 7 + `trunkBranch` varchar(255) NOT NULL, 8 + `repositoryID` int(10) unsigned NOT NULL, 9 + `repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 10 + `arcanistProjectID` int(10) unsigned NOT NULL, 11 + `createdByUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 12 + `isActive` tinyint(1) NOT NULL DEFAULT '1', 13 + `projectID` int(10) unsigned DEFAULT NULL, 14 + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 15 + PRIMARY KEY (`id`), 16 + UNIQUE KEY `projectName` (`name`) 17 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 18 + 19 + CREATE TABLE {$NAMESPACE}_releeph.`releeph_branch` ( 20 + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 21 + `dateCreated` int(10) unsigned NOT NULL, 22 + `dateModified` int(10) unsigned NOT NULL, 23 + `basename` varchar(64) NOT NULL, 24 + `releephProjectID` int(10) unsigned NOT NULL, 25 + `createdByUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 26 + `cutPointCommitIdentifier` 27 + varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 28 + `cutPointCommitPHID` 29 + varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 30 + `isActive` tinyint(1) NOT NULL DEFAULT '1', 31 + `symbolicName` varchar(64) DEFAULT NULL, 32 + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 33 + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 34 + `name` varchar(128) NOT NULL, 35 + PRIMARY KEY (`id`), 36 + UNIQUE KEY `releephProjectID_2` (`releephProjectID`,`basename`), 37 + UNIQUE KEY `releephProjectID_name` (`releephProjectID`,`name`), 38 + UNIQUE KEY `releephProjectID` (`releephProjectID`,`symbolicName`) 39 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 40 + 41 + CREATE TABLE {$NAMESPACE}_releeph.`releeph_request` ( 42 + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 43 + `dateCreated` int(10) unsigned NOT NULL, 44 + `dateModified` int(10) unsigned NOT NULL, 45 + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 46 + `branchID` int(10) unsigned NOT NULL, 47 + `summary` longtext CHARACTER SET utf8 COLLATE utf8_bin, 48 + `requestUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 49 + `requestCommitIdentifier` 50 + varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 51 + `requestCommitPHID` 52 + varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 53 + `requestCommitOrdinal` int(10) unsigned NOT NULL, 54 + `commitIdentifier` 55 + varchar(40) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 56 + `committedByUserPHID` 57 + varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 58 + `commitPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 59 + `status` tinyint(4) DEFAULT NULL, 60 + `pickStatus` tinyint(4) DEFAULT NULL, 61 + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 62 + `userIntents` longtext CHARACTER SET utf8 COLLATE utf8_bin, 63 + `inBranch` tinyint(1) NOT NULL DEFAULT '0', 64 + PRIMARY KEY (`id`), 65 + UNIQUE KEY `phid` (`phid`), 66 + UNIQUE KEY `requestIdentifierBranch` (`requestCommitIdentifier`,`branchID`), 67 + KEY `branchID` (`branchID`,`requestCommitOrdinal`) 68 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 69 + 70 + CREATE TABLE {$NAMESPACE}_releeph.`releeph_requestevent` ( 71 + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 72 + `dateCreated` int(10) unsigned NOT NULL, 73 + `dateModified` int(10) unsigned NOT NULL, 74 + `releephRequestID` int(10) unsigned NOT NULL, 75 + `actorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 76 + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 77 + `type` varchar(32) NOT NULL, 78 + PRIMARY KEY (`id`) 79 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 80 + 81 + CREATE TABLE {$NAMESPACE}_releeph.`releeph_event` ( 82 + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 83 + `dateCreated` int(10) unsigned NOT NULL, 84 + `dateModified` int(10) unsigned NOT NULL, 85 + `releephProjectID` int(10) unsigned NOT NULL, 86 + `releephBranchID` int(10) unsigned DEFAULT NULL, 87 + `type` varchar(32) NOT NULL, 88 + `epoch` int(10) unsigned DEFAULT NULL, 89 + `actorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, 90 + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, 91 + PRIMARY KEY (`id`) 92 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+133 -4
src/__celerity_resource_map__.php
··· 1977 1977 ), 1978 1978 'javelin-behavior-pholio-mock-view' => 1979 1979 array( 1980 - 'uri' => '/res/eefc43b3/rsrc/js/application/pholio/behavior-pholio-mock-view.js', 1980 + 'uri' => '/res/ecf5f969/rsrc/js/application/pholio/behavior-pholio-mock-view.js', 1981 1981 'type' => 'js', 1982 1982 'requires' => 1983 1983 array( ··· 2062 2062 ), 2063 2063 'disk' => '/rsrc/js/application/core/behavior-refresh-csrf.js', 2064 2064 ), 2065 + 'javelin-behavior-releeph-preview-branch' => 2066 + array( 2067 + 'uri' => '/res/a77ebc86/rsrc/js/application/releeph/releeph-preview-branch.js', 2068 + 'type' => 'js', 2069 + 'requires' => 2070 + array( 2071 + 0 => 'javelin-behavior', 2072 + 1 => 'javelin-dom', 2073 + 2 => 'javelin-stratcom', 2074 + 3 => 'javelin-uri', 2075 + 4 => 'javelin-util', 2076 + ), 2077 + 'disk' => '/rsrc/js/application/releeph/releeph-preview-branch.js', 2078 + ), 2079 + 'javelin-behavior-releeph-request-state-change' => 2080 + array( 2081 + 'uri' => '/res/38f96ba8/rsrc/js/application/releeph/releeph-request-state-change.js', 2082 + 'type' => 'js', 2083 + 'requires' => 2084 + array( 2085 + 0 => 'javelin-behavior', 2086 + 1 => 'javelin-dom', 2087 + 2 => 'javelin-stratcom', 2088 + 3 => 'javelin-util', 2089 + 4 => 'phabricator-keyboard-shortcut', 2090 + 5 => 'phabricator-notification', 2091 + ), 2092 + 'disk' => '/rsrc/js/application/releeph/releeph-request-state-change.js', 2093 + ), 2094 + 'javelin-behavior-releeph-request-typeahead' => 2095 + array( 2096 + 'uri' => '/res/b52096e2/rsrc/js/application/releeph/releeph-request-typeahead.js', 2097 + 'type' => 'js', 2098 + 'requires' => 2099 + array( 2100 + 0 => 'javelin-behavior', 2101 + 1 => 'javelin-util', 2102 + 2 => 'javelin-dom', 2103 + 3 => 'javelin-typeahead', 2104 + 4 => 'javelin-tokenizer', 2105 + 5 => 'javelin-typeahead-preloaded-source', 2106 + 6 => 'javelin-typeahead-ondemand-source', 2107 + 7 => 'javelin-dom', 2108 + 8 => 'javelin-stratcom', 2109 + 9 => 'javelin-util', 2110 + ), 2111 + 'disk' => '/rsrc/js/application/releeph/releeph-request-typeahead.js', 2112 + ), 2065 2113 'javelin-behavior-repository-crossreference' => 2066 2114 array( 2067 2115 'uri' => '/res/4b5fab1c/rsrc/js/application/repository/repository-crossreference.js', ··· 2637 2685 ), 2638 2686 'paste-css' => 2639 2687 array( 2640 - 'uri' => '/res/5081cf13/rsrc/css/application/paste/paste.css', 2688 + 'uri' => '/res/044639be/rsrc/css/application/paste/paste.css', 2641 2689 'type' => 'css', 2642 2690 'requires' => 2643 2691 array( ··· 3099 3147 ), 3100 3148 'phabricator-source-code-view-css' => 3101 3149 array( 3102 - 'uri' => '/res/9373e769/rsrc/css/layout/phabricator-source-code-view.css', 3150 + 'uri' => '/res/979d5280/rsrc/css/layout/phabricator-source-code-view.css', 3103 3151 'type' => 'css', 3104 3152 'requires' => 3105 3153 array( ··· 3336 3384 ), 3337 3385 'pholio-css' => 3338 3386 array( 3339 - 'uri' => '/res/bc10bf21/rsrc/css/application/pholio/pholio.css', 3387 + 'uri' => '/res/b0947e46/rsrc/css/application/pholio/pholio.css', 3340 3388 'type' => 'css', 3341 3389 'requires' => 3342 3390 array( ··· 3432 3480 array( 3433 3481 ), 3434 3482 'disk' => '/rsrc/js/raphael/g.raphael.line.js', 3483 + ), 3484 + 'releeph-branch' => 3485 + array( 3486 + 'uri' => '/res/6ad6420d/rsrc/css/application/releeph/releeph-branch.css', 3487 + 'type' => 'css', 3488 + 'requires' => 3489 + array( 3490 + ), 3491 + 'disk' => '/rsrc/css/application/releeph/releeph-branch.css', 3492 + ), 3493 + 'releeph-colors' => 3494 + array( 3495 + 'uri' => '/res/dff4b26a/rsrc/css/application/releeph/releeph-colors.css', 3496 + 'type' => 'css', 3497 + 'requires' => 3498 + array( 3499 + ), 3500 + 'disk' => '/rsrc/css/application/releeph/releeph-colors.css', 3501 + ), 3502 + 'releeph-core' => 3503 + array( 3504 + 'uri' => '/res/853f4a73/rsrc/css/application/releeph/releeph-core.css', 3505 + 'type' => 'css', 3506 + 'requires' => 3507 + array( 3508 + ), 3509 + 'disk' => '/rsrc/css/application/releeph/releeph-core.css', 3510 + ), 3511 + 'releeph-intents' => 3512 + array( 3513 + 'uri' => '/res/4e73e9dd/rsrc/css/application/releeph/releeph-intents.css', 3514 + 'type' => 'css', 3515 + 'requires' => 3516 + array( 3517 + ), 3518 + 'disk' => '/rsrc/css/application/releeph/releeph-intents.css', 3519 + ), 3520 + 'releeph-preview-branch' => 3521 + array( 3522 + 'uri' => '/res/65e5dece/rsrc/css/application/releeph/releeph-preview-branch.css', 3523 + 'type' => 'css', 3524 + 'requires' => 3525 + array( 3526 + ), 3527 + 'disk' => '/rsrc/css/application/releeph/releeph-preview-branch.css', 3528 + ), 3529 + 'releeph-project' => 3530 + array( 3531 + 'uri' => '/res/b9376e59/rsrc/css/application/releeph/releeph-project.css', 3532 + 'type' => 'css', 3533 + 'requires' => 3534 + array( 3535 + ), 3536 + 'disk' => '/rsrc/css/application/releeph/releeph-project.css', 3537 + ), 3538 + 'releeph-request-differential-create-dialog' => 3539 + array( 3540 + 'uri' => '/res/4df30ce1/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css', 3541 + 'type' => 'css', 3542 + 'requires' => 3543 + array( 3544 + ), 3545 + 'disk' => '/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css', 3546 + ), 3547 + 'releeph-request-typeahead-css' => 3548 + array( 3549 + 'uri' => '/res/9c9a1acf/rsrc/css/application/releeph/releeph-request-typeahead.css', 3550 + 'type' => 'css', 3551 + 'requires' => 3552 + array( 3553 + ), 3554 + 'disk' => '/rsrc/css/application/releeph/releeph-request-typeahead.css', 3555 + ), 3556 + 'releeph-status' => 3557 + array( 3558 + 'uri' => '/res/588529df/rsrc/css/application/releeph/releeph-status.css', 3559 + 'type' => 'css', 3560 + 'requires' => 3561 + array( 3562 + ), 3563 + 'disk' => '/rsrc/css/application/releeph/releeph-status.css', 3435 3564 ), 3436 3565 'setup-issue-css' => 3437 3566 array(
+159
src/__phutil_library_map__.php
··· 187 187 'ConduitAPI_phriction_info_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_info_Method.php', 188 188 'ConduitAPI_project_Method' => 'applications/project/conduit/ConduitAPI_project_Method.php', 189 189 'ConduitAPI_project_query_Method' => 'applications/project/conduit/ConduitAPI_project_query_Method.php', 190 + 'ConduitAPI_releeph_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_Method.php', 191 + 'ConduitAPI_releeph_getbranches_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_getbranches_Method.php', 192 + 'ConduitAPI_releeph_projectinfo_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_projectinfo_Method.php', 193 + 'ConduitAPI_releeph_request_Method' => 'applications/releeph/conduit/ConduitAPI_releeph_request_Method.php', 194 + 'ConduitAPI_releephwork_canpush_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_canpush_Method.php', 195 + 'ConduitAPI_releephwork_getauthorinfo_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getauthorinfo_Method.php', 196 + 'ConduitAPI_releephwork_getbranch_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getbranch_Method.php', 197 + 'ConduitAPI_releephwork_getbranchcommitmessage_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getbranchcommitmessage_Method.php', 198 + 'ConduitAPI_releephwork_getcommitmessage_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getcommitmessage_Method.php', 199 + 'ConduitAPI_releephwork_getorigcommitmessage_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_getorigcommitmessage_Method.php', 200 + 'ConduitAPI_releephwork_nextrequest_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_nextrequest_Method.php', 201 + 'ConduitAPI_releephwork_record_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_record_Method.php', 202 + 'ConduitAPI_releephwork_recordpickstatus_Method' => 'applications/releeph/conduit/work/ConduitAPI_releephwork_recordpickstatus_Method.php', 190 203 'ConduitAPI_remarkup_process_Method' => 'applications/remarkup/conduit/ConduitAPI_remarkup_process_Method.php', 191 204 'ConduitAPI_repository_Method' => 'applications/repository/conduit/ConduitAPI_repository_Method.php', 192 205 'ConduitAPI_repository_create_Method' => 'applications/repository/conduit/ConduitAPI_repository_create_Method.php', ··· 333 346 'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php', 334 347 'DifferentialPeopleMenuEventListener' => 'applications/differential/events/DifferentialPeopleMenuEventListener.php', 335 348 'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php', 349 + 'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php', 336 350 'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php', 337 351 'DifferentialReplyHandler' => 'applications/differential/DifferentialReplyHandler.php', 338 352 'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php', ··· 690 704 'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php', 691 705 'PhabricatorApplicationPonder' => 'applications/ponder/application/PhabricatorApplicationPonder.php', 692 706 'PhabricatorApplicationProject' => 'applications/project/application/PhabricatorApplicationProject.php', 707 + 'PhabricatorApplicationReleeph' => 'applications/releeph/application/PhabricatorApplicationReleeph.php', 708 + 'PhabricatorApplicationReleephConfigOptions' => 'applications/releeph/config/PhabricatorApplicationReleephConfigOptions.php', 693 709 'PhabricatorApplicationRepositories' => 'applications/repository/application/PhabricatorApplicationRepositories.php', 694 710 'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php', 695 711 'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php', ··· 1561 1577 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', 1562 1578 'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php', 1563 1579 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', 1580 + 'ReleephActiveProjectListView' => 'applications/releeph/view/project/list/ReleephActiveProjectListView.php', 1581 + 'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php', 1582 + 'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php', 1583 + 'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php', 1584 + 'ReleephBranchBoxView' => 'applications/releeph/view/branch/ReleephBranchBoxView.php', 1585 + 'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php', 1586 + 'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php', 1587 + 'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php', 1588 + 'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php', 1589 + 'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php', 1590 + 'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php', 1591 + 'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php', 1592 + 'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php', 1593 + 'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php', 1594 + 'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php', 1595 + 'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php', 1596 + 'ReleephController' => 'applications/releeph/controller/ReleephController.php', 1597 + 'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php', 1598 + 'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php', 1599 + 'ReleephDefaultUserView' => 'applications/releeph/view/user/ReleephDefaultUserView.php', 1600 + 'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php', 1601 + 'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php', 1602 + 'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php', 1603 + 'ReleephDifferentialRevisionDetailRenderer' => 'applications/releeph/differential/ReleephDifferentialRevisionDetailRenderer.php', 1604 + 'ReleephEvent' => 'applications/releeph/storage/event/ReleephEvent.php', 1605 + 'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php', 1606 + 'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php', 1607 + 'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php', 1608 + 'ReleephFieldSpecificationIncompleteException' => 'applications/releeph/field/exception/ReleephFieldSpecificationIncompleteException.php', 1609 + 'ReleephInactiveProjectListView' => 'applications/releeph/view/project/list/ReleephInactiveProjectListView.php', 1610 + 'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php', 1611 + 'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php', 1612 + 'ReleephObjectHandleLoader' => 'applications/releeph/ReleephObjectHandleLoader.php', 1613 + 'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php', 1614 + 'ReleephPHIDConstants' => 'applications/releeph/ReleephPHIDConstants.php', 1615 + 'ReleephProject' => 'applications/releeph/storage/ReleephProject.php', 1616 + 'ReleephProjectActionController' => 'applications/releeph/controller/project/ReleephProjectActionController.php', 1617 + 'ReleephProjectCreateController' => 'applications/releeph/controller/project/ReleephProjectCreateController.php', 1618 + 'ReleephProjectEditController' => 'applications/releeph/controller/project/ReleephProjectEditController.php', 1619 + 'ReleephProjectListController' => 'applications/releeph/controller/project/ReleephProjectListController.php', 1620 + 'ReleephProjectView' => 'applications/releeph/view/ReleephProjectView.php', 1621 + 'ReleephProjectViewController' => 'applications/releeph/controller/project/ReleephProjectViewController.php', 1622 + 'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php', 1623 + 'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php', 1624 + 'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php', 1625 + 'ReleephRequestCreateController' => 'applications/releeph/controller/request/ReleephRequestCreateController.php', 1626 + 'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php', 1627 + 'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php', 1628 + 'ReleephRequestEditor' => 'applications/releeph/editor/ReleephRequestEditor.php', 1629 + 'ReleephRequestEvent' => 'applications/releeph/storage/request/ReleephRequestEvent.php', 1630 + 'ReleephRequestEventListView' => 'applications/releeph/view/requestevent/ReleephRequestEventListView.php', 1631 + 'ReleephRequestException' => 'applications/releeph/storage/request/exception/ReleephRequestException.php', 1632 + 'ReleephRequestHeaderListView' => 'applications/releeph/view/request/header/ReleephRequestHeaderListView.php', 1633 + 'ReleephRequestHeaderView' => 'applications/releeph/view/request/header/ReleephRequestHeaderView.php', 1634 + 'ReleephRequestIntentsView' => 'applications/releeph/view/request/ReleephRequestIntentsView.php', 1635 + 'ReleephRequestMail' => 'applications/releeph/editor/mail/ReleephRequestMail.php', 1636 + 'ReleephRequestStatusView' => 'applications/releeph/view/request/ReleephRequestStatusView.php', 1637 + 'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php', 1638 + 'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php', 1639 + 'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php', 1640 + 'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php', 1641 + 'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php', 1642 + 'ReleephRiskFieldSpecification' => 'applications/releeph/field/specification/ReleephRiskFieldSpecification.php', 1643 + 'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php', 1644 + 'ReleephStatusFieldSpecification' => 'applications/releeph/field/specification/ReleephStatusFieldSpecification.php', 1645 + 'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php', 1646 + 'ReleephUserView' => 'applications/releeph/view/user/ReleephUserView.php', 1564 1647 ), 1565 1648 'function' => 1566 1649 array( ··· 1758 1841 'ConduitAPI_phriction_info_Method' => 'ConduitAPI_phriction_Method', 1759 1842 'ConduitAPI_project_Method' => 'ConduitAPIMethod', 1760 1843 'ConduitAPI_project_query_Method' => 'ConduitAPI_project_Method', 1844 + 'ConduitAPI_releeph_Method' => 'ConduitAPIMethod', 1845 + 'ConduitAPI_releeph_getbranches_Method' => 'ConduitAPI_releeph_Method', 1846 + 'ConduitAPI_releeph_projectinfo_Method' => 'ConduitAPI_releeph_Method', 1847 + 'ConduitAPI_releeph_request_Method' => 'ConduitAPI_releeph_Method', 1848 + 'ConduitAPI_releephwork_canpush_Method' => 'ConduitAPI_releeph_Method', 1849 + 'ConduitAPI_releephwork_getauthorinfo_Method' => 'ConduitAPI_releeph_Method', 1850 + 'ConduitAPI_releephwork_getbranch_Method' => 'ConduitAPI_releeph_Method', 1851 + 'ConduitAPI_releephwork_getbranchcommitmessage_Method' => 'ConduitAPI_releeph_Method', 1852 + 'ConduitAPI_releephwork_getcommitmessage_Method' => 'ConduitAPI_releeph_Method', 1853 + 'ConduitAPI_releephwork_getorigcommitmessage_Method' => 'ConduitAPI_releeph_Method', 1854 + 'ConduitAPI_releephwork_nextrequest_Method' => 'ConduitAPI_releeph_Method', 1855 + 'ConduitAPI_releephwork_record_Method' => 'ConduitAPI_releeph_Method', 1856 + 'ConduitAPI_releephwork_recordpickstatus_Method' => 'ConduitAPI_releeph_Method', 1761 1857 'ConduitAPI_remarkup_process_Method' => 'ConduitAPIMethod', 1762 1858 'ConduitAPI_repository_Method' => 'ConduitAPIMethod', 1763 1859 'ConduitAPI_repository_create_Method' => 'ConduitAPI_repository_Method', ··· 1899 1995 'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification', 1900 1996 'DifferentialPeopleMenuEventListener' => 'PhutilEventListener', 1901 1997 'DifferentialPrimaryPaneView' => 'AphrontView', 1998 + 'DifferentialReleephRequestFieldSpecification' => 'DifferentialFieldSpecification', 1902 1999 'DifferentialRemarkupRule' => 'PhabricatorRemarkupRuleObject', 1903 2000 'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler', 1904 2001 'DifferentialResultsTableView' => 'AphrontView', ··· 2216 2313 'PhabricatorApplicationPhriction' => 'PhabricatorApplication', 2217 2314 'PhabricatorApplicationPonder' => 'PhabricatorApplication', 2218 2315 'PhabricatorApplicationProject' => 'PhabricatorApplication', 2316 + 'PhabricatorApplicationReleeph' => 'PhabricatorApplication', 2317 + 'PhabricatorApplicationReleephConfigOptions' => 'PhabricatorApplicationConfigOptions', 2219 2318 'PhabricatorApplicationRepositories' => 'PhabricatorApplication', 2220 2319 'PhabricatorApplicationSettings' => 'PhabricatorApplication', 2221 2320 'PhabricatorApplicationSlowvote' => 'PhabricatorApplication', ··· 3107 3206 'PonderVoteEditor' => 'PhabricatorEditor', 3108 3207 'PonderVoteSaveController' => 'PonderController', 3109 3208 'QueryFormattingTestCase' => 'PhabricatorTestCase', 3209 + 'ReleephActiveProjectListView' => 'AphrontView', 3210 + 'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification', 3211 + 'ReleephBranch' => 'ReleephDAO', 3212 + 'ReleephBranchAccessController' => 'ReleephController', 3213 + 'ReleephBranchBoxView' => 'AphrontView', 3214 + 'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification', 3215 + 'ReleephBranchCreateController' => 'ReleephController', 3216 + 'ReleephBranchEditController' => 'ReleephController', 3217 + 'ReleephBranchEditor' => 'PhabricatorEditor', 3218 + 'ReleephBranchNamePreviewController' => 'PhabricatorController', 3219 + 'ReleephBranchPreviewView' => 'AphrontFormControl', 3220 + 'ReleephBranchViewController' => 'ReleephController', 3221 + 'ReleephCommitFinderException' => 'Exception', 3222 + 'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification', 3223 + 'ReleephController' => 'PhabricatorController', 3224 + 'ReleephDAO' => 'PhabricatorLiskDAO', 3225 + 'ReleephDefaultFieldSelector' => 'ReleephFieldSelector', 3226 + 'ReleephDefaultUserView' => 'ReleephUserView', 3227 + 'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification', 3228 + 'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification', 3229 + 'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification', 3230 + 'ReleephEvent' => 'ReleephDAO', 3231 + 'ReleephFieldParseException' => 'Exception', 3232 + 'ReleephFieldSpecificationIncompleteException' => 'Exception', 3233 + 'ReleephInactiveProjectListView' => 'AphrontView', 3234 + 'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification', 3235 + 'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification', 3236 + 'ReleephObjectHandleLoader' => 'ObjectHandleLoader', 3237 + 'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification', 3238 + 'ReleephProject' => 'ReleephDAO', 3239 + 'ReleephProjectActionController' => 'ReleephController', 3240 + 'ReleephProjectCreateController' => 'ReleephController', 3241 + 'ReleephProjectEditController' => 'ReleephController', 3242 + 'ReleephProjectListController' => 'PhabricatorController', 3243 + 'ReleephProjectView' => 'AphrontView', 3244 + 'ReleephProjectViewController' => 'ReleephController', 3245 + 'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification', 3246 + 'ReleephRequest' => 'ReleephDAO', 3247 + 'ReleephRequestActionController' => 'ReleephController', 3248 + 'ReleephRequestCreateController' => 'ReleephController', 3249 + 'ReleephRequestDifferentialCreateController' => 'ReleephController', 3250 + 'ReleephRequestEditController' => 'ReleephController', 3251 + 'ReleephRequestEditor' => 'PhabricatorEditor', 3252 + 'ReleephRequestEvent' => 'ReleephDAO', 3253 + 'ReleephRequestEventListView' => 'AphrontView', 3254 + 'ReleephRequestException' => 'Exception', 3255 + 'ReleephRequestHeaderListView' => 'AphrontView', 3256 + 'ReleephRequestHeaderView' => 'AphrontView', 3257 + 'ReleephRequestIntentsView' => 'AphrontView', 3258 + 'ReleephRequestStatusView' => 'AphrontView', 3259 + 'ReleephRequestTypeaheadControl' => 'AphrontFormControl', 3260 + 'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController', 3261 + 'ReleephRequestViewController' => 'ReleephController', 3262 + 'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification', 3263 + 'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification', 3264 + 'ReleephRiskFieldSpecification' => 'ReleephFieldSpecification', 3265 + 'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification', 3266 + 'ReleephStatusFieldSpecification' => 'ReleephFieldSpecification', 3267 + 'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification', 3268 + 'ReleephUserView' => 'AphrontView', 3110 3269 ), 3111 3270 ));
+92
src/applications/releeph/ReleephObjectHandleLoader.php
··· 1 + <?php 2 + 3 + final class ReleephObjectHandleLoader extends ObjectHandleLoader { 4 + 5 + /** 6 + * The intention for phid.external-loaders is for each new 4-char PHID type 7 + * to point to a different external loader for that type. 8 + * 9 + * For brevity, we instead just have this one class that can load any type of 10 + * Releeph PHID. 11 + */ 12 + 13 + public function loadHandles(array $phids) { 14 + $types = array(); 15 + 16 + foreach ($phids as $phid) { 17 + $type = phid_get_type($phid); 18 + $types[$type][] = $phid; 19 + } 20 + 21 + $handles = array(); 22 + 23 + foreach ($types as $type => $phids) { 24 + switch ($type) { 25 + case ReleephPHIDConstants::PHID_TYPE_RERQ: 26 + $object = new ReleephRequest(); 27 + 28 + $instances = $object->loadAllWhere('phid in (%Ls)', $phids); 29 + $instances = mpull($instances, null, 'getPHID'); 30 + 31 + foreach ($phids as $phid) { 32 + $instance = $instances[$phid]; 33 + $handle = new PhabricatorObjectHandle(); 34 + $handle->setPHID($phid); 35 + $handle->setType($type); 36 + $handle->setURI('/RQ'.$instance->getID()); 37 + 38 + $name = 'RQ'.$instance->getID(); 39 + $handle->setName($name); 40 + $handle->setFullName($name.': '.$instance->getSummaryForDisplay()); 41 + $handle->setComplete(true); 42 + 43 + $handles[$phid] = $handle; 44 + } 45 + break; 46 + 47 + case ReleephPHIDConstants::PHID_TYPE_REBR: 48 + $object = new ReleephBranch(); 49 + 50 + $branches = $object->loadAllWhere('phid IN (%Ls)', $phids); 51 + $branches = mpull($branches, null, 'getPHID'); 52 + 53 + foreach ($phids as $phid) { 54 + $branch = $branches[$phid]; 55 + $handle = new PhabricatorObjectHandle(); 56 + $handle->setPHID($phid); 57 + $handle->setType($type); 58 + $handle->setURI($branch->getURI()); 59 + $handle->setName($branch->getBasename()); 60 + $handle->setFullName($branch->getName()); 61 + $handle->setComplete(true); 62 + $handles[$phid] = $handle; 63 + } 64 + break; 65 + 66 + case ReleephPHIDConstants::PHID_TYPE_REPR: 67 + $object = new ReleephProject(); 68 + 69 + $instances = $object->loadAllWhere('phid IN (%Ls)', $phids); 70 + $instances = mpull($instances, null, 'getPHID'); 71 + 72 + foreach ($phids as $phid) { 73 + $instance = $instances[$phid]; 74 + $handle = new PhabricatorObjectHandle(); 75 + $handle->setPHID($phid); 76 + $handle->setType($type); 77 + $handle->setURI($instance->getURI()); 78 + $handle->setName($instance->getName()); // no fullName for proejcts 79 + $handle->setComplete(true); 80 + $handles[$phid] = $handle; 81 + } 82 + break; 83 + 84 + default: 85 + throw new Exception('unknown type '.$type); 86 + } 87 + } 88 + 89 + return $handles; 90 + } 91 + 92 + }
+9
src/applications/releeph/ReleephPHIDConstants.php
··· 1 + <?php 2 + 3 + final class ReleephPHIDConstants { 4 + 5 + // Releeph 6 + const PHID_TYPE_REPR = 'REPR'; 7 + const PHID_TYPE_REBR = 'REBR'; 8 + const PHID_TYPE_RERQ = 'RERQ'; 9 + }
+86
src/applications/releeph/application/PhabricatorApplicationReleeph.php
··· 1 + <?php 2 + 3 + final class PhabricatorApplicationReleeph extends PhabricatorApplication { 4 + 5 + public function getName() { 6 + return 'Releeph'; 7 + } 8 + 9 + public function getShortDescription() { 10 + return 'Release Branches'; 11 + } 12 + 13 + public function getBaseURI() { 14 + return '/releeph/'; 15 + } 16 + 17 + public function getAutospriteName() { 18 + return 'releeph'; 19 + } 20 + 21 + public function getApplicationGroup() { 22 + return self::GROUP_ORGANIZATION; 23 + } 24 + 25 + public function isInstalled() { 26 + if (PhabricatorEnv::getEnvConfig('releeph.installed')) { 27 + return parent::isInstalled(); 28 + } 29 + return false; 30 + } 31 + 32 + public function getRoutes() { 33 + return array( 34 + '/RQ(?P<requestID>[1-9]\d*)' => 'ReleephRequestViewController', 35 + '/releeph/' => array( 36 + '' => 'ReleephProjectListController', 37 + 'project/' => array( 38 + '' => 'ReleephProjectListController', 39 + 'inactive/' => 'ReleephProjectListController', 40 + 'create/' => 'ReleephProjectCreateController', 41 + '(?P<projectID>[1-9]\d*)/' => array( 42 + '' => 'ReleephProjectViewController', 43 + 'closedbranches/' => 'ReleephProjectViewController', 44 + 'edit/' => 'ReleephProjectEditController', 45 + 'cutbranch/' => 'ReleephBranchCreateController', 46 + 'action/(?P<action>.+)/' => 'ReleephProjectActionController', 47 + ), 48 + ), 49 + 'branch/' => array( 50 + 'edit/(?P<branchID>[1-9]\d*)/' => 51 + 'ReleephBranchEditController', 52 + '(?P<action>close|re-open)/(?P<branchID>[1-9]\d*)/' => 53 + 'ReleephBranchAccessController', 54 + 'preview/' => 'ReleephBranchNamePreviewController', 55 + 56 + // Left in, just in case the by-name stuff fails! 57 + '(?P<branchID>[^/]+)/' => 58 + 'ReleephBranchViewController', 59 + ), 60 + 'request/' => array( 61 + '(?P<requestID>[1-9]\d*)/' => 'ReleephRequestViewController', 62 + 'create/' => 'ReleephRequestCreateController', 63 + 'differentialcreate/' => array( 64 + 'D(?P<diffRevID>[1-9]\d*)' => 65 + 'ReleephRequestDifferentialCreateController', 66 + ), 67 + 'edit/(?P<requestID>[1-9]\d*)/' => 68 + 'ReleephRequestEditController', 69 + 'action/(?P<action>.+)/(?P<requestID>[1-9]\d*)/' => 70 + 'ReleephRequestActionController', 71 + 'typeahead/' => 72 + 'ReleephRequestTypeaheadController', 73 + ), 74 + 75 + // Branch navigation made pretty, as it's the most common: 76 + '(?P<projectName>[^/]+)/(?P<branchName>[^/]+)/' => array( 77 + '' => 'ReleephBranchViewController', 78 + 'edit/' => 'ReleephBranchEditController', 79 + 'request/' => 'ReleephRequestCreateController', 80 + '(?P<action>close|re-open)/' => 'ReleephBranchAccessController', 81 + ), 82 + ) 83 + ); 84 + } 85 + 86 + }
+74
src/applications/releeph/commitfinder/ReleephCommitFinder.php
··· 1 + <?php 2 + 3 + final class ReleephCommitFinder { 4 + 5 + private $releephProject; 6 + 7 + public function setReleephProject(ReleephProject $rp) { 8 + $this->releephProject = $rp; 9 + return $this; 10 + } 11 + 12 + public function fromPartial($partial_string) { 13 + // Look for diffs 14 + $matches = array(); 15 + if (preg_match('/^D([1-9]\d*)$/', $partial_string, $matches)) { 16 + $diff_id = $matches[1]; 17 + $diff_rev = id(new DifferentialRevision())->load($diff_id); 18 + if (!$diff_rev) { 19 + throw new ReleephCommitFinderException( 20 + "{$partial_string} does not refer to an existing diff."); 21 + } 22 + $commit_phids = $diff_rev->loadCommitPHIDs(); 23 + 24 + if (!$commit_phids) { 25 + throw new ReleephCommitFinderException( 26 + "{$partial_string} has no commits associated with it yet."); 27 + } 28 + 29 + $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 30 + 'phid IN (%Ls) ORDER BY epoch ASC', 31 + $commit_phids); 32 + return head($commits); 33 + } 34 + 35 + // Look for a raw commit number, or r<callsign><commit-number>. 36 + $repository = $this->releephProject->loadPhabricatorRepository(); 37 + $dr_data = null; 38 + $matches = array(); 39 + if (preg_match('/^r(?P<callsign>[A-Z]+)(?P<commit>\w+)$/', 40 + $partial_string, $matches)) { 41 + $callsign = $matches['callsign']; 42 + if ($callsign != $repository->getCallsign()) { 43 + throw new ReleephCommitFinderException(sprintf( 44 + "%s is in a different repository to this Releeph project (%s).", 45 + $partial_string, 46 + $repository->getCallsign())); 47 + } else { 48 + $dr_data = $matches; 49 + } 50 + } else { 51 + $dr_data = array( 52 + 'callsign' => $repository->getCallsign(), 53 + 'commit' => $partial_string 54 + ); 55 + } 56 + 57 + try { 58 + $dr = DiffusionRequest::newFromDictionary($dr_data); 59 + } catch (Exception $ex) { 60 + $message = "No commit matches {$partial_string}: ".$ex->getMessage(); 61 + throw new ReleephCommitFinderException($message); 62 + } 63 + 64 + $phabricator_repository_commit = $dr->loadCommit(); 65 + 66 + if (!$phabricator_repository_commit) { 67 + throw new ReleephCommitFinderException( 68 + "The commit {$partial_string} doesn't exist in this repository."); 69 + } 70 + 71 + return $phabricator_repository_commit; 72 + } 73 + 74 + }
+3
src/applications/releeph/commitfinder/ReleephCommitFinderException.php
··· 1 + <?php 2 + 3 + final class ReleephCommitFinderException extends Exception {}
+9
src/applications/releeph/conduit/ConduitAPI_releeph_Method.php
··· 1 + <?php 2 + 3 + abstract class ConduitAPI_releeph_Method extends ConduitAPIMethod { 4 + 5 + public function getApplication() { 6 + return PhabricatorApplication::getByClass('PhabricatorApplicationReleeph'); 7 + } 8 + 9 + }
+62
src/applications/releeph/conduit/ConduitAPI_releeph_getbranches_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releeph_getbranches_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodDescription() { 7 + return "Return information about all active Releeph branches."; 8 + } 9 + 10 + public function defineParamTypes() { 11 + return array( 12 + ); 13 + } 14 + 15 + public function defineReturnType() { 16 + return 'nonempty list<dict<string, wild>>'; 17 + } 18 + 19 + public function defineErrorTypes() { 20 + return array( 21 + ); 22 + } 23 + 24 + protected function execute(ConduitAPIRequest $request) { 25 + $results = array(); 26 + 27 + $projects = id(new ReleephProject())->loadAllWhere('isActive = 1'); 28 + 29 + foreach ($projects as $project) { 30 + $repository = $project->loadOneRelative( 31 + id(new PhabricatorRepository()), 32 + 'id', 33 + 'getRepositoryID'); 34 + 35 + $branches = $project->loadRelatives( 36 + id(new ReleephBranch()), 37 + 'releephProjectID', 38 + 'getID', 39 + 'isActive = 1'); 40 + 41 + foreach ($branches as $branch) { 42 + $full_branch_name = $branch->getName(); 43 + 44 + $cut_point_commit = $branch->loadOneRelative( 45 + id(new PhabricatorRepositoryCommit()), 46 + 'phid', 47 + 'getCutPointCommitPHID'); 48 + 49 + $results[] = array( 50 + 'project' => $project->getName(), 51 + 'repository' => $repository->getCallsign(), 52 + 'branch' => $branch->getBasename(), 53 + 'fullBranchName' => $full_branch_name, 54 + 'symbolicName' => $branch->getSymbolicName(), 55 + 'cutPoint' => $branch->getCutPointCommitIdentifier(), 56 + ); 57 + } 58 + } 59 + 60 + return $results; 61 + } 62 + }
+96
src/applications/releeph/conduit/ConduitAPI_releeph_projectinfo_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releeph_projectinfo_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodDescription() { 7 + return 8 + "Fetch information about all Releeph projects ". 9 + "for a given Arcanist project."; 10 + } 11 + 12 + public function defineParamTypes() { 13 + return array( 14 + 'arcProjectName' => 'optional string', 15 + ); 16 + } 17 + 18 + public function defineReturnType() { 19 + return 'dict<string, wild>'; 20 + } 21 + 22 + public function defineErrorTypes() { 23 + return array( 24 + "ERR_UNKNOWN_ARC" => 25 + "The given Arcanist project name doesn't exist in the ". 26 + "installation of Phabricator you are accessing.", 27 + ); 28 + } 29 + 30 + protected function execute(ConduitAPIRequest $request) { 31 + $arc_project_name = $request->getValue('arcProjectName'); 32 + if ($arc_project_name) { 33 + $arc_project = id(new PhabricatorRepositoryArcanistProject()) 34 + ->loadOneWhere('name = %s', $arc_project_name); 35 + if (!$arc_project) { 36 + throw id(new ConduitException("ERR_UNKNOWN_ARC")) 37 + ->setErrorDescription( 38 + "Unknown Arcanist project '{$arc_project_name}': ". 39 + "are you using the correct Conduit URI?"); 40 + } 41 + 42 + $releeph_projects = id(new ReleephProject()) 43 + ->loadAllWhere('arcanistProjectID = %d', $arc_project->getID()); 44 + } else { 45 + $releeph_projects = id(new ReleephProject())->loadAll(); 46 + } 47 + 48 + $releeph_projects = mfilter($releeph_projects, 'getIsActive'); 49 + 50 + $result = array(); 51 + foreach ($releeph_projects as $releeph_project) { 52 + $selector = $releeph_project->getReleephFieldSelector(); 53 + $fields = $selector->getFieldSpecifications(); 54 + 55 + $fields_info = array(); 56 + foreach ($fields as $field) { 57 + $field->setReleephProject($releeph_project); 58 + if ($field->isEditable()) { 59 + $key = $field->getKeyForConduit(); 60 + $fields_info[$key] = array( 61 + 'class' => get_class($field), 62 + 'name' => $field->getName(), 63 + 'key' => $key, 64 + 'arcHelp' => $field->renderHelpForArcanist(), 65 + ); 66 + } 67 + } 68 + 69 + $releeph_branches = mfilter( 70 + id(new ReleephBranch()) 71 + ->loadAllWhere('releephProjectID = %d', $releeph_project->getID()), 72 + 'getIsActive'); 73 + 74 + $releeph_branches_struct = array(); 75 + foreach ($releeph_branches as $branch) { 76 + $releeph_branches_struct[] = array( 77 + 'branchName' => $branch->getName(), 78 + 'projectName' => $releeph_project->getName(), 79 + 'projectPHID' => $releeph_project->getPHID(), 80 + 'branchPHID' => $branch->getPHID(), 81 + ); 82 + } 83 + 84 + $result[] = array( 85 + 'projectName' => $releeph_project->getName(), 86 + 'projectPHID' => $releeph_project->getPHID(), 87 + 'branches' => $releeph_branches_struct, 88 + 'fields' => $fields_info, 89 + ); 90 + } 91 + 92 + return $result; 93 + } 94 + 95 + 96 + }
+130
src/applications/releeph/conduit/ConduitAPI_releeph_request_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releeph_request_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodDescription() { 7 + return "Request a commit or diff to be picked to a branch."; 8 + } 9 + 10 + public function defineParamTypes() { 11 + return array( 12 + 'branchPHID' => 'required string', 13 + 'things' => 'required string', 14 + 'fields' => 'dict<string, string>', 15 + ); 16 + } 17 + 18 + public function defineReturnType() { 19 + return 'dict<string, wild>'; 20 + } 21 + 22 + public function defineErrorTypes() { 23 + return array( 24 + "ERR_BRANCH" => 'Unknown Releeph branch.', 25 + "ERR_FIELD_PARSE" => 'Unable to parse a Releeph field.', 26 + ); 27 + } 28 + 29 + protected function execute(ConduitAPIRequest $request) { 30 + $branch_phid = $request->getValue('branchPHID'); 31 + $releeph_branch = id(new ReleephBranch()) 32 + ->loadOneWhere('phid = %s', $branch_phid); 33 + 34 + if (!$releeph_branch) { 35 + throw id(new ConduitException("ERR_BRANCH"))->setErrorDescription( 36 + "No ReleephBranch found with PHID {$branch_phid}!"); 37 + } 38 + 39 + $releeph_project = $releeph_branch->loadReleephProject(); 40 + 41 + // Find the requested commit identifiers 42 + $requested_commits = array(); 43 + $things = $request->getValue('things'); 44 + $finder = id(new ReleephCommitFinder()) 45 + ->setReleephProject($releeph_project); 46 + foreach ($things as $thing) { 47 + try { 48 + $requested_commits[$thing] = $finder->fromPartial($thing); 49 + } catch (ReleephCommitFinderException $ex) { 50 + throw id(new ConduitException('ERR_NO_MATCHES')) 51 + ->setErrorDescription($ex->getMessage()); 52 + } 53 + } 54 + 55 + // Find any existing requests that clash on the commit id, for this branch 56 + $existing_releeph_requests = id(new ReleephRequest())->loadAllWhere( 57 + 'requestCommitPHID IN (%Ls) AND branchID = %d', 58 + mpull($requested_commits, 'getPHID'), 59 + $releeph_branch->getID()); 60 + $existing_releeph_requests = mpull( 61 + $existing_releeph_requests, 62 + null, 63 + 'getRequestCommitPHID'); 64 + 65 + $selector = $releeph_project->getReleephFieldSelector(); 66 + $fields = $selector->getFieldSpecifications(); 67 + foreach ($fields as $field) { 68 + $field 69 + ->setReleephProject($releeph_project) 70 + ->setReleephBranch($releeph_branch); 71 + } 72 + 73 + $results = array(); 74 + foreach ($requested_commits as $thing => $commit) { 75 + $phid = $commit->getPHID(); 76 + $handles = id(new PhabricatorObjectHandleData(array($phid))) 77 + ->setViewer($request->getUser()) 78 + ->loadHandles(); 79 + $name = id($handles[$phid])->getName(); 80 + 81 + $releeph_request = null; 82 + 83 + $existing_releeph_request = idx($existing_releeph_requests, $phid); 84 + if ($existing_releeph_request) { 85 + $releeph_request = $existing_releeph_request; 86 + } else { 87 + $releeph_request = new ReleephRequest(); 88 + foreach ($fields as $field) { 89 + if (!$field->isEditable()) { 90 + continue; 91 + } 92 + $field->setReleephRequest($releeph_request); 93 + try { 94 + $field->setValueFromConduitAPIRequest($request); 95 + } catch (ReleephFieldParseException $ex) { 96 + throw id(new ConduitException('ERR_FIELD_PARSE')) 97 + ->setErrorDescription($ex->getMessage()); 98 + } 99 + } 100 + id(new ReleephRequestEditor($releeph_request)) 101 + ->setActor($request->getUser()) 102 + ->create($commit, $releeph_branch); 103 + } 104 + 105 + $releeph_branch->populateReleephRequestHandles( 106 + $request->getUser(), 107 + array($releeph_request)); 108 + $rq_handles = $releeph_request->getHandles(); 109 + $requestor_phid = $releeph_request->getRequestUserPHID(); 110 + $requestor = $rq_handles[$requestor_phid]->getName(); 111 + 112 + $url = PhabricatorEnv::getProductionURI('/RQ'.$releeph_request->getID()); 113 + 114 + $results[$thing] = array( 115 + 'thing' => $thing, 116 + 'branch' => $releeph_branch->getDisplayNameWithDetail(), 117 + 'commitName' => $name, 118 + 'commitID' => $commit->getCommitIdentifier(), 119 + 'url' => $url, 120 + 'requestID' => $releeph_request->getID(), 121 + 'requestor' => $requestor, 122 + 'requestTime' => $releeph_request->getDateCreated(), 123 + 'existing' => $existing_releeph_request !== null, 124 + ); 125 + } 126 + 127 + return $results; 128 + } 129 + 130 + }
+39
src/applications/releeph/conduit/work/ConduitAPI_releephwork_canpush_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_canpush_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Return whether the conduit user is allowed to push."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'projectPHID' => 'required string', 17 + ); 18 + } 19 + 20 + public function defineReturnType() { 21 + return 'bool'; 22 + } 23 + 24 + public function defineErrorTypes() { 25 + return array(); 26 + } 27 + 28 + protected function execute(ConduitAPIRequest $request) { 29 + $releeph_project = id(new ReleephProject()) 30 + ->loadOneWhere('phid = %s', $request->getValue('projectPHID')); 31 + 32 + if (!$releeph_project->getPushers()) { 33 + return true; 34 + } else { 35 + $user = $request->getUser(); 36 + return $releeph_project->isPusher($user); 37 + } 38 + } 39 + }
+43
src/applications/releeph/conduit/work/ConduitAPI_releephwork_getauthorinfo_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_getauthorinfo_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Return a string to use as the VCS author."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'userPHID' => 'required string', 17 + 'vcsType' => 'required string', 18 + ); 19 + } 20 + 21 + public function defineReturnType() { 22 + return 'nonempty string'; 23 + } 24 + 25 + public function defineErrorTypes() { 26 + return array(); 27 + } 28 + 29 + protected function execute(ConduitAPIRequest $request) { 30 + $user = id(new PhabricatorUser()) 31 + ->loadOneWhere('phid = %s', $request->getValue('userPHID')); 32 + 33 + $email = $user->loadPrimaryEmailAddress(); 34 + if (is_numeric($email)) { 35 + $email = $user->getUserName().'@fb.com'; 36 + } 37 + 38 + return sprintf( 39 + '%s <%s>', 40 + $user->getRealName(), 41 + $email); 42 + } 43 + }
+52
src/applications/releeph/conduit/work/ConduitAPI_releephwork_getbranch_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_getbranch_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Return information to help checkout / cut a Releeph branch."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'branchPHID' => 'required string', 17 + ); 18 + } 19 + 20 + public function defineReturnType() { 21 + return 'dict<string, wild>'; 22 + } 23 + 24 + public function defineErrorTypes() { 25 + return array(); 26 + } 27 + 28 + protected function execute(ConduitAPIRequest $request) { 29 + $branch = id(new ReleephBranch()) 30 + ->loadOneWhere('phid = %s', $request->getValue('branchPHID')); 31 + 32 + $cut_phid = $branch->getCutPointCommitPHID(); 33 + $phids = array($cut_phid); 34 + $handles = id(new PhabricatorObjectHandleData($phids)) 35 + ->setViewer($request->getUser()) 36 + ->loadHandles(); 37 + 38 + $project = $branch->loadReleephProject(); 39 + $repo = $project->loadPhabricatorRepository(); 40 + 41 + return array( 42 + 'branchName' => $branch->getName(), 43 + 'branchPHID' => $branch->getPHID(), 44 + 'vcsType' => $repo->getVersionControlSystem(), 45 + 'cutCommitID' => $branch->getCutPointCommitIdentifier(), 46 + 'cutCommitName' => $handles[$cut_phid]->getName(), 47 + 'creatorPHID' => $branch->getCreatedByUserPHID(), 48 + 'trunk' => $project->getTrunkBranch(), 49 + ); 50 + } 51 + 52 + }
+94
src/applications/releeph/conduit/work/ConduitAPI_releephwork_getbranchcommitmessage_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_getbranchcommitmessage_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Get a commit message for committing a Releeph branch."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'branchPHID' => 'required string', 17 + ); 18 + } 19 + 20 + public function defineReturnType() { 21 + return 'nonempty string'; 22 + } 23 + 24 + public function defineErrorTypes() { 25 + return array(); 26 + } 27 + 28 + protected function execute(ConduitAPIRequest $request) { 29 + $branch = id(new ReleephBranch()) 30 + ->loadOneWhere('phid = %s', $request->getValue('branchPHID')); 31 + 32 + $project = $branch->loadReleephProject(); 33 + 34 + $creator_phid = $branch->getCreatedByUserPHID(); 35 + $cut_phid = $branch->getCutPointCommitPHID(); 36 + 37 + $phids = array( 38 + $branch->getPHID(), 39 + $project->getPHID(), 40 + $creator_phid, 41 + $cut_phid, 42 + ); 43 + 44 + $handles = id(new PhabricatorObjectHandleData($phids)) 45 + ->setViewer($request->getUser()) 46 + ->loadHandles(); 47 + 48 + $h_branch = $handles[$branch->getPHID()]; 49 + $h_project = $handles[$project->getPHID()]; 50 + 51 + // Not as customizable as a ReleephRequest's commit message. It doesn't 52 + // really need to be. 53 + $commit_message = array(); 54 + $commit_message[] = $h_branch->getFullName(); 55 + $commit_message[] = $h_branch->getURI(); 56 + 57 + $commit_message[] = "Cut Point: ".$handles[$cut_phid]->getName(); 58 + 59 + $cut_point_pr_commit = id(new PhabricatorRepositoryCommit()) 60 + ->loadOneWhere('phid = %s', $cut_phid); 61 + $cut_point_commit_date = strftime( 62 + '%Y-%m-%d %H:%M:%S%z', 63 + $cut_point_pr_commit->getEpoch()); 64 + $commit_message[] = "Cut Point Date: {$cut_point_commit_date}"; 65 + 66 + $commit_message[] = "Created By: ".$handles[$creator_phid]->getName(); 67 + 68 + $project_uri = $project->getURI(); 69 + $commit_message[] = "Project: ".$h_project->getName()." ".$project_uri; 70 + 71 + /** 72 + * Required for 090-limit_new_branch_creations.sh in 73 + * admin/scripts/git/hosting/hooks/update.d (in the E repo): 74 + * 75 + * http://fburl.com/2372545 76 + * 77 + * The commit message must have a line saying: 78 + * 79 + * @new-branch: <branch-name> 80 + * 81 + */ 82 + $repo = $project->loadPhabricatorRepository(); 83 + switch ($repo->getVersionControlSystem()) { 84 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 85 + $commit_message[] = sprintf( 86 + '@new-branch: %s', 87 + $branch->getName()); 88 + break; 89 + } 90 + 91 + return implode("\n\n", $commit_message); 92 + } 93 + 94 + }
+90
src/applications/releeph/conduit/work/ConduitAPI_releephwork_getcommitmessage_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_getcommitmessage_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return 12 + "Get commit message components for building ". 13 + "a ReleephRequest commit message."; 14 + } 15 + 16 + public function defineParamTypes() { 17 + return array( 18 + 'requestPHID' => 'required string', 19 + 'action' => 'required enum<"pick", "revert">', 20 + ); 21 + } 22 + 23 + public function defineReturnType() { 24 + return 'dict<string, string>'; 25 + } 26 + 27 + public function defineErrorTypes() { 28 + return array(); 29 + } 30 + 31 + protected function execute(ConduitAPIRequest $request) { 32 + $releeph_request = id(new ReleephRequest()) 33 + ->loadOneWhere('phid = %s', $request->getValue('requestPHID')); 34 + 35 + $action = $request->getValue('action'); 36 + 37 + $title = $releeph_request->getSummaryForDisplay(); 38 + 39 + $commit_message = array(); 40 + 41 + $project = $releeph_request->loadReleephProject(); 42 + $branch = $releeph_request->loadReleephBranch(); 43 + 44 + $selector = $project->getReleephFieldSelector(); 45 + $fields = $selector->getFieldSpecifications(); 46 + $fields = $selector->sortFieldsForCommitMessage($fields); 47 + 48 + foreach ($fields as $field) { 49 + $field 50 + ->setUser($request->getUser()) 51 + ->setReleephProject($project) 52 + ->setReleephBranch($branch) 53 + ->setReleephRequest($releeph_request); 54 + 55 + $label = null; 56 + $value = null; 57 + 58 + switch ($action) { 59 + case 'pick': 60 + if ($field->shouldAppearOnCommitMessage()) { 61 + $label = $field->renderLabelForCommitMessage(); 62 + $value = $field->renderValueForCommitMessage(); 63 + } 64 + break; 65 + 66 + case 'revert': 67 + if ($field->shouldAppearOnRevertMessage()) { 68 + $label = $field->renderLabelForRevertMessage(); 69 + $value = $field->renderValueForRevertMessage(); 70 + } 71 + break; 72 + } 73 + 74 + if ($label && $value) { 75 + if (strpos($value, "\n") !== false || 76 + substr($value, 0, 2) === ' ') { 77 + $commit_message[] = "{$label}:\n{$value}"; 78 + } else { 79 + $commit_message[] = "{$label}: {$value}"; 80 + } 81 + } 82 + } 83 + 84 + return array( 85 + 'title' => $title, 86 + 'body' => implode("\n\n", $commit_message), 87 + ); 88 + } 89 + 90 + }
+35
src/applications/releeph/conduit/work/ConduitAPI_releephwork_getorigcommitmessage_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_getorigcommitmessage_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Return the original commit message for the given commit."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'commitPHID' => 'required string', 17 + ); 18 + } 19 + 20 + public function defineReturnType() { 21 + return 'nonempty string'; 22 + } 23 + 24 + public function defineErrorTypes() { 25 + return array(); 26 + } 27 + 28 + protected function execute(ConduitAPIRequest $request) { 29 + $commit = id(new PhabricatorRepositoryCommit()) 30 + ->loadOneWhere('phid = %s', $request->getValue('commitPHID')); 31 + $commit_data = $commit->loadCommitData(); 32 + $commit_message = $commit_data->getCommitMessage(); 33 + return trim($commit_message); 34 + } 35 + }
+208
src/applications/releeph/conduit/work/ConduitAPI_releephwork_nextrequest_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_nextrequest_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + private $project; 7 + private $branch; 8 + 9 + public function getMethodStatus() { 10 + return self::METHOD_STATUS_UNSTABLE; 11 + } 12 + 13 + public function getMethodDescription() { 14 + return 15 + "Return info required to cut a branch, ". 16 + "and pick and revert ReleephRequests"; 17 + } 18 + 19 + public function defineParamTypes() { 20 + return array( 21 + 'branchPHID' => 'required int', 22 + 'seen' => 'required list<string, bool>', 23 + ); 24 + } 25 + 26 + public function defineReturnType() { 27 + return ''; 28 + } 29 + 30 + public function defineErrorTypes() { 31 + return array( 32 + 'ERR-NOT-PUSHER' => 33 + 'You are not listed as a pusher for thie Releeph project!', 34 + ); 35 + } 36 + 37 + protected function execute(ConduitAPIRequest $request) { 38 + $seen = $request->getValue('seen'); 39 + 40 + $branch = id(new ReleephBranch()) 41 + ->loadOneWhere('phid = %s', $request->getValue('branchPHID')); 42 + 43 + $project = $branch->loadReleephProject(); 44 + 45 + $needs_pick = array(); 46 + $needs_revert = array(); 47 + 48 + $releeph_requests = $branch->loadReleephRequests($request->getUser()); 49 + 50 + foreach ($releeph_requests as $candidate) { 51 + $phid = $candidate->getPHID(); 52 + if (idx($seen, $phid)) { 53 + continue; 54 + } 55 + 56 + $should = $candidate->shouldBeInBranch(); 57 + $in = $candidate->getInBranch(); 58 + if ($should && !$in) { 59 + $needs_pick[] = $candidate; 60 + } 61 + if (!$should && $in) { 62 + $needs_revert[] = $candidate; 63 + } 64 + } 65 + 66 + /** 67 + * Sort both needs_pick and needs_revert in ascending commit order, as 68 + * discovered by Phabricator (using the `id` column to perform that 69 + * ordering). 70 + * 71 + * This is easy for $needs_pick as the ordinal is stored. It is hard for 72 + * reverts, as we have to look that information up. 73 + */ 74 + $needs_pick = msort($needs_pick, 'getRequestCommitOrdinal'); 75 + $needs_revert = $this->sortReverts($needs_revert); 76 + 77 + /** 78 + * Do reverts first in reverse order, then the picks in original-commit 79 + * order. 80 + * 81 + * This seems like the correct thing to do, but there may be a better 82 + * algorithm for the releephwork.nextrequest Conduit call that orders 83 + * things better. 84 + * 85 + * We could also button-mash our way through everything that failed (at the 86 + * end of the run) to try failed things again. 87 + */ 88 + $releeph_request = null; 89 + $action = null; 90 + if ($needs_revert) { 91 + $releeph_request = last($needs_revert); 92 + $action = 'revert'; 93 + $commit_id = $releeph_request->getCommitIdentifier(); 94 + $commit_phid = $releeph_request->getCommitPHID(); 95 + } elseif ($needs_pick) { 96 + $releeph_request = head($needs_pick); 97 + $action = 'pick'; 98 + $commit_id = $releeph_request->getRequestCommitIdentifier(); 99 + $commit_phid = $releeph_request->getRequestCommitPHID(); 100 + } else { 101 + // Return early if there's nothing to do! 102 + return array(); 103 + } 104 + 105 + // Build the response 106 + $phids = array(); 107 + $phids[] = $commit_phid; 108 + 109 + $diff_phid = null; 110 + $diff_rev_id = null; 111 + $diff_rev = $releeph_request->loadDifferentialRevision(); 112 + if ($diff_rev) { 113 + $diff_phid = $diff_rev->getPHID(); 114 + $phids[] = $diff_phid; 115 + $diff_rev_id = $diff_rev->getID(); 116 + } 117 + 118 + $phids[] = $releeph_request->getPHID(); 119 + $handles = id(new PhabricatorObjectHandleData($phids)) 120 + ->setViewer($request->getUser()) 121 + ->loadHandles(); 122 + 123 + $diff_name = null; 124 + if ($diff_rev) { 125 + $diff_name = $handles[$diff_phid]->getName(); 126 + } 127 + 128 + // Calculate the new-author information (if any) 129 + $new_author = null; 130 + $new_author_phid = null; 131 + switch ($project->getDetail('commitWithAuthor')) { 132 + case ReleephProject::COMMIT_AUTHOR_NONE: 133 + break; 134 + 135 + case ReleephProject::COMMIT_AUTHOR_FROM_DIFF: 136 + if ($diff_rev) { 137 + $new_author_phid = $diff_rev->getAuthorPHID(); 138 + } else { 139 + $pr_commit = $releeph_request->loadPhabricatorRepositoryCommit(); 140 + if ($pr_commit) { 141 + $new_author_phid = $pr_commit->getAuthorPHID(); 142 + } 143 + } 144 + break; 145 + 146 + case ReleephProject::COMMIT_AUTHOR_REQUESTOR: 147 + $new_author_phid = $releeph_request->getRequestUserPHID(); 148 + break; 149 + } 150 + 151 + return array( 152 + 'requestID' => $releeph_request->getID(), 153 + 'requestPHID' => $releeph_request->getPHID(), 154 + 'requestName' => $handles[$releeph_request->getPHID()]->getName(), 155 + 'requestorPHID' => $releeph_request->getRequestUserPHID(), 156 + 'action' => $action, 157 + 'diffRevID' => $diff_rev_id, 158 + 'diffName' => $diff_name, 159 + 'commitIdentifier' => $commit_id, 160 + 'commitPHID' => $commit_phid, 161 + 'commitName' => $handles[$commit_phid]->getName(), 162 + 'needsRevert' => mpull($needs_revert, 'getID'), 163 + 'needsPick' => mpull($needs_pick, 'getID'), 164 + 'newAuthorPHID' => $new_author_phid, 165 + ); 166 + } 167 + 168 + /** 169 + * Sort an array of ReleephRequests, that have been picked into a branch, in 170 + * the order in which they were picked to the branch. 171 + */ 172 + private function sortReverts(array $releeph_requests) { 173 + if (!$releeph_requests) { 174 + return array(); 175 + } 176 + 177 + // ReleephRequests, keyed by <branch-commit-id> 178 + $releeph_requests = mpull($releeph_requests, null, 'getCommitIdentifier'); 179 + 180 + $commits = id(new PhabricatorRepositoryCommit()) 181 + ->loadAllWhere( 182 + 'commitIdentifier IN (%Ls)', 183 + mpull($releeph_requests, 'getCommitIdentifier')); 184 + 185 + // A map of <branch-commit-id> => <branch-commit-ordinal> 186 + $surrogate = mpull($commits, 'getID', 'getCommitIdentifier'); 187 + 188 + $unparsed = array(); 189 + $result = array(); 190 + 191 + foreach ($releeph_requests as $commit_id => $releeph_request) { 192 + $ordinal = idx($surrogate, $commit_id); 193 + if ($ordinal) { 194 + $result[$ordinal] = $releeph_request; 195 + } else { 196 + $unparsed[] = $releeph_request; 197 + } 198 + } 199 + 200 + // Sort $result in ascending order 201 + ksort($result); 202 + 203 + // Unparsed commits we'll just have to guess, based on time 204 + $unparsed = msort($unparsed, 'getDateModified'); 205 + 206 + return array_merge($result, $unparsed); 207 + } 208 + }
+42
src/applications/releeph/conduit/work/ConduitAPI_releephwork_record_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_record_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Wrapper to ReleephRequestEditor->recordSuccessfulCommit()."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'requestPHID' => 'required string', 17 + 'action' => 'required enum<"pick", "revert">', 18 + 'commitIdentifier' => 'required string', 19 + ); 20 + } 21 + 22 + public function defineReturnType() { 23 + return 'void'; 24 + } 25 + 26 + public function defineErrorTypes() { 27 + return array(); 28 + } 29 + 30 + protected function execute(ConduitAPIRequest $request) { 31 + $action = $request->getValue('action'); 32 + $new_commit_id = $request->getValue('commitIdentifier'); 33 + 34 + $releeph_request = id(new ReleephRequest()) 35 + ->loadOneWhere('phid = %s', $request->getValue('requestPHID')); 36 + 37 + id(new ReleephRequestEditor($releeph_request)) 38 + ->setActor($request->getUser()) 39 + ->recordSuccessfulCommit($action, $new_commit_id); 40 + } 41 + 42 + }
+63
src/applications/releeph/conduit/work/ConduitAPI_releephwork_recordpickstatus_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_releephwork_recordpickstatus_Method 4 + extends ConduitAPI_releeph_Method { 5 + 6 + public function getMethodStatus() { 7 + return self::METHOD_STATUS_UNSTABLE; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return "Wrapper to ReleephRequestEditor->changePickStatus()."; 12 + } 13 + 14 + public function defineParamTypes() { 15 + return array( 16 + 'requestPHID' => 'required string', 17 + 'action' => 'required enum<"pick", "revert">', 18 + 'ok' => 'required bool', 19 + 'dryRun' => 'optional bool', 20 + 'details' => 'optional dict<string, wild>', 21 + ); 22 + } 23 + 24 + public function defineReturnType() { 25 + return ''; 26 + } 27 + 28 + public function defineErrorTypes() { 29 + return array(); 30 + } 31 + 32 + protected function execute(ConduitAPIRequest $request) { 33 + $action = $request->getValue('action'); 34 + $ok = $request->getValue('ok'); 35 + $dry_run = $request->getValue('dryRun'); 36 + $details = $request->getValue('details', array()); 37 + 38 + switch ($request->getValue('action')) { 39 + case 'pick': 40 + $pick_status = $ok 41 + ? ReleephRequest::PICK_OK 42 + : ReleephRequest::PICK_FAILED; 43 + break; 44 + 45 + case 'revert': 46 + $pick_status = $ok 47 + ? ReleephRequest::REVERT_OK 48 + : ReleephRequest::REVERT_FAILED; 49 + break; 50 + 51 + default: 52 + throw new Exception("Unknown action {$action}!"); 53 + } 54 + 55 + $releeph_request = id(new ReleephRequest()) 56 + ->loadOneWhere('phid = %s', $request->getValue('requestPHID')); 57 + 58 + id(new ReleephRequestEditor($releeph_request)) 59 + ->setActor($request->getUser()) 60 + ->changePickStatus($pick_status, $dry_run, $details); 61 + } 62 + 63 + }
+64
src/applications/releeph/config/PhabricatorApplicationReleephConfigOptions.php
··· 1 + <?php 2 + 3 + final class PhabricatorApplicationReleephConfigOptions 4 + extends PhabricatorApplicationConfigOptions { 5 + 6 + public function getName() { 7 + return pht("Releeph"); 8 + } 9 + 10 + public function getDescription() { 11 + return pht("Options for configuring Releeph, the release branch tool."); 12 + } 13 + 14 + public function getOptions() { 15 + return array( 16 + $this->newOption('releeph.installed', 'bool', false) 17 + ->setSummary(pht('Enable the Releeph application.')) 18 + ->setDescription( 19 + pht( 20 + "Releeph, a tool for managing release branches, will eventually ". 21 + "fit in to the Phabricator suite as a general purpose tool. ". 22 + "However Releeph is currently unstable in multiple ways that may ". 23 + "not migrate properly for you: the code is still in alpha stage ". 24 + "of design, the storage format is likely to change in unexpected ". 25 + "ways, and the workflows presented are very specific to a core ". 26 + "set of alpha testers at Facebook. For the time being you are ". 27 + "strongly discouraged from relying on Releeph being at all ". 28 + "stable.")), 29 + $this->newOption( 30 + 'releeph.field-selector', 31 + 'class', 32 + 'ReleephDefaultFieldSelector') 33 + ->setBaseClass('ReleephFieldSelector') 34 + ->setSummary(pht('Field selector class')) 35 + ->setDescription( 36 + pht( 37 + "Control which fields are available when making a new Releeph ". 38 + "request, and which are then shown in the Releeph UI.")), 39 + $this->newOption( 40 + 'releeph.user-view', 41 + 'class', 42 + 'ReleephDefaultUserView') 43 + ->setBaseClass('ReleephUserView') 44 + ->setSummary(pht('Extra markup when rendering usernames')) 45 + ->setDescription( 46 + pht( 47 + "A wrapper to render Phabricator users in Releeph, with custom ". 48 + "markup. For example, Facebook extends this to render additional ". 49 + "information about requestors, to each Releeph project's ". 50 + "pushers.")), 51 + $this->newOption( 52 + 'releeph.default-branch-template', 53 + 'string', 54 + 'releases/%P/%p-%Y%m%d-%v') 55 + ->setDescription( 56 + pht( 57 + "The default branch template for new branches in unconfigured ". 58 + "Releeph projects. This is also configurable on a per-project ". 59 + "basis.")), 60 + ); 61 + } 62 + 63 + 64 + }
+122
src/applications/releeph/controller/ReleephController.php
··· 1 + <?php 2 + 3 + abstract class ReleephController extends PhabricatorController { 4 + 5 + private $releephProject; 6 + private $releephBranch; 7 + private $releephRequest; 8 + 9 + /** 10 + * ReleephController will take care of loading any Releeph* objects 11 + * referenced in the URL. 12 + */ 13 + public function willProcessRequest(array $data) { 14 + // Project 15 + $project = null; 16 + $project_id = idx($data, 'projectID'); 17 + $project_name = idx($data, 'projectName'); 18 + if ($project_id) { 19 + $project = id(new ReleephProject())->load($project_id); 20 + if (!$project) { 21 + throw new Exception( 22 + "ReleephProject with id '{$project_id}' not found!"); 23 + } 24 + } elseif ($project_name) { 25 + $project = id(new ReleephProject()) 26 + ->loadOneWhere('name = %s', $project_name); 27 + if (!$project) { 28 + throw new Exception( 29 + "ReleephProject with name '{$project_name}' not found!"); 30 + } 31 + } 32 + 33 + // Branch 34 + $branch = null; 35 + $branch_id = idx($data, 'branchID'); 36 + $branch_name = idx($data, 'branchName'); 37 + if ($branch_id) { 38 + $branch = id(new ReleephBranch())->load($branch_id); 39 + if (!$branch) { 40 + throw new Exception("Branch with id '{$branch_id}' not found!"); 41 + } 42 + } elseif ($branch_name) { 43 + if (!$project) { 44 + throw new Exception( 45 + "You cannot refer to a branch by name without also referring ". 46 + "to a ReleephProject (branch names are only unique in projects)."); 47 + } 48 + $branch = id(new ReleephBranch())->loadOneWhere( 49 + 'basename = %s AND releephProjectID = %d', 50 + $branch_name, 51 + $project->getID()); 52 + if (!$branch) { 53 + throw new Exception( 54 + "ReleephBranch with basename '{$branch_name}' not found ". 55 + "in project '{$project->getName()}'!"); 56 + } 57 + } 58 + 59 + // Request 60 + $request = null; 61 + $request_id = idx($data, 'requestID'); 62 + if ($request_id) { 63 + $request = id(new ReleephRequest())->load($request_id); 64 + if (!$request) { 65 + throw new Exception( 66 + "ReleephRequest with id '{$request_id}' not found!"); 67 + } 68 + } 69 + 70 + // Fill in the gaps 71 + if ($request && !$branch) { 72 + $branch = $request->loadReleephBranch(); 73 + } 74 + 75 + if ($branch && !$project) { 76 + $project = $branch->loadReleephProject(); 77 + } 78 + 79 + // Set! 80 + $this->releephProject = $project; 81 + $this->releephBranch = $branch; 82 + $this->releephRequest = $request; 83 + } 84 + 85 + protected function getReleephProject() { 86 + if (!$this->releephProject) { 87 + throw new Exception( 88 + 'This controller did not load a ReleephProject from the URL $data.'); 89 + } 90 + return $this->releephProject; 91 + } 92 + 93 + protected function getReleephBranch() { 94 + if (!$this->releephBranch) { 95 + throw new Exception( 96 + 'This controller did not load a ReleephBranch from the URL $data.'); 97 + } 98 + return $this->releephBranch; 99 + } 100 + 101 + protected function getReleephRequest() { 102 + if (!$this->releephRequest) { 103 + throw new Exception( 104 + 'This controller did not load a ReleephRequest from the URL $data.'); 105 + } 106 + return $this->releephRequest; 107 + } 108 + 109 + public function buildStandardPageResponse($view, array $data) { 110 + $page = $this->buildStandardPageView(); 111 + 112 + $page->setApplicationName('Releeph'); 113 + $page->setBaseURI('/releeph/'); 114 + $page->setTitle(idx($data, 'title')); 115 + $page->setGlyph("\xD3\x82"); 116 + $page->appendChild($view); 117 + 118 + $response = new AphrontWebpageResponse(); 119 + return $response->setContent($page->render()); 120 + } 121 + 122 + }
+61
src/applications/releeph/controller/branch/ReleephBranchAccessController.php
··· 1 + <?php 2 + 3 + final class ReleephBranchAccessController extends ReleephController { 4 + 5 + private $action; 6 + 7 + public function willProcessRequest(array $data) { 8 + $this->action = $data['action']; 9 + parent::willProcessRequest($data); 10 + } 11 + 12 + public function processRequest() { 13 + $rph_branch = $this->getReleephBranch(); 14 + $request = $this->getRequest(); 15 + 16 + $active_uri = '/releeph/project/'.$rph_branch->getReleephProjectID().'/'; 17 + $inactive_uri = $active_uri.'inactive/'; 18 + 19 + switch ($this->action) { 20 + case 'close': 21 + $is_active = false; 22 + $origin_uri = $active_uri; 23 + break; 24 + 25 + case 're-open': 26 + $is_active = true; 27 + $origin_uri = $inactive_uri; 28 + break; 29 + 30 + default: 31 + throw new Exception("Unknown action '{$this->action}'!"); 32 + break; 33 + } 34 + 35 + if ($request->isDialogFormPost()) { 36 + id(new ReleephBranchEditor()) 37 + ->setActor($request->getUser()) 38 + ->setReleephBranch($rph_branch) 39 + ->changeBranchAccess($is_active ? 1 : 0); 40 + return id(new AphrontRedirectResponse()) 41 + ->setURI($origin_uri); 42 + } 43 + 44 + $button_text = ucfirst($this->action).' Branch'; 45 + $message = hsprintf( 46 + '<p>Really %s the branch <i>%s</i>?</p>', 47 + $this->action, 48 + $rph_branch->getBasename()); 49 + 50 + 51 + $dialog = new AphrontDialogView(); 52 + $dialog 53 + ->setUser($request->getUser()) 54 + ->setTitle('Confirm') 55 + ->appendChild($message) 56 + ->addSubmitButton($button_text) 57 + ->addCancelButton($origin_uri); 58 + 59 + return id(new AphrontDialogResponse())->setDialog($dialog); 60 + } 61 + }
+105
src/applications/releeph/controller/branch/ReleephBranchCreateController.php
··· 1 + <?php 2 + 3 + final class ReleephBranchCreateController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $releeph_project = $this->getReleephProject(); 7 + 8 + $request = $this->getRequest(); 9 + 10 + $cut_point = $request->getStr('cutPoint'); 11 + $symbolic_name = $request->getStr('symbolicName'); 12 + 13 + if (!$cut_point) { 14 + $repository = $releeph_project->loadPhabricatorRepository(); 15 + switch ($repository->getVersionControlSystem()) { 16 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 17 + break; 18 + 19 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 20 + $cut_point = $releeph_project->getTrunkBranch(); 21 + break; 22 + } 23 + } 24 + 25 + $e_cut = true; 26 + $errors = array(); 27 + 28 + $branch_date_control = id(new AphrontFormDateControl()) 29 + ->setUser($request->getUser()) 30 + ->setName('templateDate') 31 + ->setLabel('Date') 32 + ->setCaption('The date used for filling out the branch template.') 33 + ->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY); 34 + $branch_date = $branch_date_control->readValueFromRequest($request); 35 + 36 + if ($request->isFormPost()) { 37 + $cut_commit = null; 38 + if (!$cut_point) { 39 + $e_cut = 'Required'; 40 + $errors[] = 'You must give a branch cut point'; 41 + } else { 42 + try { 43 + $finder = id(new ReleephCommitFinder()) 44 + ->setReleephProject($releeph_project); 45 + $cut_commit = $finder->fromPartial($cut_point); 46 + } catch (Exception $e) { 47 + $e_cut = 'Invalid'; 48 + $errors[] = $e->getMessage(); 49 + } 50 + } 51 + 52 + if (!$errors) { 53 + $branch = id(new ReleephBranchEditor()) 54 + ->setReleephProject($releeph_project) 55 + ->setActor($request->getUser()) 56 + ->newBranchFromCommit( 57 + $cut_commit, 58 + $branch_date, 59 + $symbolic_name); 60 + 61 + return id(new AphrontRedirectResponse()) 62 + ->setURI($branch->getURI()); 63 + } 64 + } 65 + 66 + $error_view = array(); 67 + if ($errors) { 68 + $error_view = new AphrontErrorView(); 69 + $error_view->setErrors($errors); 70 + $error_view->setTitle('Form Errors'); 71 + } 72 + 73 + $form = id(new AphrontFormView()) 74 + ->setUser($request->getUser()) 75 + ->appendChild( 76 + id(new AphrontFormTextControl()) 77 + ->setLabel('Symbolic name') 78 + ->setName('symbolicName') 79 + ->setValue($symbolic_name) 80 + ->setCaption('Mutable alternate name, for easy reference, '. 81 + '(e.g. "LATEST")')) 82 + ->appendChild( 83 + id(new AphrontFormTextControl()) 84 + ->setLabel('Cut point') 85 + ->setName('cutPoint') 86 + ->setValue($cut_point) 87 + ->setError($e_cut) 88 + ->setCaption( 89 + 'A commit ID for your repo type, or a Diffusion ID like "rE123"')) 90 + ->appendChild($branch_date_control) 91 + ->appendChild( 92 + id(new AphrontFormSubmitControl()) 93 + ->setValue('Cut Branch') 94 + ->addCancelButton($releeph_project->getURI())); 95 + 96 + $panel = id(new AphrontPanelView()) 97 + ->appendChild($form) 98 + ->setHeader('Cut Branch') 99 + ->setWidth(AphrontPanelView::WIDTH_FORM); 100 + 101 + return $this->buildStandardPageResponse( 102 + array($error_view, $panel), 103 + array('title' => 'Cut new branch')); 104 + } 105 + }
+136
src/applications/releeph/controller/branch/ReleephBranchEditController.php
··· 1 + <?php 2 + 3 + final class ReleephBranchEditController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + $releeph_branch = $this->getReleephBranch(); 8 + $branch_name = $request->getStr( 9 + 'branchName', 10 + $releeph_branch->getName()); 11 + $symbolic_name = $request->getStr( 12 + 'symbolicName', 13 + $releeph_branch->getSymbolicName()); 14 + 15 + $e_existing_with_same_branch_name = false; 16 + $errors = array(); 17 + 18 + if ($request->isFormPost()) { 19 + $existing_with_same_branch_name = 20 + id(new ReleephBranch()) 21 + ->loadOneWhere( 22 + 'id != %d AND releephProjectID = %d AND name = %s', 23 + $releeph_branch->getID(), 24 + $releeph_branch->getReleephProjectID(), 25 + $branch_name); 26 + 27 + if ($existing_with_same_branch_name) { 28 + $errors[] = sprintf( 29 + "The branch name %s is currently taken. Please use another name. ", 30 + $branch_name); 31 + $e_existing_with_same_branch_name = 'Error'; 32 + } 33 + 34 + if (!$errors) { 35 + $existing_with_same_symbolic_name = 36 + id(new ReleephBranch()) 37 + ->loadOneWhere( 38 + 'id != %d AND releephProjectID = %d AND symbolicName = %s', 39 + $releeph_branch->getID(), 40 + $releeph_branch->getReleephProjectID(), 41 + $symbolic_name); 42 + 43 + $releeph_branch->openTransaction(); 44 + $releeph_branch 45 + ->setName($branch_name) 46 + ->setBasename(last(explode('/', $branch_name))) 47 + ->setSymbolicName($symbolic_name); 48 + 49 + if ($existing_with_same_symbolic_name) { 50 + $existing_with_same_symbolic_name 51 + ->setSymbolicName(null) 52 + ->save(); 53 + } 54 + 55 + $releeph_branch->save(); 56 + $releeph_branch->saveTransaction(); 57 + 58 + return id(new AphrontRedirectResponse()) 59 + ->setURI('/releeph/project/'.$releeph_branch->getReleephProjectID()); 60 + } 61 + } 62 + 63 + $phids = array(); 64 + 65 + $phids[] = $creator_phid = $releeph_branch->getCreatedByUserPHID(); 66 + $phids[] = $cut_commit_phid = $releeph_branch->getCutPointCommitPHID(); 67 + 68 + $handles = id(new PhabricatorObjectHandleData($phids)) 69 + ->setViewer($request->getUser()) 70 + ->loadHandles(); 71 + 72 + $form = id(new AphrontFormView()) 73 + ->setUser($request->getUser()) 74 + ->appendChild( 75 + id(new AphrontFormStaticControl()) 76 + ->setLabel('Branch name') 77 + ->setValue($branch_name)) 78 + ->appendChild( 79 + id(new AphrontFormMarkupControl()) 80 + ->setLabel('Cut point') 81 + ->setValue($handles[$cut_commit_phid]->renderLink())) 82 + ->appendChild( 83 + id(new AphrontFormMarkupControl()) 84 + ->setLabel('Created by') 85 + ->setValue($handles[$creator_phid]->renderLink())) 86 + ->appendChild( 87 + id(new AphrontFormTextControl) 88 + ->setLabel('Symbolic Name') 89 + ->setName('symbolicName') 90 + ->setValue($symbolic_name) 91 + ->setCaption('Mutable alternate name, for easy reference, '. 92 + '(e.g. "LATEST")')) 93 + ->appendChild(hsprintf( 94 + '<br>' . 95 + 'In dire situations where the branch name is wrong, ' . 96 + 'you can edit it in the database by changing the field below. ' . 97 + 'If you do this, it is very important that you change your ' . 98 + 'branch\'s name in the VCS to reflect the new name in Releeph, ' . 99 + 'otherwise a catastrophe of previously unheard-of magnitude ' . 100 + 'will befall your project.')) 101 + ->appendChild( 102 + id(new AphrontFormTextControl) 103 + ->setLabel('New branch name') 104 + ->setName('branchName') 105 + ->setValue($branch_name) 106 + ->setError($e_existing_with_same_branch_name)) 107 + ->appendChild( 108 + id(new AphrontFormSubmitControl()) 109 + ->addCancelButton($releeph_branch->getURI()) 110 + ->setValue('Save')); 111 + 112 + $error_view = null; 113 + if ($errors) { 114 + $error_view = id(new AphrontErrorView()) 115 + ->setSeverity(AphrontErrorView::SEVERITY_ERROR) 116 + ->setErrors($errors) 117 + ->setTitle('Errors'); 118 + } 119 + 120 + $title = hsprintf( 121 + 'Edit branch %s', 122 + $releeph_branch->getDisplayNameWithDetail()); 123 + 124 + $panel = id(new AphrontPanelView()) 125 + ->setHeader($title) 126 + ->appendChild($form) 127 + ->setWidth(AphrontPanelView::WIDTH_FORM); 128 + 129 + return $this->buildStandardPageResponse( 130 + array( 131 + $error_view, 132 + $panel, 133 + ), 134 + array('title' => $title)); 135 + } 136 + }
+46
src/applications/releeph/controller/branch/ReleephBranchNamePreviewController.php
··· 1 + <?php 2 + 3 + final class ReleephBranchNamePreviewController 4 + extends PhabricatorController { 5 + 6 + public function processRequest() { 7 + $request = $this->getRequest(); 8 + 9 + $is_symbolic = $request->getBool('isSymbolic'); 10 + $template = $request->getStr('template'); 11 + 12 + if (!$is_symbolic && !$template) { 13 + $template = ReleephBranchTemplate::getDefaultTemplate(); 14 + } 15 + 16 + $arc_project_id = $request->getInt('arcProjectID'); 17 + $fake_commit_handle = 18 + ReleephBranchTemplate::getFakeCommitHandleFor($arc_project_id); 19 + 20 + list($name, $errors) = id(new ReleephBranchTemplate()) 21 + ->setCommitHandle($fake_commit_handle) 22 + ->setReleephProjectName($request->getStr('projectName')) 23 + ->setSymbolic($is_symbolic) 24 + ->interpolate($template); 25 + 26 + $markup = ''; 27 + 28 + if ($name) { 29 + $markup = phutil_tag( 30 + 'div', 31 + array('class' => 'name'), 32 + $name); 33 + } 34 + 35 + if ($errors) { 36 + $markup .= phutil_tag( 37 + 'div', 38 + array('class' => 'error'), 39 + head($errors)); 40 + } 41 + 42 + return id(new AphrontAjaxResponse()) 43 + ->setContent(array('markup' => $markup)); 44 + } 45 + 46 + }
+94
src/applications/releeph/controller/branch/ReleephBranchViewController.php
··· 1 + <?php 2 + 3 + final class ReleephBranchViewController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + 8 + $releeph_branch = $this->getReleephBranch(); 9 + $releeph_project = $this->getReleephProject(); 10 + $all_releeph_requests = $releeph_branch->loadReleephRequests( 11 + $request->getUser()); 12 + 13 + $selector = $releeph_project->getReleephFieldSelector(); 14 + $fields = $selector->arrangeFieldsForSelectForm( 15 + $selector->getFieldSpecifications()); 16 + 17 + $form = id(new AphrontFormView()) 18 + ->setMethod('GET') 19 + ->setUser($request->getUser()); 20 + 21 + $filtered_releeph_requests = $all_releeph_requests; 22 + foreach ($fields as $field) { 23 + $all_releeph_requests_without_this_field = $all_releeph_requests; 24 + foreach ($fields as $other_field) { 25 + if ($other_field != $field) { 26 + $other_field->selectReleephRequestsHook( 27 + $request, 28 + $all_releeph_requests_without_this_field); 29 + 30 + } 31 + } 32 + 33 + $field->appendSelectControlsHook( 34 + $form, 35 + $request, 36 + $all_releeph_requests, 37 + $all_releeph_requests_without_this_field); 38 + 39 + $field->selectReleephRequestsHook( 40 + $request, 41 + $filtered_releeph_requests); 42 + } 43 + 44 + $form->appendChild( 45 + id(new AphrontFormSubmitControl()) 46 + ->setValue('Filter')); 47 + 48 + $list = id(new ReleephRequestHeaderListView()) 49 + ->setOriginType('branch') 50 + ->setUser($request->getUser()) 51 + ->setAphrontRequest($this->getRequest()) 52 + ->setReleephProject($releeph_project) 53 + ->setReleephBranch($releeph_branch) 54 + ->setReleephRequests($filtered_releeph_requests); 55 + 56 + $filter = id(new AphrontListFilterView()) 57 + ->appendChild($form); 58 + 59 + $crumbs = $this->buildApplicationCrumbs() 60 + ->addCrumb( 61 + id(new PhabricatorCrumbView()) 62 + ->setName($releeph_project->getName()) 63 + ->setHref($releeph_project->getURI())) 64 + ->addCrumb( 65 + id(new PhabricatorCrumbView()) 66 + ->setName($releeph_branch->getDisplayNameWithDetail()) 67 + ->setHref($releeph_branch->getURI())); 68 + 69 + // Don't show the request button for inactive (closed) branches 70 + if ($releeph_branch->isActive()) { 71 + $create_uri = $releeph_branch->getURI('request/'); 72 + $crumbs->addAction( 73 + id(new PhabricatorMenuItemView()) 74 + ->setHref($create_uri) 75 + ->setName('Request Pick') 76 + ->setIcon('create')); 77 + } 78 + 79 + return $this->buildStandardPageResponse( 80 + array( 81 + $crumbs, 82 + $filter, 83 + $list 84 + ), 85 + array( 86 + 'title' => 87 + $releeph_project->getName(). 88 + ' - '. 89 + $releeph_branch->getDisplayName(). 90 + ' requests' 91 + )); 92 + } 93 + 94 + }
+63
src/applications/releeph/controller/project/ReleephProjectActionController.php
··· 1 + <?php 2 + 3 + final class ReleephProjectActionController extends ReleephController { 4 + 5 + private $action; 6 + 7 + public function willProcessRequest(array $data) { 8 + parent::willProcessRequest($data); 9 + $this->action = $data['action']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + 15 + $action = $this->action; 16 + $rph_project = $this->getReleephProject(); 17 + 18 + switch ($action) { 19 + case 'deactivate': 20 + if ($request->isDialogFormPost()) { 21 + $rph_project->deactivate($request->getUser())->save(); 22 + return id(new AphrontRedirectResponse())->setURI('/releeph'); 23 + } 24 + 25 + $dialog = id(new AphrontDialogView()) 26 + ->setUser($request->getUser()) 27 + ->setTitle('Really deactivate Releeph Project?') 28 + ->appendChild(hsprintf( 29 + '<p>Really deactivate the Releeph project <i>%s</i>?', 30 + $rph_project->getName())) 31 + ->appendChild(hsprintf( 32 + '<p style="margin-top:1em">It will still exist, but '. 33 + 'will be hidden from the list of active projects.</p>')) 34 + ->addSubmitButton('Deactivate Releeph Project') 35 + ->addCancelButton($request->getRequestURI()); 36 + 37 + return id(new AphrontDialogResponse())->setDialog($dialog); 38 + 39 + case 'activate': 40 + $rph_project->setIsActive(1)->save(); 41 + return id(new AphrontRedirectResponse())->setURI('/releeph'); 42 + 43 + case 'delete': 44 + if ($request->isDialogFormPost()) { 45 + $rph_project->delete(); 46 + return id(new AphrontRedirectResponse()) 47 + ->setURI('/releeph/project/inactive'); 48 + } 49 + 50 + $dialog = id(new AphrontDialogView()) 51 + ->setUser($request->getUser()) 52 + ->setTitle('Really delete Releeph Project?') 53 + ->appendChild(hsprintf( 54 + '<p>Really delete the "%s" Releeph project? '. 55 + 'This cannot be undone!</p>', 56 + $rph_project->getName())) 57 + ->addSubmitButton('Delete Releeph Project') 58 + ->addCancelButton($request->getRequestURI()); 59 + return id(new AphrontDialogResponse())->setDialog($dialog); 60 + 61 + } 62 + } 63 + }
+149
src/applications/releeph/controller/project/ReleephProjectCreateController.php
··· 1 + <?php 2 + 3 + final class ReleephProjectCreateController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + $name = trim($request->getStr('name')); 8 + $trunk_branch = trim($request->getStr('trunkBranch')); 9 + $arc_pr_id = $request->getInt('arcPrID'); 10 + 11 + 12 + // Only allow arc projects with repositories. Sort and re-key by ID. 13 + $arc_projects = id(new PhabricatorRepositoryArcanistProject())->loadAll(); 14 + $arc_projects = mpull( 15 + msort( 16 + mfilter($arc_projects, 'getRepositoryID'), 17 + 'getName'), 18 + null, 19 + 'getID'); 20 + 21 + $e_name = true; 22 + $e_trunk_branch = true; 23 + $errors = array(); 24 + 25 + if ($request->isFormPost()) { 26 + if (!$name) { 27 + $e_name = 'Required'; 28 + $errors[] = 29 + 'Your releeph project should have a simple descriptive name.'; 30 + } 31 + 32 + if (!$trunk_branch) { 33 + $e_trunk_branch = 'Required'; 34 + $errors[] = 35 + 'You must specify which branch you will be picking from.'; 36 + } 37 + 38 + $all_names = mpull(id(new ReleephProject())->loadAll(), 'getName'); 39 + 40 + if (in_array($name, $all_names)) { 41 + $errors[] = "Releeph project name {$name} is already taken"; 42 + } 43 + 44 + $arc_project = $arc_projects[$arc_pr_id]; 45 + $pr_repository = $arc_project->loadRepository(); 46 + 47 + if (!$errors) { 48 + $releeph_project = id(new ReleephProject()) 49 + ->setName($name) 50 + ->setTrunkBranch($trunk_branch) 51 + ->setRepositoryID($pr_repository->getID()) 52 + ->setRepositoryPHID($pr_repository->getPHID()) 53 + ->setArcanistProjectID($arc_project->getID()) 54 + ->setCreatedByUserPHID($request->getUser()->getPHID()) 55 + ->setIsActive(1) 56 + ->save(); 57 + 58 + return id(new AphrontRedirectResponse())->setURI('/releeph/'); 59 + } 60 + } 61 + 62 + $error_view = null; 63 + if ($errors) { 64 + $error_view = new AphrontErrorView(); 65 + $error_view->setErrors($errors); 66 + $error_view->setTitle('Form Errors'); 67 + } 68 + 69 + // Make our own optgroup select control 70 + $arc_project_choices = array(); 71 + $pr_repositories = mpull( 72 + msort( 73 + array_filter( 74 + // Some arc-projects don't have repositories 75 + mpull($arc_projects, 'loadRepository')), 76 + 'getName'), 77 + null, 78 + 'getID'); 79 + 80 + foreach ($pr_repositories as $pr_repo_id => $pr_repository) { 81 + $options = array(); 82 + foreach ($arc_projects as $arc_project) { 83 + if ($arc_project->getRepositoryID() == $pr_repo_id) { 84 + $options[$arc_project->getID()] = $arc_project->getName(); 85 + } 86 + } 87 + $arc_project_choices[$pr_repository->getName()] = $options; 88 + } 89 + 90 + $project_name_input = id(new AphrontFormTextControl()) 91 + ->setLabel('Name') 92 + ->setDisableAutocomplete(true) 93 + ->setName('name') 94 + ->setValue($name) 95 + ->setError($e_name) 96 + ->setCaption('A name like "Thrift" but not "Thrift releases".'); 97 + 98 + $arc_project_input = id(new AphrontFormSelectControl()) 99 + ->setLabel('Arc Project') 100 + ->setName('arcPrID') 101 + ->setValue($arc_pr_id) 102 + ->setCaption(hsprintf( 103 + "If your Arc project isn't listed, associate it with a repository %s", 104 + phutil_tag( 105 + 'a', 106 + array( 107 + 'href' => '/repository/', 108 + 'target' => '_blank', 109 + ), 110 + 'here'))) 111 + ->setOptions($arc_project_choices); 112 + 113 + $branch_name_preview = id(new ReleephBranchPreviewView()) 114 + ->setLabel('Example Branch') 115 + ->addControl('projectName', $project_name_input) 116 + ->addControl('arcProjectID', $arc_project_input) 117 + ->addStatic('template', '') 118 + ->addStatic('isSymbolic', false); 119 + 120 + $form = id(new AphrontFormView()) 121 + ->setUser($request->getUser()) 122 + ->appendChild($project_name_input) 123 + ->appendChild($arc_project_input) 124 + ->appendChild( 125 + id(new AphrontFormTextControl()) 126 + ->setLabel('Trunk') 127 + ->setName('trunkBranch') 128 + ->setValue($trunk_branch) 129 + ->setError($e_trunk_branch) 130 + ->setCaption('The development branch, '. 131 + 'from which requests will be picked.')) 132 + ->appendChild($branch_name_preview) 133 + ->appendChild( 134 + id(new AphrontFormSubmitControl()) 135 + ->addCancelButton('/releeph/project/') 136 + ->setValue('Create')); 137 + 138 + $panel = id(new AphrontPanelView()) 139 + ->setHeader('Create Releeph Project') 140 + ->appendChild($form) 141 + ->setWidth(AphrontPanelView::WIDTH_FORM); 142 + 143 + return $this->buildStandardPageResponse( 144 + array($error_view, $panel), 145 + array( 146 + 'title' => 'Create new Releeph Project' 147 + )); 148 + } 149 + }
+388
src/applications/releeph/controller/project/ReleephProjectEditController.php
··· 1 + <?php 2 + 3 + final class ReleephProjectEditController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + 8 + $e_name = true; 9 + $e_trunk_branch = true; 10 + $e_branch_template = false; 11 + $errors = array(); 12 + 13 + $project_name = $request->getStr('name', 14 + $this->getReleephProject()->getName()); 15 + 16 + $phabricator_project_id = $request->getInt('projectID', 17 + $this->getReleephProject()->getProjectID()); 18 + $trunk_branch = $request->getStr('trunkBranch', 19 + $this->getReleephProject()->getTrunkBranch()); 20 + $branch_template = $request->getStr('branchTemplate'); 21 + if ($branch_template === null) { 22 + $branch_template = 23 + $this->getReleephProject()->getDetail('branchTemplate'); 24 + } 25 + $pick_failure_instructions = $request->getStr('pickFailureInstructions', 26 + $this->getReleephProject()->getDetail('pick_failure_instructions')); 27 + $commit_author = $request->getStr('commitWithAuthor', 28 + $this->getReleephProject()->getDetail('commitWithAuthor')); 29 + $test_paths = $request->getStr('testPaths'); 30 + if ($test_paths !== null) { 31 + $test_paths = array_filter(explode("\n", $test_paths)); 32 + } else { 33 + $test_paths = $this->getReleephProject()->getDetail('testPaths', array()); 34 + } 35 + 36 + $field_selector = $request->getStr('fieldSelector', 37 + get_class($this->getReleephProject()->getReleephFieldSelector())); 38 + 39 + $release_counter = $request->getInt( 40 + 'releaseCounter', 41 + $this->getReleephProject()->getCurrentReleaseNumber()); 42 + 43 + $arc_project_id = $this->getReleephProject()->getArcanistProjectID(); 44 + 45 + if ($request->isFormPost()) { 46 + $pusher_phids = $request->getArr('pushers'); 47 + 48 + if (!$project_name) { 49 + $e_name = 'Required'; 50 + $errors[] = 51 + 'Your releeph project should have a simple descriptive name'; 52 + } 53 + 54 + if (!$trunk_branch) { 55 + $e_trunk_branch = 'Required'; 56 + $errors[] = 57 + 'You must specify which branch you will be picking from.'; 58 + } 59 + 60 + if ($release_counter && !is_int($release_counter)) { 61 + $errors[] = "Release counter must be a positive integer!"; 62 + } 63 + 64 + $other_releeph_projects = id(new ReleephProject()) 65 + ->loadAllWhere('id <> %d', $this->getReleephProject()->getID()); 66 + $other_releeph_project_names = mpull($other_releeph_projects, 67 + 'getName', 'getID'); 68 + 69 + if (in_array($project_name, $other_releeph_project_names)) { 70 + $errors[] = "Releeph project name {$project_name} is already taken"; 71 + } 72 + 73 + foreach ($test_paths as $test_path) { 74 + $result = @preg_match($test_path, ''); 75 + $is_a_valid_regexp = $result !== false; 76 + if (!$is_a_valid_regexp) { 77 + $errors[] = 'Please provide a valid regular expression: '. 78 + "{$test_path} is not valid"; 79 + } 80 + } 81 + 82 + $project = $this->getReleephProject() 83 + ->setProjectID($phabricator_project_id) 84 + ->setTrunkBranch($trunk_branch) 85 + ->setDetail('pushers', $pusher_phids) 86 + ->setDetail('pick_failure_instructions', $pick_failure_instructions) 87 + ->setDetail('field_selector', $field_selector) 88 + ->setDetail('branchTemplate', $branch_template) 89 + ->setDetail('commitWithAuthor', $commit_author) 90 + ->setDetail('testPaths', $test_paths); 91 + 92 + if ($release_counter) { 93 + $project->setDetail('releaseCounter', $release_counter); 94 + } 95 + 96 + $fake_commit_handle = 97 + ReleephBranchTemplate::getFakeCommitHandleFor($arc_project_id); 98 + 99 + if ($branch_template) { 100 + list($branch_name, $template_errors) = id(new ReleephBranchTemplate()) 101 + ->setCommitHandle($fake_commit_handle) 102 + ->setReleephProjectName($project_name) 103 + ->interpolate($branch_template); 104 + 105 + if ($template_errors) { 106 + $e_branch_template = 'Invalid!'; 107 + foreach ($template_errors as $template_error) { 108 + $errors[] = "Template error: {$template_error}"; 109 + } 110 + } 111 + } 112 + 113 + if (!$errors) { 114 + $project->save(); 115 + 116 + return id(new AphrontRedirectResponse()) 117 + ->setURI('/releeph/project/'); 118 + } 119 + } 120 + 121 + $error_view = null; 122 + if ($errors) { 123 + $error_view = new AphrontErrorView(); 124 + $error_view->setErrors($errors); 125 + $error_view->setTitle('Form Errors'); 126 + } 127 + 128 + $projects = mpull( 129 + id(new PhabricatorProject())->loadAll(), 130 + 'getName', 131 + 'getID'); 132 + 133 + $projects[0] = '-'; // no project associated, that's ok 134 + 135 + $pusher_phids = $request->getArr( 136 + 'pushers', 137 + $this->getReleephProject()->getDetail('pushers', array())); 138 + 139 + $handles = id(new PhabricatorObjectHandleData($pusher_phids)) 140 + ->setViewer($request->getUser()) 141 + ->loadHandles(); 142 + 143 + $pusher_tokens = array(); 144 + foreach ($pusher_phids as $phid) { 145 + $pusher_tokens[$phid] = $handles[$phid]->getFullName(); 146 + } 147 + 148 + $basic_inset = id(new AphrontFormInsetView()) 149 + ->setTitle('Basics') 150 + ->appendChild( 151 + id(new AphrontFormTextControl()) 152 + ->setLabel('Name') 153 + ->setName('name') 154 + ->setValue($project_name) 155 + ->setError($e_name) 156 + ->setCaption('A name like "Thrift" but not "Thrift releases".')) 157 + ->appendChild( 158 + id(new AphrontFormStaticControl()) 159 + ->setLabel('Repository') 160 + ->setValue( 161 + $this 162 + ->getReleephProject() 163 + ->loadPhabricatorRepository() 164 + ->getName())) 165 + ->appendChild( 166 + id(new AphrontFormStaticControl()) 167 + ->setLabel('Arc Project') 168 + ->setValue( 169 + $this->getReleephProject()->loadArcanistProject()->getName())) 170 + ->appendChild( 171 + id(new AphrontFormStaticControl()) 172 + ->setLabel('Releeph Project PHID') 173 + ->setValue( 174 + $this->getReleephProject()->getPHID())) 175 + ->appendChild( 176 + id(new AphrontFormSelectControl()) 177 + ->setLabel('Phabricator Project') 178 + ->setValue($phabricator_project_id) 179 + ->setName('projectID') 180 + ->setOptions($projects)) 181 + ->appendChild( 182 + id(new AphrontFormTextControl()) 183 + ->setLabel('Trunk') 184 + ->setValue($trunk_branch) 185 + ->setName('trunkBranch') 186 + ->setError($e_trunk_branch)) 187 + ->appendChild( 188 + id(new AphrontFormTextControl()) 189 + ->setLabel('Release counter') 190 + ->setValue($release_counter) 191 + ->setName('releaseCounter') 192 + ->setCaption( 193 + "Used by the command line branch cutter's %N field")) 194 + ->appendChild( 195 + id(new AphrontFormTextAreaControl()) 196 + ->setLabel('Pick Instructions') 197 + ->setValue($pick_failure_instructions) 198 + ->setName('pickFailureInstructions') 199 + ->setCaption( 200 + "Instructions for pick failures, which will be used " . 201 + "in emails generated by failed picks")) 202 + ->appendChild( 203 + id(new AphrontFormTextAreaControl()) 204 + ->setLabel('Tests paths') 205 + ->setValue(implode("\n", $test_paths)) 206 + ->setName('testPaths') 207 + ->setCaption( 208 + 'List of strings that all test files contain in their path '. 209 + 'in this project. One string per line. '. 210 + 'Examples: \'__tests__\', \'/javatests/\'...')); 211 + 212 + $pushers_inset = id(new AphrontFormInsetView()) 213 + ->setTitle('Pushers') 214 + ->appendChild( 215 + 'Pushers are allowed to approve Releeph requests to be committed. '. 216 + 'to this project\'s branches. If you leave this blank then anyone '. 217 + 'is allowed to approve requests.') 218 + ->appendChild( 219 + id(new AphrontFormTokenizerControl()) 220 + ->setLabel('Pushers') 221 + ->setName('pushers') 222 + ->setDatasource('/typeahead/common/users/') 223 + ->setValue($pusher_tokens)); 224 + 225 + $field_selector_options = array(); 226 + $field_selector_symbols = id(new PhutilSymbolLoader()) 227 + ->setType('class') 228 + ->setConcreteOnly(true) 229 + ->setAncestorClass('ReleephFieldSelector') 230 + ->selectAndLoadSymbols(); 231 + foreach ($field_selector_symbols as $symbol) { 232 + $selector_name = $symbol['name']; 233 + $field_selector_options[$selector_name] = $selector_name; 234 + } 235 + 236 + $field_selector_blurb = hsprintf( 237 + "If you you have additional information to render about Releeph ". 238 + "requests, or want to re-arrange the UI, implement a ". 239 + "<tt>ReleephFieldSelector</tt> and select it here."); 240 + 241 + $fields_inset = id(new AphrontFormInsetView()) 242 + ->setTitle('Fields') 243 + ->appendChild($field_selector_blurb) 244 + ->appendChild( 245 + id(new AphrontFormSelectControl()) 246 + ->setLabel('Selector') 247 + ->setName('fieldSelector') 248 + ->setValue($field_selector) 249 + ->setOptions($field_selector_options)); 250 + 251 + $commit_author_inset = $this->buildCommitAuthorInset($commit_author); 252 + 253 + // Build the Template inset 254 + $markup_engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); 255 + 256 + // From DifferentialUnitFieldSpecification... 257 + $markup_engine->setConfig('viewer', $request->getUser()); 258 + 259 + $help_markup = phutil_tag( 260 + 'div', 261 + array( 262 + 'class' => 'phabricator-remarkup', 263 + ), 264 + phutil_safe_html( 265 + $markup_engine->markupText(ReleephBranchTemplate::getHelpRemarkup()))); 266 + 267 + $branch_template_input = id(new AphrontFormTextControl()) 268 + ->setName('branchTemplate') 269 + ->setValue($branch_template) 270 + ->setLabel('Template') 271 + ->setError($e_branch_template) 272 + ->setCaption( 273 + "Leave this blank to use your installation's default."); 274 + 275 + $branch_template_preview = id(new ReleephBranchPreviewView()) 276 + ->setLabel('Preview') 277 + ->addControl('template', $branch_template_input) 278 + ->addStatic('arcProjectID', $arc_project_id) 279 + ->addStatic('isSymbolic', false) 280 + ->addStatic('projectName', $this->getReleephProject()->getName()); 281 + 282 + $template_inset = id(new AphrontFormInsetView()) 283 + ->setTitle('Branch Cutting') 284 + ->appendChild( 285 + 'Provide a pattern for creating new branches.') 286 + ->appendChild($branch_template_input) 287 + ->appendChild($branch_template_preview) 288 + ->appendChild($help_markup); 289 + 290 + // Build the form 291 + $form = id(new AphrontFormView()) 292 + ->setUser($request->getUser()) 293 + ->appendChild($basic_inset) 294 + ->appendChild($pushers_inset) 295 + ->appendChild($fields_inset) 296 + ->appendChild($commit_author_inset) 297 + ->appendChild($template_inset); 298 + 299 + $form 300 + ->appendChild( 301 + id(new AphrontFormSubmitControl()) 302 + ->addCancelButton('/releeph/project/') 303 + ->setValue('Save')); 304 + 305 + $panel = id(new AphrontPanelView()) 306 + ->setHeader('Edit Releeph Project') 307 + ->appendChild($form) 308 + ->setWidth(AphrontPanelView::WIDTH_FORM); 309 + 310 + return $this->buildStandardPageResponse( 311 + array($error_view, $panel), 312 + array('title' => 'Edit Releeph Project')); 313 + } 314 + 315 + private function buildCommitAuthorInset($current) { 316 + $vcs_type = $this->getReleephProject() 317 + ->loadPhabricatorRepository() 318 + ->getVersionControlSystem(); 319 + 320 + switch ($vcs_type) { 321 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 322 + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 323 + break; 324 + 325 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 326 + return; 327 + break; 328 + } 329 + 330 + $vcs_name = PhabricatorRepositoryType::getNameForRepositoryType($vcs_type); 331 + 332 + $help_markup = hsprintf(<<<EOTEXT 333 + When your project's release engineers run <tt>arc releeph</tt>, they will be 334 + listed as the <strong>committer</strong> of the code committed to release 335 + branches. 336 + 337 + %s allows you to specify a separate author when committing code. Some 338 + tools use the author of a commit (rather than the committer) when they need to 339 + notify someone about a build or test failure. 340 + 341 + Releeph can use one of the following to set the <strong>author</strong> of the 342 + commits it makes: 343 + EOTEXT 344 + , $vcs_name); 345 + 346 + $trunk = $this->getReleephProject()->getTrunkBranch(); 347 + 348 + $options = array( 349 + array( 350 + 'value' => ReleephProject::COMMIT_AUTHOR_FROM_DIFF, 351 + 'label' => 'Original Author', 352 + 'caption' => 353 + "The author of the original commit in {$trunk}.", 354 + ), 355 + array( 356 + 'value' => ReleephProject::COMMIT_AUTHOR_REQUESTOR, 357 + 'label' => 'Requestor', 358 + 'caption' => 359 + "The person who requested that this code go into the release.", 360 + ), 361 + array( 362 + 'value' => ReleephProject::COMMIT_AUTHOR_NONE, 363 + 'label' => "None", 364 + 'caption' => 365 + "Only record the default committer information.", 366 + ), 367 + ); 368 + 369 + if (!$current) { 370 + $current = ReleephProject::COMMIT_AUTHOR_FROM_DIFF; 371 + } 372 + 373 + $control = id(new AphrontFormRadioButtonControl()) 374 + ->setLabel('Author') 375 + ->setName('commitWithAuthor') 376 + ->setValue($current); 377 + 378 + foreach ($options as $dict) { 379 + $control->addButton($dict['value'], $dict['label'], $dict['caption']); 380 + } 381 + 382 + return id(new AphrontFormInsetView()) 383 + ->setTitle('Authors') 384 + ->appendChild($help_markup) 385 + ->appendChild($control); 386 + } 387 + 388 + }
+70
src/applications/releeph/controller/project/ReleephProjectListController.php
··· 1 + <?php 2 + 3 + final class ReleephProjectListController extends PhabricatorController { 4 + 5 + public function processRequest() { 6 + $path = $this->getRequest()->getRequestURI()->getPath(); 7 + $is_active = strpos($path, 'inactive/') === false; 8 + 9 + $releeph_projects = mfilter( 10 + id(new ReleephProject())->loadAll(), 11 + 'getIsActive', 12 + !$is_active); 13 + $releeph_projects = msort($releeph_projects, 'getName'); 14 + 15 + $releeph_projects_set = new LiskDAOSet(); 16 + foreach ($releeph_projects as $releeph_project) { 17 + $releeph_projects_set->addToSet($releeph_project); 18 + } 19 + 20 + $panel = new AphrontPanelView(); 21 + 22 + if ($is_active) { 23 + $view_inactive_link = phutil_tag( 24 + 'a', 25 + array( 26 + 'href' => '/releeph/project/inactive/', 27 + ), 28 + 'View inactive projects'); 29 + $panel 30 + ->setHeader(hsprintf( 31 + 'Active Releeph Projects &middot; %s', $view_inactive_link)) 32 + ->appendChild( 33 + id(new ReleephActiveProjectListView()) 34 + ->setUser($this->getRequest()->getUser()) 35 + ->setReleephProjects($releeph_projects)); 36 + } else { 37 + $view_active_link = phutil_tag( 38 + 'a', 39 + array( 40 + 'href' => '/releeph/project/' 41 + ), 42 + 'View active projects'); 43 + $panel 44 + ->setHeader(hsprintf( 45 + 'Inactive Releeph Projects &middot; %s', $view_active_link)) 46 + ->appendChild( 47 + id(new ReleephInactiveProjectListView()) 48 + ->setUser($this->getRequest()->getUser()) 49 + ->setReleephProjects($releeph_projects)); 50 + } 51 + 52 + if ($is_active) { 53 + $create_new_project_button = phutil_tag( 54 + 'a', 55 + array( 56 + 'href' => '/releeph/project/create/', 57 + 'class' => 'green button', 58 + ), 59 + 'Create New Project'); 60 + $panel->addButton($create_new_project_button); 61 + } 62 + 63 + return $this->buildStandardPageResponse( 64 + $panel, 65 + array( 66 + 'title' => 'List Releeph Projects' 67 + )); 68 + } 69 + 70 + }
+45
src/applications/releeph/controller/project/ReleephProjectViewController.php
··· 1 + <?php 2 + 3 + final class ReleephProjectViewController extends ReleephController { 4 + 5 + public function processRequest() { 6 + // Load all branches 7 + $releeph_project = $this->getReleephProject(); 8 + $releeph_branches = id(new ReleephBranch()) 9 + ->loadAllWhere('releephProjectID = %d', 10 + $releeph_project->getID()); 11 + 12 + $path = $this->getRequest()->getRequestURI()->getPath(); 13 + $is_open_branches = strpos($path, 'closedbranches/') === false; 14 + 15 + $view = id(new ReleephProjectView()) 16 + ->setShowOpenBranches($is_open_branches) 17 + ->setUser($this->getRequest()->getUser()) 18 + ->setReleephProject($releeph_project) 19 + ->setBranches($releeph_branches); 20 + 21 + $crumbs = $this->buildApplicationCrumbs() 22 + ->addCrumb( 23 + id(new PhabricatorCrumbView()) 24 + ->setName($releeph_project->getName()) 25 + ->setHref($releeph_project->getURI())); 26 + 27 + if ($releeph_project->getIsActive()) { 28 + $crumbs->addAction( 29 + id(new PhabricatorMenuItemView()) 30 + ->setHref($releeph_project->getURI('cutbranch')) 31 + ->setName('Cut New Branch') 32 + ->setIcon('create')); 33 + } 34 + 35 + return $this->buildStandardPageResponse( 36 + array( 37 + $crumbs, 38 + $view, 39 + ), 40 + array( 41 + 'title' => $releeph_project->getName().' Releeph Project' 42 + )); 43 + } 44 + 45 + }
+71
src/applications/releeph/controller/request/ReleephRequestActionController.php
··· 1 + <?php 2 + 3 + final class ReleephRequestActionController extends ReleephController { 4 + 5 + private $action; 6 + 7 + public function willProcessRequest(array $data) { 8 + parent::willProcessRequest($data); 9 + $this->action = $data['action']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + 15 + $releeph_branch = $this->getReleephBranch(); 16 + $releeph_request = $this->getReleephRequest(); 17 + 18 + $releeph_branch->populateReleephRequestHandles( 19 + $request->getUser(), array($releeph_request)); 20 + 21 + $action = $this->action; 22 + 23 + $user = $request->getUser(); 24 + 25 + $origin_uri = $releeph_request->loadReleephBranch()->getURI(); 26 + 27 + $editor = id(new ReleephRequestEditor($releeph_request)) 28 + ->setActor($user); 29 + 30 + switch ($action) { 31 + case 'want': 32 + case 'pass': 33 + static $action_map = array( 34 + 'want' => ReleephRequest::INTENT_WANT, 35 + 'pass' => ReleephRequest::INTENT_PASS); 36 + $intent = $action_map[$action]; 37 + $editor->changeUserIntent($user, $intent); 38 + break; 39 + 40 + case 'mark-manually-picked': 41 + $editor->markManuallyActioned('pick'); 42 + break; 43 + 44 + case 'mark-manually-reverted': 45 + $editor->markManuallyActioned('revert'); 46 + break; 47 + 48 + default: 49 + throw new Exception("unknown or unimplemented action {$action}"); 50 + } 51 + 52 + // If we're adding a new user to userIntents, we'll have to re-populate 53 + // request handles to load that user's data. 54 + // 55 + // This is cheap enough to do every time. 56 + $this->getReleephBranch()->populateReleephRequestHandles( 57 + $user, array($releeph_request)); 58 + 59 + $list = id(new ReleephRequestHeaderListView()) 60 + ->setReleephProject($this->getReleephProject()) 61 + ->setReleephBranch($this->getReleephBranch()) 62 + ->setReleephRequests(array($releeph_request)) 63 + ->setUser($request->getUser()) 64 + ->setAphrontRequest($this->getRequest()) 65 + ->setOriginType('request'); 66 + 67 + return id(new AphrontAjaxResponse())->setContent(array( 68 + 'markup' => head($list->renderInner()) 69 + )); 70 + } 71 + }
+165
src/applications/releeph/controller/request/ReleephRequestCreateController.php
··· 1 + <?php 2 + 3 + final class ReleephRequestCreateController extends ReleephController { 4 + 5 + const MAX_SUMMARY_LENGTH = 70; 6 + 7 + public function processRequest() { 8 + $request = $this->getRequest(); 9 + 10 + // We arrived via /releeph/request/create/?branchID=$id 11 + $releeph_branch_id = $request->getInt('branchID'); 12 + if ($releeph_branch_id) { 13 + $releeph_branch = id(new ReleephBranch())->load($releeph_branch_id); 14 + } else { 15 + // We arrived via /releeph/$project/$branch/request. 16 + // 17 + // If this throws an Exception, then somethind weird happened. 18 + $releeph_branch = $this->getReleephBranch(); 19 + } 20 + 21 + $releeph_project = $releeph_branch->loadReleephProject(); 22 + $repo = $releeph_project->loadPhabricatorRepository(); 23 + 24 + $request_identifier = $request->getStr('requestIdentifierRaw'); 25 + $e_request_identifier = true; 26 + 27 + $releeph_request = new ReleephRequest(); 28 + 29 + $errors = array(); 30 + 31 + $selector = $releeph_project->getReleephFieldSelector(); 32 + $fields = $selector->getFieldSpecifications(); 33 + foreach ($fields as $field) { 34 + $field 35 + ->setReleephProject($releeph_project) 36 + ->setReleephBranch($releeph_branch) 37 + ->setReleephRequest($releeph_request); 38 + } 39 + 40 + if ($request->isFormPost()) { 41 + foreach ($fields as $field) { 42 + if ($field->isEditable()) { 43 + try { 44 + $field->setValueFromAphrontRequest($request); 45 + } catch (ReleephFieldParseException $ex) { 46 + $errors[] = $ex->getMessage(); 47 + } 48 + } 49 + } 50 + 51 + $pr_commit = null; 52 + $finder = id(new ReleephCommitFinder()) 53 + ->setReleephProject($releeph_project); 54 + try { 55 + $pr_commit = $finder->fromPartial($request_identifier); 56 + } catch (Exception $e) { 57 + $e_request_identifier = 'Invalid'; 58 + $errors[] = 59 + "Request {$request_identifier} is probably not a valid commit"; 60 + $errors[] = $e->getMessage(); 61 + } 62 + 63 + $pr_commit_data = null; 64 + if (!$errors) { 65 + $pr_commit_data = $pr_commit->loadCommitData(); 66 + if (!$pr_commit_data) { 67 + $e_request_identifier = 'Not parsed yet'; 68 + $errors[] = "The requested commit hasn't been parsed yet."; 69 + } 70 + } 71 + 72 + if (!$errors) { 73 + $existing = id(new ReleephRequest()) 74 + ->loadOneWhere('requestCommitPHID = %s AND branchID = %d', 75 + $pr_commit->getPHID(), $releeph_branch->getID()); 76 + 77 + if ($existing) { 78 + return id(new AphrontRedirectResponse()) 79 + ->setURI('/releeph/request/edit/'.$existing->getID(). 80 + '?existing=1'); 81 + } 82 + 83 + id(new ReleephRequestEditor($releeph_request)) 84 + ->setActor($request->getUser()) 85 + ->create($pr_commit, $releeph_branch); 86 + 87 + return id(new AphrontRedirectResponse()) 88 + ->setURI($releeph_branch->getURI()); 89 + } 90 + } 91 + 92 + $error_view = null; 93 + if ($errors) { 94 + $error_view = new AphrontErrorView(); 95 + $error_view->setErrors($errors); 96 + $error_view->setTitle('Form Errors'); 97 + } 98 + 99 + // For the typeahead 100 + $branch_cut_point = id(new PhabricatorRepositoryCommit()) 101 + ->loadOneWhere( 102 + 'phid = %s', 103 + $releeph_branch->getCutPointCommitPHID()); 104 + 105 + // Build the form 106 + $form = id(new AphrontFormView()) 107 + ->setUser($request->getUser()); 108 + 109 + $origin = null; 110 + $diff_rev_id = $request->getStr('D'); 111 + if ($diff_rev_id) { 112 + $diff_rev = id(new DifferentialRevision())->load($diff_rev_id); 113 + $origin = '/D'.$diff_rev->getID(); 114 + $title = sprintf( 115 + 'D%d: %s', 116 + $diff_rev_id, 117 + $diff_rev->getTitle()); 118 + $form 119 + ->addHiddenInput('requestIdentifierRaw', 'D'.$diff_rev_id) 120 + ->appendChild( 121 + id(new AphrontFormStaticControl()) 122 + ->setLabel('Diff') 123 + ->setValue($title)); 124 + } else { 125 + $origin = $releeph_branch->getURI(); 126 + $form->appendChild( 127 + id(new ReleephRequestTypeaheadControl()) 128 + ->setName('requestIdentifierRaw') 129 + ->setLabel('Commit ID') 130 + ->setRepo($repo) 131 + ->setValue($request_identifier) 132 + ->setError($e_request_identifier) 133 + ->setStartTime($branch_cut_point->getEpoch()) 134 + ->setCaption( 135 + 'Start typing to autocomplete on commit title, '. 136 + 'or give a Phabricator commit identifier like rFOO1234')); 137 + } 138 + 139 + // Fields 140 + foreach ($fields as $field) { 141 + if ($field->isEditable()) { 142 + $control = $field->renderEditControl($request); 143 + $form->appendChild($control); 144 + } 145 + } 146 + 147 + $form 148 + ->appendChild( 149 + id(new AphrontFormSubmitControl()) 150 + ->addCancelButton($origin) 151 + ->setValue('Request')); 152 + 153 + $panel = id(new AphrontPanelView()) 154 + ->setHeader( 155 + 'Request for '. 156 + $releeph_branch->getDisplayNameWithDetail()) 157 + ->setWidth(AphrontPanelView::WIDTH_FORM) 158 + ->appendChild($form); 159 + 160 + return $this->buildStandardPageResponse( 161 + array($error_view, $panel), 162 + array('title' => 'Request pick')); 163 + } 164 + 165 + }
+99
src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
··· 1 + <?php 2 + 3 + final class ReleephRequestDifferentialCreateController 4 + extends ReleephController { 5 + 6 + private $revision; 7 + 8 + public function willProcessRequest($data) { 9 + $diff_rev_id = $data['diffRevID']; 10 + $diff_rev = id(new DifferentialRevision())->load($diff_rev_id); 11 + if (!$diff_rev) { 12 + throw new Exception(sprintf('D%d not found!', $diff_rev_id)); 13 + } 14 + $this->revision = $diff_rev; 15 + } 16 + 17 + public function processRequest() { 18 + $request = $this->getRequest(); 19 + $user = $request->getUser(); 20 + 21 + $arc_project = id(new PhabricatorRepositoryArcanistProject()) 22 + ->loadOneWhere('phid = %s', $this->revision->getArcanistProjectPHID()); 23 + 24 + $projects = id(new ReleephProject())->loadAllWhere( 25 + 'arcanistProjectID = %d AND isActive = 1', 26 + $arc_project->getID()); 27 + if (!$projects) { 28 + throw new ReleephRequestException(sprintf( 29 + "D%d belongs to the '%s' Arcanist project, ". 30 + "which is not part of any Releeph project!", 31 + $this->revision->getID(), 32 + $arc_project->getName())); 33 + } 34 + 35 + $branches = id(new ReleephBranch())->loadAllWhere( 36 + 'releephProjectID IN (%Ld) AND isActive = 1', 37 + mpull($projects, 'getID')); 38 + if (!$branches) { 39 + throw new ReleephRequestException(sprintf( 40 + "D%d could be in the Releeph project(s) %s, ". 41 + "but this project / none of these projects have open branches.", 42 + $this->revision->getID(), 43 + implode(', ', mpull($projects, 'getName')))); 44 + } 45 + 46 + if (count($branches) === 1) { 47 + return id(new AphrontRedirectResponse()) 48 + ->setURI($this->buildReleephRequestURI(head($branches))); 49 + } 50 + 51 + $projects = msort( 52 + mpull($projects, null, 'getID'), 53 + 'getName'); 54 + 55 + $branch_groups = mgroup($branches, 'getReleephProjectID'); 56 + 57 + require_celerity_resource('releeph-request-differential-create-dialog'); 58 + $dialog = id(new AphrontDialogView()) 59 + ->setUser($user) 60 + ->setTitle('Choose Releeph Branch') 61 + ->setClass('releeph-request-differential-create-dialog') 62 + ->addCancelButton('/D'.$request->getStr('D')); 63 + 64 + $dialog->appendChild( 65 + "This differential revision changes code that is associated ". 66 + "with multiple Releeph branches. ". 67 + "Please select the branch where you would like this code to be picked."); 68 + 69 + foreach ($branch_groups as $project_id => $branches) { 70 + $project = idx($projects, $project_id); 71 + $dialog->appendChild( 72 + phutil_tag( 73 + 'h1', 74 + array(), 75 + $project->getName())); 76 + $branches = msort($branches, 'getBasename'); 77 + foreach ($branches as $branch) { 78 + $uri = $this->buildReleephRequestURI($branch); 79 + $dialog->appendChild( 80 + phutil_tag( 81 + 'a', 82 + array( 83 + 'href' => $uri, 84 + ), 85 + $branch->getDisplayNameWithDetail())); 86 + } 87 + } 88 + 89 + return id(new AphrontDialogResponse) 90 + ->setDialog($dialog); 91 + } 92 + 93 + private function buildReleephRequestURI(ReleephBranch $branch) { 94 + return id(new PhutilURI('/releeph/request/create/')) 95 + ->setQueryParam('branchID', $branch->getID()) 96 + ->setQueryParam('D', $this->revision->getID()); 97 + } 98 + 99 + }
+132
src/applications/releeph/controller/request/ReleephRequestEditController.php
··· 1 + <?php 2 + 3 + final class ReleephRequestEditController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + 8 + $releeph_branch = $this->getReleephBranch(); 9 + $releeph_request = $this->getReleephRequest(); 10 + 11 + $releeph_branch->populateReleephRequestHandles( 12 + $request->getUser(), array($releeph_request)); 13 + 14 + $phids = array(); 15 + $phids[] = $releeph_request->getRequestCommitPHID(); 16 + $phids[] = $releeph_request->getRequestUserPHID(); 17 + $phids[] = $releeph_request->getCommittedByUserPHID(); 18 + 19 + $handles = id(new PhabricatorObjectHandleData($phids)) 20 + ->setViewer($request->getUser()) 21 + ->loadHandles(); 22 + 23 + $age_string = phabricator_format_relative_time( 24 + time() - $releeph_request->getDateCreated()); 25 + 26 + // Warn the user if we see this 27 + $notice_view = null; 28 + if ($request->getInt('existing')) { 29 + $notice_messages = array( 30 + 'You are editing an existing pick request!', 31 + hsprintf( 32 + "Requested %s ago by %s", 33 + $age_string, 34 + $handles[$releeph_request->getRequestUserPHID()]->renderLink()) 35 + ); 36 + $notice_view = id(new AphrontErrorView()) 37 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 38 + ->setErrors($notice_messages); 39 + } 40 + 41 + // <aidehua> epriestley: Is it common to pass around a referer URL to 42 + // return from whence one came? [...] 43 + // <epriestley> If you only have two places, maybe consider some parameter 44 + // rather than the full URL. 45 + switch ($request->getStr('origin')) { 46 + case 'request': 47 + $origin_uri = '/RQ'.$releeph_request->getID(); 48 + break; 49 + 50 + case 'branch': 51 + default: 52 + $origin_uri = $releeph_request->loadReleephBranch()->getURI(); 53 + break; 54 + } 55 + 56 + $errors = array(); 57 + 58 + $selector = $this->getReleephProject()->getReleephFieldSelector(); 59 + $fields = $selector->getFieldSpecifications(); 60 + foreach ($fields as $field) { 61 + $field 62 + ->setReleephProject($this->getReleephProject()) 63 + ->setReleephBranch($this->getReleephBranch()) 64 + ->setReleephRequest($this->getReleephRequest()); 65 + } 66 + 67 + if ($request->isFormPost()) { 68 + foreach ($fields as $field) { 69 + if ($field->isEditable()) { 70 + try { 71 + $field->setValueFromAphrontRequest($request); 72 + } catch (ReleephFieldParseException $ex) { 73 + $errors[] = $ex->getMessage(); 74 + } 75 + } 76 + } 77 + 78 + if (!$errors) { 79 + $releeph_request->save(); 80 + return id(new AphrontRedirectResponse())->setURI($origin_uri); 81 + } 82 + } 83 + 84 + /** 85 + * Build the rest of the page 86 + */ 87 + $error_view = null; 88 + if ($errors) { 89 + $error_view = new AphrontErrorView(); 90 + $error_view->setErrors($errors); 91 + $error_view->setTitle('Form Errors'); 92 + } 93 + 94 + $form = id(new AphrontFormView()) 95 + ->setUser($request->getUser()) 96 + ->appendChild( 97 + id(new AphrontFormMarkupControl()) 98 + ->setLabel('Original Commit') 99 + ->setValue( 100 + $handles[$releeph_request->getRequestCommitPHID()]->renderLink())) 101 + ->appendChild( 102 + id(new AphrontFormMarkupControl()) 103 + ->setLabel('Requestor') 104 + ->setValue(hsprintf( 105 + '%s %s ago', 106 + $handles[$releeph_request->getRequestUserPHID()]->renderLink(), 107 + $age_string))); 108 + 109 + // Fields 110 + foreach ($fields as $field) { 111 + if ($field->isEditable()) { 112 + $control = $field->renderEditControl($request); 113 + $form->appendChild($control); 114 + } 115 + } 116 + 117 + $form 118 + ->appendChild( 119 + id(new AphrontFormSubmitControl()) 120 + ->addCancelButton($origin_uri, 'Cancel') 121 + ->setValue('Save')); 122 + 123 + $panel = id(new AphrontPanelView()) 124 + ->setHeader('Edit Pick Request') 125 + ->setWidth(AphrontPanelView::WIDTH_FORM) 126 + ->appendChild($form); 127 + 128 + return $this->buildStandardPageResponse( 129 + array($notice_view, $error_view, $panel), 130 + array('title', 'Edit Pick Request')); 131 + } 132 + }
+92
src/applications/releeph/controller/request/ReleephRequestTypeaheadController.php
··· 1 + <?php 2 + 3 + final class ReleephRequestTypeaheadController 4 + extends PhabricatorTypeaheadDatasourceController { 5 + 6 + public function processRequest() { 7 + $request = $this->getRequest(); 8 + 9 + $query = $request->getStr('q'); 10 + $repo_id = $request->getInt('repo'); 11 + $since = $request->getInt('since'); 12 + $limit = $request->getInt('limit'); 13 + 14 + $now = time(); 15 + $data = array(); 16 + 17 + // Dummy instances used for getting connections, table names, etc. 18 + $pr_commit = new PhabricatorRepositoryCommit(); 19 + $pr_commit_data = new PhabricatorRepositoryCommitData(); 20 + 21 + $conn = $pr_commit->establishConnection('r'); 22 + 23 + $rows = queryfx_all( 24 + $conn, 25 + 'SELECT 26 + rc.phid as commitPHID, 27 + rc.authorPHID, 28 + rcd.authorName, 29 + SUBSTRING(rcd.commitMessage, 1, 100) AS shortMessage, 30 + rc.commitIdentifier, 31 + rc.epoch 32 + FROM %T rc 33 + INNER JOIN %T rcd ON rcd.commitID = rc.id 34 + WHERE repositoryID = %d 35 + AND rc.epoch >= %d 36 + AND ( 37 + rcd.commitMessage LIKE %~ 38 + OR 39 + rc.commitIdentifier LIKE %~ 40 + ) 41 + ORDER BY rc.epoch DESC 42 + LIMIT %d', 43 + $pr_commit->getTableName(), 44 + $pr_commit_data->getTableName(), 45 + $repo_id, 46 + $since, 47 + $query, 48 + $query, 49 + $limit); 50 + 51 + foreach ($rows as $row) { 52 + $full_commit_id = $row['commitIdentifier']; 53 + $short_commit_id = substr($full_commit_id, 0, 12); 54 + $first_line = $this->getFirstLine($row['shortMessage']); 55 + $data[] = array( 56 + $full_commit_id, 57 + $short_commit_id, 58 + $row['authorName'], 59 + phabricator_format_relative_time($now - $row['epoch']), 60 + $first_line, 61 + ); 62 + } 63 + 64 + return id(new AphrontAjaxResponse()) 65 + ->setContent($data); 66 + } 67 + 68 + /** 69 + * Split either at the first new line, or a bunch of dashes. 70 + * 71 + * Really just a legacy from old Releeph Daemon commit messages where I used 72 + * to say: 73 + * 74 + * Commit of FOO for BAR 75 + * ------------ 76 + * This does X Y Z 77 + * 78 + */ 79 + private function getFirstLine($commit_message_fragment) { 80 + static $separators = array('-------', "\n"); 81 + $string = ltrim($commit_message_fragment); 82 + $first_line = $string; 83 + foreach ($separators as $separator) { 84 + if ($pos = strpos($string, $separator)) { 85 + $first_line = substr($string, 0, $pos); 86 + break; 87 + } 88 + } 89 + return $first_line; 90 + } 91 + 92 + }
+99
src/applications/releeph/controller/request/ReleephRequestViewController.php
··· 1 + <?php 2 + 3 + final class ReleephRequestViewController extends ReleephController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + 8 + $uri_path = $request->getRequestURI()->getPath(); 9 + $legacy_prefix = '/releeph/request/'; 10 + if (strncmp($uri_path, $legacy_prefix, strlen($legacy_prefix)) === 0) { 11 + return id(new AphrontRedirectResponse()) 12 + ->setURI('/RQ'.$this->getReleephRequest()->getID()); 13 + } 14 + 15 + $releeph_request = $this->getReleephRequest(); 16 + $releeph_branch = $this->getReleephBranch(); 17 + $releeph_project = $this->getReleephProject(); 18 + 19 + $releeph_branch->populateReleephRequestHandles( 20 + $request->getUser(), array($releeph_request)); 21 + 22 + $rq_view = 23 + id(new ReleephRequestHeaderListView()) 24 + ->setReleephProject($releeph_project) 25 + ->setReleephBranch($releeph_branch) 26 + ->setReleephRequests(array($releeph_request)) 27 + ->setUser($request->getUser()) 28 + ->setAphrontRequest($this->getRequest()) 29 + ->setReloadOnStateChange(true) 30 + ->setOriginType('request'); 31 + 32 + $events = $releeph_request->loadEvents(); 33 + $phids = array_mergev(mpull($events, 'extractPHIDs')); 34 + $handles = id(new PhabricatorObjectHandleData($phids)) 35 + ->setViewer($request->getUser()) 36 + ->loadHandles(); 37 + 38 + $rq_event_list_view = 39 + id(new ReleephRequestEventListView()) 40 + ->setUser($request->getUser()) 41 + ->setEvents($events) 42 + ->setHandles($handles); 43 + 44 + // Handle comment submit 45 + $origin_uri = '/RQ'.$releeph_request->getID(); 46 + if ($request->isFormPost()) { 47 + id(new ReleephRequestEditor($releeph_request)) 48 + ->setActor($request->getUser()) 49 + ->addComment($request->getStr('comment')); 50 + return id(new AphrontRedirectResponse())->setURI($origin_uri); 51 + } 52 + 53 + $form = id(new AphrontFormView()) 54 + ->setUser($request->getUser()) 55 + ->appendChild( 56 + id(new AphrontFormTextAreaControl()) 57 + ->setName('comment')) 58 + ->appendChild( 59 + id(new AphrontFormSubmitControl()) 60 + ->addCancelButton($origin_uri, 'Cancel') 61 + ->setValue("Submit")); 62 + 63 + $rq_comment_form = id(new AphrontPanelView()) 64 + ->setHeader('Add a comment') 65 + ->setWidth(AphrontPanelView::WIDTH_FULL) 66 + ->appendChild($form); 67 + 68 + $title = hsprintf("RQ%d: %s", 69 + $releeph_request->getID(), 70 + $releeph_request->getSummaryForDisplay()); 71 + 72 + $crumbs = $this->buildApplicationCrumbs() 73 + ->addCrumb( 74 + id(new PhabricatorCrumbView()) 75 + ->setName($releeph_project->getName()) 76 + ->setHref($releeph_project->getURI())) 77 + ->addCrumb( 78 + id(new PhabricatorCrumbView()) 79 + ->setName($releeph_branch->getDisplayNameWithDetail()) 80 + ->setHref($releeph_branch->getURI())) 81 + ->addCrumb( 82 + id(new PhabricatorCrumbView()) 83 + ->setName('RQ'.$releeph_request->getID()) 84 + ->setHref('/RQ'.$releeph_request->getID())); 85 + 86 + return $this->buildStandardPageResponse( 87 + array( 88 + $crumbs, 89 + array( 90 + $rq_view, 91 + $rq_event_list_view, 92 + $rq_comment_form 93 + ) 94 + ), 95 + array( 96 + 'title' => $title 97 + )); 98 + } 99 + }
+348
src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
··· 1 + <?php 2 + 3 + /** 4 + * This DifferentialFieldSpecification exists for two reason: 5 + * 6 + * 1: To parse "Releeph: picks RQ<nn>" headers in commits created by 7 + * arc-releeph so that RQs committed by arc-releeph have real 8 + * PhabricatorRepositoryCommits associated with them (instaed of just the SHA 9 + * of the commit, as seen by the pusher). 10 + * 11 + * 2: If requestors want to commit directly to their release branch, they can 12 + * use this header to (i) indicate on a differential revision that this 13 + * differential revision is for the release branch, and (ii) when they land 14 + * their diff on to the release branch manually, the ReleephRequest is 15 + * automatically updated (instead of having to use the "Mark Manually Picked" 16 + * button.) 17 + * 18 + */ 19 + final class DifferentialReleephRequestFieldSpecification 20 + extends DifferentialFieldSpecification { 21 + 22 + const ACTION_PICKS = 'picks'; 23 + const ACTION_REVERTS = 'reverts'; 24 + 25 + private $releephAction; 26 + private $releephPHIDs = array(); 27 + 28 + public function getStorageKey() { 29 + return 'releeph:actions'; 30 + } 31 + 32 + public function getValueForStorage() { 33 + return json_encode(array( 34 + 'releephAction' => $this->releephAction, 35 + 'releephPHIDs' => $this->releephPHIDs, 36 + )); 37 + } 38 + 39 + public function setValueFromStorage($json) { 40 + if ($json) { 41 + $dict = json_decode($json, true); 42 + $this->releephAction = idx($dict, 'releephAction'); 43 + $this->releephPHIDs = idx($dict, 'releephPHIDs'); 44 + } 45 + return $this; 46 + } 47 + 48 + public function shouldAppearOnRevisionView() { 49 + return true; 50 + } 51 + 52 + public function renderLabelForRevisionView() { 53 + return 'Releeph'; 54 + } 55 + 56 + public function getRequiredHandlePHIDs() { 57 + return mpull($this->loadReleephRequests(), 'getPHID'); 58 + } 59 + 60 + public function renderValueForRevisionView() { 61 + static $tense = array( 62 + self::ACTION_PICKS => array( 63 + 'future' => 'Will pick', 64 + 'past' => 'Picked', 65 + ), 66 + self::ACTION_REVERTS => array( 67 + 'future' => 'Will revert', 68 + 'past' => 'Reverted', 69 + ), 70 + ); 71 + 72 + $releeph_requests = $this->loadReleephRequests(); 73 + if (!$releeph_requests) { 74 + return null; 75 + } 76 + 77 + $status = $this->getRevision()->getStatus(); 78 + if ($status == ArcanistDifferentialRevisionStatus::CLOSED) { 79 + $verb = $tense[$this->releephAction]['past']; 80 + } else { 81 + $verb = $tense[$this->releephAction]['future']; 82 + } 83 + 84 + $parts = hsprintf('%s...', $verb); 85 + foreach ($releeph_requests as $releeph_request) { 86 + $parts->appendHTML(phutil_tag('br')); 87 + $parts->appendHTML( 88 + $this->getHandle($releeph_request->getPHID())->renderLink()); 89 + } 90 + 91 + return $parts; 92 + } 93 + 94 + public function shouldAppearOnCommitMessage() { 95 + return true; 96 + } 97 + 98 + public function getCommitMessageKey() { 99 + return 'releephActions'; 100 + } 101 + 102 + public function setValueFromParsedCommitMessage($dict) { 103 + $this->releephAction = $dict['releephAction']; 104 + $this->releephPHIDs = $dict['releephPHIDs']; 105 + return $this; 106 + } 107 + 108 + public function renderValueForCommitMessage($is_edit) { 109 + $releeph_requests = $this->loadReleephRequests(); 110 + if (!$releeph_requests) { 111 + return null; 112 + } 113 + 114 + $parts = array($this->releephAction); 115 + foreach ($releeph_requests as $releeph_request) { 116 + $parts[] = 'RQ'.$releeph_request->getID(); 117 + } 118 + 119 + return implode(' ', $parts); 120 + } 121 + 122 + /** 123 + * Releeph fields should look like: 124 + * 125 + * Releeph: picks RQ1 RQ2, RQ3 126 + * Releeph: reverts RQ1 127 + */ 128 + public function parseValueFromCommitMessage($value) { 129 + /** 130 + * Releeph commit messages look like this (but with more blank lines, 131 + * omitted here): 132 + * 133 + * Make CaptainHaddock more reasonable 134 + * Releeph: picks RQ1 135 + * Requested By: edward 136 + * Approved By: edward (requestor) 137 + * Request Reason: x 138 + * Summary: Make the Haddock implementation more reasonable. 139 + * Test Plan: none 140 + * Reviewers: user1 141 + * 142 + * Some of these fields are recognized by Differential (e.g. "Requested 143 + * By"). They are folded up into the "Releeph" field, parsed by this 144 + * class. As such $value includes more than just the first-line: 145 + * 146 + * "picks RQ1\n\nRequested By: edward\n\nApproved By: edward (requestor)" 147 + * 148 + * To hack around this, just consider the first line of $value when 149 + * determining what Releeph actions the parsed commit is performing. 150 + */ 151 + $first_line = head(array_filter(explode("\n", $value))); 152 + 153 + $tokens = preg_split('/\s*,?\s+/', $first_line); 154 + $raw_action = array_shift($tokens); 155 + $action = strtolower($raw_action); 156 + 157 + if (!$action) { 158 + return null; 159 + } 160 + 161 + switch ($action) { 162 + case self::ACTION_REVERTS: 163 + case self::ACTION_PICKS: 164 + break; 165 + 166 + default: 167 + throw new DifferentialFieldParseException( 168 + "Commit message contains unknown Releeph action '{$raw_action}'!"); 169 + break; 170 + } 171 + 172 + $releeph_requests = array(); 173 + foreach ($tokens as $token) { 174 + $match = array(); 175 + if (!preg_match('/^(?:RQ)?(\d+)$/i', $token, $match)) { 176 + $label = $this->renderLabelForCommitMessage(); 177 + throw new DifferentialFieldParseException( 178 + "Commit message contains unparseable ". 179 + "Releeph request token '{$token}'!"); 180 + } 181 + 182 + $id = (int) $match[1]; 183 + $releeph_request = id(new ReleephRequest())->load($id); 184 + 185 + if (!$releeph_request) { 186 + throw new DifferentialFieldParseException( 187 + "Commit message references non existent releeph request: {$value}!"); 188 + } 189 + 190 + $releeph_requests[] = $releeph_request; 191 + } 192 + 193 + if (count($releeph_requests) > 1) { 194 + $rqs_seen = array(); 195 + $groups = array(); 196 + foreach ($releeph_requests as $releeph_request) { 197 + $releeph_branch = $releeph_request->loadReleephBranch(); 198 + $branch_name = $releeph_branch->getName(); 199 + $rq_id = 'RQ'.$releeph_request->getID(); 200 + 201 + if (idx($rqs_seen, $rq_id)) { 202 + throw new DifferentialFieldParseException( 203 + "Commit message refers to {$rq_id} multiple times!"); 204 + } 205 + $rqs_seen[$rq_id] = true; 206 + 207 + if (!isset($groups[$branch_name])) { 208 + $groups[$branch_name] = array(); 209 + } 210 + $groups[$branch_name][] = $rq_id; 211 + } 212 + 213 + if (count($groups) > 1) { 214 + $lists = array(); 215 + foreach ($groups as $branch_name => $rq_ids) { 216 + $lists[] = implode(', ', $rq_ids).' in '.$branch_name; 217 + } 218 + throw new DifferentialFieldParseException( 219 + "Commit message references multiple Releeph requests, ". 220 + "but the requests are in different branches: ". 221 + implode('; ', $lists)); 222 + } 223 + } 224 + 225 + $phids = mpull($releeph_requests, 'getPHID'); 226 + 227 + $data = array( 228 + 'releephAction' => $action, 229 + 'releephPHIDs' => $phids, 230 + ); 231 + return $data; 232 + } 233 + 234 + public function renderLabelForCommitMessage() { 235 + return 'Releeph'; 236 + } 237 + 238 + public function shouldAppearOnCommitMessageTemplate() { 239 + return false; 240 + } 241 + 242 + public function didParseCommit(PhabricatorRepository $repo, 243 + PhabricatorRepositoryCommit $commit, 244 + PhabricatorRepositoryCommitData $data) { 245 + 246 + $releeph_requests = $this->loadReleephRequests(); 247 + 248 + if (!$releeph_requests) { 249 + return; 250 + } 251 + 252 + $releeph_branch = head($releeph_requests)->loadReleephBranch(); 253 + if (!$this->isCommitOnBranch($repo, $commit, $releeph_branch)) { 254 + return; 255 + } 256 + 257 + foreach ($releeph_requests as $releeph_request) { 258 + if ($this->releephAction === self::ACTION_PICKS) { 259 + $action = 'pick'; 260 + } else { 261 + $action = 'revert'; 262 + } 263 + 264 + $actor_phid = coalesce( 265 + $data->getCommitDetail('committerPHID'), 266 + $data->getCommitDetail('authorPHID')); 267 + 268 + $actor = id(new PhabricatorUser()) 269 + ->loadOneWhere('phid = %s', $actor_phid); 270 + 271 + id(new ReleephRequestEditor($releeph_request)) 272 + ->setActor($actor) 273 + ->discoverCommit($action, $commit, $data); 274 + } 275 + } 276 + 277 + private function loadReleephRequests() { 278 + if (!$this->releephPHIDs) { 279 + return array(); 280 + } else { 281 + return id(new ReleephRequest()) 282 + ->loadAllWhere('phid IN (%Ls)', $this->releephPHIDs); 283 + } 284 + } 285 + 286 + private function isCommitOnBranch(PhabricatorRepository $repo, 287 + PhabricatorRepositoryCommit $commit, 288 + ReleephBranch $releeph_branch) { 289 + 290 + switch ($repo->getVersionControlSystem()) { 291 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 292 + list($output) = $repo->execxLocalCommand( 293 + 'branch --all --no-color --contains %s', 294 + $commit->getCommitIdentifier()); 295 + 296 + $remote_prefix = 'remotes/origin/'; 297 + $branches = array(); 298 + foreach (array_filter(explode("\n", $output)) as $line) { 299 + $tokens = explode(' ', $line); 300 + $ref = last($tokens); 301 + if (strncmp($ref, $remote_prefix, strlen($remote_prefix)) === 0) { 302 + $branch = substr($ref, strlen($remote_prefix)); 303 + $branches[$branch] = $branch; 304 + } 305 + } 306 + 307 + return idx($branches, $releeph_branch->getName()); 308 + break; 309 + 310 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 311 + $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( 312 + DiffusionRequest::newFromDictionary(array( 313 + 'repository' => $repo, 314 + 'commit' => $commit->getCommitIdentifier(), 315 + ))); 316 + $path_changes = $change_query->loadChanges(); 317 + $commit_paths = mpull($path_changes, 'getPath'); 318 + 319 + $branch_path = $releeph_branch->getName(); 320 + 321 + $in_branch = array(); 322 + $ex_branch = array(); 323 + foreach ($commit_paths as $path) { 324 + if (strncmp($path, $branch_path, strlen($branch_path)) === 0) { 325 + $in_branch[] = $path; 326 + } else { 327 + $ex_branch[] = $path; 328 + } 329 + } 330 + 331 + if ($in_branch && $ex_branch) { 332 + $error = sprintf( 333 + "CONFUSION: commit %s in %s contains %d path change(s) that were ". 334 + "part of a Releeph branch, but also has %d path change(s) not ". 335 + "part of a Releeph branch!", 336 + $commit->getCommitIdentifier(), 337 + $repo->getCallsign(), 338 + count($in_branch), 339 + count($ex_branch)); 340 + phlog($error); 341 + } 342 + 343 + return !empty($in_branch); 344 + break; 345 + } 346 + } 347 + 348 + }
+39
src/applications/releeph/differential/ReleephDifferentialRevisionDetailRenderer.php
··· 1 + <?php 2 + 3 + final class ReleephDifferentialRevisionDetailRenderer { 4 + 5 + public static function generateActionLink(DifferentialRevision $revision, 6 + DifferentialDiff $diff) { 7 + 8 + $arc_project = $diff->loadArcanistProject(); // 93us 9 + if (!$arc_project) { 10 + return; 11 + } 12 + 13 + $releeph_projects = id(new ReleephProject())->loadAllWhere( 14 + 'arcanistProjectID = %d AND isActive = 1', 15 + $arc_project->getID()); 16 + 17 + if (!$releeph_projects) { 18 + return; 19 + } 20 + 21 + $releeph_branches = id(new ReleephBranch())->loadAllWhere( 22 + 'releephProjectID IN (%Ld) AND isActive = 1', 23 + mpull($releeph_projects, 'getID')); 24 + 25 + if (!$releeph_branches) { 26 + return; 27 + } 28 + 29 + $uri = new PhutilURI( 30 + '/releeph/request/differentialcreate/D'.$revision->getID()); 31 + return array( 32 + 'name' => 'Releeph Request', 33 + 'sigil' => 'workflow', 34 + 'href' => $uri, 35 + 'icon' => 'fork', 36 + ); 37 + } 38 + 39 + }
+106
src/applications/releeph/editor/ReleephBranchEditor.php
··· 1 + <?php 2 + 3 + final class ReleephBranchEditor extends PhabricatorEditor { 4 + 5 + private $releephProject; 6 + private $releephBranch; 7 + 8 + public function setReleephProject(ReleephProject $rp) { 9 + $this->releephProject = $rp; 10 + return $this; 11 + } 12 + 13 + public function setReleephBranch(ReleephBranch $branch) { 14 + $this->releephBranch = $branch; 15 + return $this; 16 + } 17 + 18 + public function newBranchFromCommit(PhabricatorRepositoryCommit $cut_point, 19 + $branch_date, 20 + $symbolic_name = null) { 21 + 22 + $template = $this->releephProject->getDetail('branchTemplate'); 23 + if (!$template) { 24 + $template = ReleephBranchTemplate::getRequiredDefaultTemplate(); 25 + } 26 + 27 + $cut_point_handle = head( 28 + id(new PhabricatorObjectHandleData(array($cut_point->getPHID()))) 29 + // We'll assume that whoever found the $cut_point has passed privacy 30 + // checks. 31 + ->setViewer($this->requireActor()) 32 + ->loadHandles()); 33 + 34 + list($name, $errors) = id(new ReleephBranchTemplate()) 35 + ->setCommitHandle($cut_point_handle) 36 + ->setBranchDate($branch_date) 37 + ->setReleephProjectName($this->releephProject->getName()) 38 + ->interpolate($template); 39 + 40 + $basename = last(explode('/', $name)); 41 + 42 + $table = id(new ReleephBranch()); 43 + $transaction = $table->openTransaction(); 44 + $branch = id(new ReleephBranch()) 45 + ->setName($name) 46 + ->setBasename($basename) 47 + ->setReleephProjectID($this->releephProject->getID()) 48 + ->setCreatedByUserPHID($this->requireActor()->getPHID()) 49 + ->setCutPointCommitIdentifier($cut_point->getCommitIdentifier()) 50 + ->setCutPointCommitPHID($cut_point->getPHID()) 51 + ->setIsActive(1) 52 + ->setDetail('branchDate', $branch_date) 53 + ->save(); 54 + 55 + /** 56 + * Steal the symbolic name from any other branch that has it (in this 57 + * project). 58 + */ 59 + if ($symbolic_name) { 60 + $others = id(new ReleephBranch())->loadAllWhere( 61 + 'releephProjectID = %d', 62 + $this->releephProject->getID()); 63 + foreach ($others as $other) { 64 + if ($other->getSymbolicName() == $symbolic_name) { 65 + $other 66 + ->setSymbolicName(null) 67 + ->save(); 68 + } 69 + } 70 + $branch 71 + ->setSymbolicName($symbolic_name) 72 + ->save(); 73 + } 74 + 75 + id(new ReleephEvent()) 76 + ->setType(ReleephEvent::TYPE_BRANCH_CREATE) 77 + ->setActorPHID($this->requireActor()->getPHID()) 78 + ->setReleephProjectID($this->releephProject->getID()) 79 + ->setReleephBranchID($branch->getID()) 80 + ->save(); 81 + 82 + $table->saveTransaction(); 83 + return $branch; 84 + } 85 + 86 + // aka "close" and "reopen" 87 + public function changeBranchAccess($is_active) { 88 + $branch = $this->releephBranch; 89 + $branch->openTransaction(); 90 + 91 + $branch 92 + ->setIsActive((int)$is_active) 93 + ->save(); 94 + 95 + id(new ReleephEvent()) 96 + ->setType(ReleephEvent::TYPE_BRANCH_ACCESS) 97 + ->setActorPHID($this->requireActor()->getPHID()) 98 + ->setReleephProjectID($branch->getReleephProjectID()) 99 + ->setReleephBranchID($branch->getID()) 100 + ->setDetail('isActive', $is_active) 101 + ->save(); 102 + 103 + $branch->saveTransaction(); 104 + } 105 + 106 + }
+408
src/applications/releeph/editor/ReleephRequestEditor.php
··· 1 + <?php 2 + 3 + /** 4 + * Provide methods for the common ways of creating and mutating a 5 + * ReleephRequest, sending email when something interesting happens. 6 + * 7 + * This class generates ReleephRequestEvents, and each type of event 8 + * (ReleephRequestEvent::TYPE_*) corresponds to one of the editor methods. 9 + * 10 + * The editor methods (except for create() use newEvent() and commit() to save 11 + * some code duplication. 12 + */ 13 + final class ReleephRequestEditor extends PhabricatorEditor { 14 + 15 + private $releephRequest; 16 + private $event; 17 + private $silentUpdate; 18 + 19 + public function __construct(ReleephRequest $rq) { 20 + $this->releephRequest = $rq; 21 + } 22 + 23 + public function setSilentUpdate($silent) { 24 + $this->silentUpdate = $silent; 25 + return $this; 26 + } 27 + 28 + 29 + /* -( ReleephRequest edit methods )---------------------------------------- */ 30 + 31 + /** 32 + * Request a PhabricatorRepositoryCommit to be committed to the given 33 + * ReleephBranch. 34 + */ 35 + public function create(PhabricatorRepositoryCommit $commit, 36 + ReleephBranch $branch) { 37 + 38 + // We can't use newEvent() / commit() abstractions, so do what those 39 + // helpers do manually. 40 + $requestor = $this->requireActor(); 41 + 42 + $rq = $this->releephRequest; 43 + $rq->openTransaction(); 44 + 45 + $rq 46 + ->setBranchID($branch->getID()) 47 + ->setRequestCommitIdentifier($commit->getCommitIdentifier()) 48 + ->setRequestCommitPHID($commit->getPHID()) 49 + ->setRequestCommitOrdinal($commit->getID()) 50 + ->setInBranch(0) 51 + ->setRequestUserPHID($requestor->getPHID()) 52 + ->setUserIntent($requestor, ReleephRequest::INTENT_WANT) 53 + ->save(); 54 + 55 + $event = id(new ReleephRequestEvent()) 56 + ->setType(ReleephRequestEvent::TYPE_CREATE) 57 + ->setActorPHID($requestor->getPHID()) 58 + ->setStatusBefore(null) 59 + ->setStatusAfter($rq->getStatus()) 60 + ->setReleephRequestID($rq->getID()) 61 + ->setDetail('commitPHID', $commit->getPHID()) 62 + ->save(); 63 + 64 + $rq->saveTransaction(); 65 + 66 + // Mail 67 + if (!$this->silentUpdate) { 68 + $project = $this->releephRequest->loadReleephProject(); 69 + $mail = id(new ReleephRequestMail()) 70 + ->setReleephRequest($this->releephRequest) 71 + ->setReleephProject($project) 72 + ->setEvents(array($event)) 73 + ->setSenderAndRecipientPHID($requestor->getPHID()) 74 + ->addTos(ReleephRequestMail::ENT_ALL_PUSHERS) 75 + ->addCCs(ReleephRequestMail::ENT_REQUESTOR) 76 + ->send(); 77 + } 78 + } 79 + 80 + /** 81 + * Record whether the PhabricatorUser wants or passes on this request. 82 + */ 83 + public function changeUserIntent(PhabricatorUser $user, $intent) { 84 + $project = $this->releephRequest->loadReleephProject(); 85 + $is_pusher = $project->isPusher($user); 86 + 87 + $event = $this->newEvent() 88 + ->setType(ReleephRequestEvent::TYPE_USER_INTENT) 89 + ->setDetail('userPHID', $user->getPHID()) 90 + ->setDetail('wasPusher', $is_pusher) 91 + ->setDetail('newIntent', $intent); 92 + 93 + $this->releephRequest 94 + ->setUserIntent($user, $intent); 95 + 96 + $this->commit(); 97 + 98 + // Mail if this is 'interesting' 99 + if (!$this->silentUpdate && 100 + $event->getStatusBefore() != $event->getStatusAfter()) { 101 + 102 + $project = $this->releephRequest->loadReleephProject(); 103 + $mail = id(new ReleephRequestMail()) 104 + ->setReleephRequest($this->releephRequest) 105 + ->setReleephProject($project) 106 + ->setEvents(array($event)) 107 + ->setSenderAndRecipientPHID($this->requireActor()->getPHID()) 108 + ->addTos(ReleephRequestMail::ENT_REQUESTOR) 109 + ->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS) 110 + ->send(); 111 + } 112 + } 113 + 114 + /** 115 + * Record the results of someone trying to pick or revert a request in their 116 + * local repository, to give advance warning that something doesn't pick or 117 + * revert cleanly. 118 + */ 119 + public function changePickStatus($pick_status, $dry_run, $details) { 120 + $event = $this->newEvent() 121 + ->setType(ReleephRequestEvent::TYPE_PICK_STATUS) 122 + ->setDetail('newPickStatus', $pick_status) 123 + ->setDetail('commitDetails', $details); 124 + $this->releephRequest->setPickStatus($pick_status); 125 + $this->commit(); 126 + 127 + // Failures should generate an email 128 + if (!$this->silentUpdate && 129 + !$dry_run && 130 + ($pick_status == ReleephRequest::PICK_FAILED || 131 + $pick_status == ReleephRequest::REVERT_FAILED)) { 132 + 133 + $project = $this->releephRequest->loadReleephProject(); 134 + $mail = id(new ReleephRequestMail()) 135 + ->setReleephRequest($this->releephRequest) 136 + ->setReleephProject($project) 137 + ->setEvents(array($event)) 138 + ->setSenderAndRecipientPHID($this->requireActor()->getPHID()) 139 + ->addTos(ReleephRequestMail::ENT_REQUESTOR) 140 + ->addCCs(ReleephRequestMail::ENT_ACTORS) 141 + ->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS) 142 + ->send(); 143 + } 144 + } 145 + 146 + /** 147 + * Record that a request was committed locally, and is about to be pushed to 148 + * the remote repository. 149 + * 150 + * This lets us mark a ReleephRequest as being in a branch in real time so 151 + * that no one else tries to pick it. 152 + * 153 + * When the daemons discover this commit in the repository with 154 + * DifferentialReleephRequestFieldSpecification, we'll be able to recrod the 155 + * commit's PHID as well. That process is slow though, and 156 + * we don't want to wait a whole minute before marking something as cleanly 157 + * picked or reverted. 158 + */ 159 + public function recordSuccessfulCommit($action, $new_commit_id) { 160 + $table = $this->releephRequest; 161 + $table->openTransaction(); 162 + 163 + $actor = $this->requireActor(); 164 + 165 + $event = id(new ReleephRequestEvent()) 166 + ->setReleephRequestID($this->releephRequest->getID()) 167 + ->setActorPHID($actor->getPHID()) 168 + ->setType(ReleephRequestEvent::TYPE_COMMIT) 169 + ->setDetail('action', $action) 170 + ->setDetail('newCommitIdentifier', $new_commit_id) 171 + ->save(); 172 + 173 + switch ($action) { 174 + case 'pick': 175 + $this->releephRequest 176 + ->setInBranch(1) 177 + ->setPickStatus(ReleephRequest::PICK_OK) 178 + ->setCommitIdentifier($new_commit_id) 179 + ->setCommitPHID(null) 180 + ->setCommittedByUserPHID($actor->getPHID()) 181 + ->save(); 182 + break; 183 + 184 + case 'revert': 185 + $this->releephRequest 186 + ->setInBranch(0) 187 + ->setPickStatus(ReleephRequest::REVERT_OK) 188 + ->setCommitIdentifier(null) 189 + ->setCommitPHID(null) 190 + ->setCommittedByUserPHID(null) 191 + ->save(); 192 + break; 193 + 194 + default: 195 + $table->killTransaction(); 196 + throw new Exception("Unknown action {$action}!"); 197 + break; 198 + } 199 + 200 + $table->saveTransaction(); 201 + 202 + // Don't spam people about local commits -- we'll do that with 203 + // discoverCommit() instead! 204 + } 205 + 206 + /** 207 + * Mark this request as picked or reverted based on discovering it in the 208 + * branch. We have a PhabricatorRepositoryCommit, so we're able to 209 + * setCommitPHID on the ReleephRequest (unlike recordSuccessfulCommit()). 210 + */ 211 + public function discoverCommit( 212 + $action, 213 + PhabricatorRepositoryCommit $commit, 214 + PhabricatorRepositoryCommitData $data) { 215 + 216 + $table = $this->releephRequest; 217 + $table->openTransaction(); 218 + $table->beginWriteLocking(); 219 + 220 + $past_events = id(new ReleephRequestEvent())->loadAllWhere( 221 + 'releephRequestID = %d AND type = %s', 222 + $this->releephRequest->getID(), 223 + ReleephRequestEvent::TYPE_DISCOVERY); 224 + 225 + foreach ($past_events as $past_event) { 226 + if ($past_event->getDetail('newCommitIdentifier') 227 + == $commit->getCommitIdentifier()) { 228 + 229 + // Avoid re-discovery if reparsing! 230 + $table->endWriteLocking(); 231 + $table->killTransaction(); 232 + return; 233 + } 234 + } 235 + 236 + $actor = $this->requireActor(); 237 + 238 + $event = id(new ReleephRequestEvent()) 239 + ->setReleephRequestID($this->releephRequest->getID()) 240 + ->setActorPHID($actor->getPHID()) 241 + ->setType(ReleephRequestEvent::TYPE_DISCOVERY) 242 + ->setDateCreated($commit->getEpoch()) 243 + ->setDetail('action', $action) 244 + ->setDetail('newCommitIdentifier', $commit->getCommitIdentifier()) 245 + ->setDetail('newCommitPHID', $commit->getPHID()) 246 + ->setDetail('authorPHID', $data->getCommitDetail('authorPHID')) 247 + ->setDetail('committerPHID', $data->getCommitDetail('committerPHID')) 248 + ->save(); 249 + 250 + switch ($action) { 251 + case 'pick': 252 + $this->releephRequest 253 + ->setInBranch(1) 254 + ->setPickStatus(ReleephRequest::PICK_OK) 255 + ->setCommitIdentifier($commit->getCommitIdentifier()) 256 + ->setCommitPHID($commit->getPHID()) 257 + ->setCommittedByUserPHID($actor->getPHID()) 258 + ->save(); 259 + break; 260 + 261 + case 'revert': 262 + $this->releephRequest 263 + ->setInBranch(0) 264 + ->setPickStatus(ReleephRequest::REVERT_OK) 265 + ->setCommitIdentifier(null) 266 + ->setCommitPHID(null) 267 + ->setCommittedByUserPHID(null) 268 + ->save(); 269 + break; 270 + 271 + default: 272 + $table->killTransaction(); 273 + throw new Exception("Unknown action {$action}!"); 274 + break; 275 + } 276 + 277 + $table->endWriteLocking(); 278 + $table->saveTransaction(); 279 + 280 + // Mail 281 + if (!$this->silentUpdate) { 282 + $project = $this->releephRequest->loadReleephProject(); 283 + $mail = id(new ReleephRequestMail()) 284 + ->setReleephRequest($this->releephRequest) 285 + ->setReleephProject($project) 286 + ->setEvents(array($event)) 287 + ->setSenderAndRecipientPHID($this->requireActor()->getPHID()) 288 + ->addTos(ReleephRequestMail::ENT_REQUESTOR) 289 + ->addCCs(ReleephRequestMail::ENT_ACTORS) 290 + ->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS) 291 + ->send(); 292 + } 293 + } 294 + 295 + public function addComment($comment) { 296 + $event = $this->newEvent() 297 + ->setType(ReleephRequestEvent::TYPE_COMMENT) 298 + ->setDetail('comment', $comment); 299 + $this->commit(); 300 + 301 + // Mail 302 + if (!$this->silentUpdate) { 303 + $project = $this->releephRequest->loadReleephProject(); 304 + $mail = id(new ReleephRequestMail()) 305 + ->setReleephRequest($this->releephRequest) 306 + ->setReleephProject($project) 307 + ->setEvents(array($event)) 308 + ->setSenderAndRecipientPHID($this->requireActor()->getPHID()) 309 + ->addTos(ReleephRequestMail::ENT_REQUESTOR) 310 + ->addCCs(ReleephRequestMail::ENT_ACTORS) 311 + ->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS) 312 + ->send(); 313 + } 314 + } 315 + 316 + public function markManuallyActioned($action) { 317 + $event = $this->newEvent() 318 + ->setType(ReleephRequestEvent::TYPE_MANUAL_ACTION) 319 + ->setDetail('action', $action); 320 + 321 + $actor = $this->requireActor(); 322 + $project = $this->releephRequest->loadReleephProject(); 323 + $requestor_phid = $this->releephRequest->getRequestUserPHID(); 324 + if (!$project->isPusher($actor) && 325 + $actor->getPHID() !== $requestor_phid) { 326 + 327 + throw new Exception( 328 + "Only pushers or requestors can mark requests as ". 329 + "manually picked or reverted!"); 330 + } 331 + 332 + switch ($action) { 333 + case 'pick': 334 + $in_branch = true; 335 + $intent = ReleephRequest::INTENT_WANT; 336 + break; 337 + 338 + case 'revert': 339 + $in_branch = false; 340 + $intent = ReleephRequest::INTENT_PASS; 341 + break; 342 + 343 + default: 344 + throw new Exception("Unknown action {$action}!"); 345 + break; 346 + } 347 + 348 + $this->releephRequest 349 + ->setInBranch((int)$in_branch) 350 + ->setUserIntent($this->getActor(), $intent); 351 + 352 + $this->commit(); 353 + 354 + // Mail 355 + if (!$this->silentUpdate) { 356 + $project = $this->releephRequest->loadReleephProject(); 357 + $mail = id(new ReleephRequestMail()) 358 + ->setReleephRequest($this->releephRequest) 359 + ->setReleephProject($project) 360 + ->setEvents(array($event)) 361 + ->setSenderAndRecipientPHID($this->requireActor()->getPHID()) 362 + ->addTos(ReleephRequestMail::ENT_REQUESTOR) 363 + ->addCCs(ReleephRequestMail::ENT_INTERESTED_PUSHERS) 364 + ->send(); 365 + } 366 + } 367 + 368 + /* -( Implementation )----------------------------------------------------- */ 369 + 370 + /** 371 + * Create and return a new ReleephRequestEvent bound to the editor's 372 + * ReleephRequest, inside a transaction. 373 + * 374 + * When you call commit(), the event and this editor's ReleephRequest (along 375 + * with any changes you made to the ReleephRequest) are saved and the 376 + * transaction committed. 377 + */ 378 + private function newEvent() { 379 + $actor = $this->requireActor(); 380 + 381 + if ($this->event) { 382 + throw new Exception("You have already called newEvent()!"); 383 + } 384 + $rq = $this->releephRequest; 385 + $rq->openTransaction(); 386 + 387 + $this->event = id(new ReleephRequestEvent()) 388 + ->setReleephRequestID($rq->getID()) 389 + ->setActorPHID($actor->getPHID()) 390 + ->setStatusBefore($rq->getStatus()); 391 + 392 + return $this->event; 393 + } 394 + 395 + private function commit() { 396 + if (!$this->event) { 397 + throw new Exception("You must call newEvent first!"); 398 + } 399 + $rq = $this->releephRequest; 400 + $this->event 401 + ->setStatusAfter($rq->getStatus()) 402 + ->save(); 403 + $rq->save(); 404 + $rq->saveTransaction(); 405 + $this->event = null; 406 + } 407 + 408 + }
+213
src/applications/releeph/editor/mail/ReleephRequestMail.php
··· 1 + <?php 2 + 3 + /** 4 + * Build an email that renders a group of events with and appends some standard 5 + * Releeph things (a URI for this request, and this branch). 6 + * 7 + * Also includes some helper stuff for adding groups of people to the To: and 8 + * Cc: headers. 9 + */ 10 + final class ReleephRequestMail { 11 + 12 + const ENT_REQUESTOR = 'requestor'; 13 + const ENT_DIFF = 'diff'; 14 + const ENT_ALL_PUSHERS = 'pushers'; 15 + const ENT_ACTORS = 'actors'; 16 + const ENT_INTERESTED_PUSHERS = 'interested-pushers'; 17 + 18 + private $sender; 19 + private $tos = array(); 20 + private $ccs = array(); 21 + private $events; 22 + private $releephRequest; 23 + private $releephProject; 24 + 25 + public function setReleephRequest(ReleephRequest $rq) { 26 + $this->releephRequest = $rq; 27 + return $this; 28 + } 29 + 30 + public function setReleephProject(ReleephProject $rp) { 31 + $this->releephProject = $rp; 32 + return $this; 33 + } 34 + 35 + public function setEvents(array $events) { 36 + assert_instances_of($events, 'ReleephRequestEvent'); 37 + $this->events = $events; 38 + return $this; 39 + } 40 + 41 + public function setSenderAndRecipientPHID($sender_phid) { 42 + $this->sender = $sender_phid; 43 + $this->tos[] = $sender_phid; 44 + return $this; 45 + } 46 + 47 + public function addTos($entity) { 48 + $this->tos = array_merge( 49 + $this->tos, 50 + $this->getEntityPHIDs($entity)); 51 + return $this; 52 + } 53 + 54 + public function addCcs($entity) { 55 + $this->ccs = array_merge( 56 + $this->tos, 57 + $this->getEntityPHIDs($entity)); 58 + return $this; 59 + } 60 + 61 + public function send() { 62 + $this->buildMail()->save(); 63 + } 64 + 65 + public function buildMail() { 66 + return id(new PhabricatorMetaMTAMail()) 67 + ->setSubject($this->renderSubject()) 68 + ->setBody($this->buildBody()->render()) 69 + ->setFrom($this->sender) 70 + ->addTos($this->tos) 71 + ->addCCs($this->ccs); 72 + } 73 + 74 + private function getEntityPHIDs($entity) { 75 + $phids = array(); 76 + switch ($entity) { 77 + // The requestor 78 + case self::ENT_REQUESTOR: 79 + $phids[] = $this->releephRequest->getRequestUserPHID(); 80 + break; 81 + 82 + // People on the original diff 83 + case self::ENT_DIFF: 84 + $commit = $this->releephRequest->loadPhabricatorRepositoryCommit(); 85 + $commit_data = $commit->loadCommitData(); 86 + if ($commit_data) { 87 + $phids[] = $commit_data->getCommitDetail('reviewerPHID'); 88 + $phids[] = $commit_data->getCommitDetail('authorPHID'); 89 + } 90 + break; 91 + 92 + // All pushers for this project 93 + case self::ENT_ALL_PUSHERS: 94 + $phids = array_merge( 95 + $phids, 96 + $this->releephProject->getPushers()); 97 + break; 98 + 99 + // Pushers who have explicitly wanted or passed on this request 100 + case self::ENT_INTERESTED_PUSHERS: 101 + $all_pushers = $this->releephProject->getPushers(); 102 + $intents = $this->releephRequest->getUserIntents(); 103 + foreach ($all_pushers as $pusher) { 104 + if (idx($intents, $pusher)) { 105 + $phids[] = $pusher; 106 + } 107 + } 108 + break; 109 + 110 + // Anyone who created our list of events 111 + case self::ENT_ACTORS: 112 + $phids = array_merge( 113 + $phids, 114 + mpull($this->events, 'getActorPHID')); 115 + break; 116 + 117 + default: 118 + throw new Exception( 119 + "Unknown entity type {$entity}!"); 120 + break; 121 + } 122 + 123 + return array_filter($phids); 124 + } 125 + 126 + private function buildBody() { 127 + $body = new PhabricatorMetaMTAMailBody(); 128 + $rq = $this->releephRequest; 129 + 130 + // Events and comments 131 + $phids = array( 132 + $rq->getPHID(), 133 + ); 134 + foreach ($this->events as $event) { 135 + $phids = array_merge($phids, $event->extractPHIDs()); 136 + } 137 + $handles = id(new PhabricatorObjectHandleData($phids)) 138 + // By the time we're generating email, we can assume that whichever 139 + // entitties are receving the email are authorized to see the loaded 140 + // handles! 141 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 142 + ->loadHandles(); 143 + 144 + $raw_events = id(new ReleephRequestEventListView()) 145 + ->setUser(PhabricatorUser::getOmnipotentUser()) 146 + ->setHandles($handles) 147 + ->setEvents($this->events) 148 + ->renderForEmail(); 149 + 150 + $body->addRawSection($raw_events); 151 + 152 + $project = $rq->loadReleephProject(); 153 + $branch = $rq->loadReleephBranch(); 154 + 155 + /** 156 + * If any of the events we are emailing about were TYPE_PICK_STATUS where 157 + * the newPickStatus was a pick failure (and/or a revert failure?), include 158 + * pick failure instructions. 159 + */ 160 + $pick_failure_events = array(); 161 + foreach ($this->events as $event) { 162 + if ($event->getType() == ReleephRequestEvent::TYPE_PICK_STATUS && 163 + $event->getDetail('newPickStatus') == ReleephRequest::PICK_FAILED) { 164 + 165 + $pick_failure_events[] = $event; 166 + } 167 + } 168 + 169 + if ($pick_failure_events) { 170 + $instructions = $project->getDetail('pick_failure_instructions'); 171 + if ($instructions) { 172 + $body->addTextSection('PICK FAILURE INSTRUCTIONS', $instructions); 173 + } 174 + } 175 + 176 + // Common stuff at the end 177 + $body->addTextSection( 178 + 'RELEEPH REQUEST', 179 + $handles[$rq->getPHID()]->getFullName()."\n". 180 + PhabricatorEnv::getProductionURI('/RQ'.$rq->getID())); 181 + 182 + $project_and_branch = sprintf( 183 + '%s - %s', 184 + $project->getName(), 185 + $branch->getDisplayNameWithDetail()); 186 + 187 + $body->addTextSection( 188 + 'RELEEPH BRANCH', 189 + $project_and_branch."\n". 190 + $branch->getURI()); 191 + 192 + // But verbose stuff at the *very* end! 193 + foreach ($pick_failure_events as $event) { 194 + $failure_details = $event->getDetail('commitDetails'); 195 + if ($failure_details) { 196 + $body->addRawSection('PICK FAILURE DETAILS'); 197 + foreach ($failure_details as $heading => $data) { 198 + $body->addTextSection($heading, $data); 199 + } 200 + } 201 + } 202 + 203 + return $body; 204 + } 205 + 206 + private function renderSubject() { 207 + $rq = $this->releephRequest; 208 + $id = $rq->getID(); 209 + $summary = $rq->getSummaryForDisplay(); 210 + return "RQ{$id}: {$summary}"; 211 + } 212 + 213 + }
+12
src/applications/releeph/field/exception/ReleephFieldParseException.php
··· 1 + <?php 2 + 3 + final class ReleephFieldParseException extends Exception { 4 + 5 + public function __construct(ReleephFieldSpecification $field, 6 + $message) { 7 + 8 + $name = $field->getName(); 9 + parent::__construct("{$name}: {$message}"); 10 + } 11 + 12 + }
+11
src/applications/releeph/field/exception/ReleephFieldSpecificationIncompleteException.php
··· 1 + <?php 2 + 3 + final class ReleephFieldSpecificationIncompleteException extends Exception { 4 + 5 + public function __construct(ReleephFieldSpecification $field) { 6 + $class = get_class($field); 7 + parent::__construct( 8 + "Releeph field class {$class} is incompletely implemented."); 9 + } 10 + 11 + }
+73
src/applications/releeph/field/selector/ReleephDefaultFieldSelector.php
··· 1 + <?php 2 + 3 + final class ReleephDefaultFieldSelector extends ReleephFieldSelector { 4 + 5 + public function getFieldSpecifications() { 6 + return array( 7 + new ReleephCommitMessageFieldSpecification(), 8 + new ReleephSummaryFieldSpecification(), 9 + new ReleephReasonFieldSpecification(), 10 + new ReleephAuthorFieldSpecification(), 11 + new ReleephRevisionFieldSpecification(), 12 + new ReleephRequestorFieldSpecification(), 13 + new ReleephSeverityFieldSpecification(), 14 + new ReleephOriginalCommitFieldSpecification(), 15 + new ReleephDiffMessageFieldSpecification(), 16 + new ReleephStatusFieldSpecification(), 17 + new ReleephIntentFieldSpecification(), 18 + new ReleephBranchCommitFieldSpecification(), 19 + new ReleephDiffSizeFieldSpecification(), 20 + new ReleephDiffChurnFieldSpecification(), 21 + ); 22 + } 23 + 24 + public function arrangeFieldsForHeaderView(array $fields) { 25 + return array( 26 + // Top group 27 + array( 28 + 'left' => self::selectFields($fields, array( 29 + 'ReleephAuthorFieldSpecification', 30 + 'ReleephRevisionFieldSpecification', 31 + 'ReleephOriginalCommitFieldSpecification', 32 + 'ReleephDiffSizeFieldSpecification', 33 + 'ReleephDiffChurnFieldSpecification', 34 + )), 35 + 'right' => self::selectFields($fields, array( 36 + 'ReleephRequestorFieldSpecification', 37 + 'ReleephSeverityFieldSpecification', 38 + 'ReleephStatusFieldSpecification', 39 + 'ReleephIntentFieldSpecification', 40 + 'ReleephBranchCommitFieldSpecification', 41 + )) 42 + ), 43 + 44 + // Bottom group 45 + array( 46 + 'left' => self::selectFields($fields, array( 47 + 'ReleephDiffMessageFieldSpecification', 48 + )), 49 + 'right' => self::selectFields($fields, array( 50 + 'ReleephReasonFieldSpecification', 51 + )) 52 + ) 53 + ); 54 + } 55 + 56 + public function arrangeFieldsForSelectForm(array $fields) { 57 + self::selectFields($fields, array( 58 + 'ReleephStatusFieldSpecification', 59 + 'ReleephSeverityFieldSpecification', 60 + 'ReleephRequestorFieldSpecification', 61 + )); 62 + } 63 + 64 + public function sortFieldsForCommitMessage(array $fields) { 65 + self::selectFields($fields, array( 66 + 'ReleephCommitMessageFieldSpecification', 67 + 'ReleephRequestorFieldSpecification', 68 + 'ReleephIntentFieldSpecification', 69 + 'ReleephReasonFieldSpecification', 70 + )); 71 + } 72 + 73 + }
+53
src/applications/releeph/field/selector/ReleephFieldSelector.php
··· 1 + <?php 2 + 3 + /** 4 + * Control the rendering of ReleephRequestHeaderView, and the layout of the 5 + * ReleephRequest search dialog (in ReleephBranchViewController.) 6 + */ 7 + abstract class ReleephFieldSelector { 8 + 9 + final public function __construct() { 10 + // <empty> 11 + } 12 + 13 + abstract public function getFieldSpecifications(); 14 + 15 + abstract public function arrangeFieldsForHeaderView(array $fields); 16 + 17 + abstract public function arrangeFieldsForSelectForm(array $fields); 18 + 19 + public function sortFieldsForCommitMessage(array $fields) { 20 + assert_instances_of($fields, 'ReleephFieldSpecification'); 21 + return $fields; 22 + } 23 + 24 + protected static function selectFields(array $fields, array $classes) { 25 + assert_instances_of($fields, 'ReleephFieldSpecification'); 26 + 27 + $map = array(); 28 + foreach ($fields as $field) { 29 + $map[get_class($field)] = $field; 30 + } 31 + 32 + $result = array(); 33 + foreach ($classes as $class) { 34 + $field = idx($map, $class); 35 + if (!$field) { 36 + throw new Exception( 37 + "Tried to select a in instance of '{$class}' but that field ". 38 + "is not configured for this project!"); 39 + } 40 + 41 + if (idx($result, $class)) { 42 + throw new Exception( 43 + "You have asked to select the field '{$class}' ". 44 + "more than once!"); 45 + } 46 + 47 + $result[$class] = $field; 48 + } 49 + 50 + return $result; 51 + } 52 + 53 + }
+39
src/applications/releeph/field/specification/ReleephAuthorFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephAuthorFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + private static $authorMap = array(); 7 + 8 + public function bulkLoad(array $releeph_requests) { 9 + foreach ($releeph_requests as $releeph_request) { 10 + $commit = $releeph_request->loadPhabricatorRepositoryCommit(); 11 + if ($commit) { 12 + $author_phid = $commit->getAuthorPHID(); 13 + self::$authorMap[$releeph_request->getPHID()] = $author_phid; 14 + } 15 + } 16 + 17 + ReleephUserView::getNewInstance() 18 + ->setUser($this->getUser()) 19 + ->setReleephProject($this->getReleephProject()) 20 + ->load(self::$authorMap); 21 + } 22 + 23 + public function getName() { 24 + return 'Author'; 25 + } 26 + 27 + public function renderValueForHeaderView() { 28 + $rr = $this->getReleephRequest(); 29 + $author_phid = idx(self::$authorMap, $rr->getPHID()); 30 + if ($author_phid) { 31 + return ReleephUserView::getNewInstance() 32 + ->setRenderUserPHID($author_phid) 33 + ->render(); 34 + } else { 35 + return 'Unknown Author'; 36 + } 37 + } 38 + 39 + }
+30
src/applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephBranchCommitFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Commit'; 8 + } 9 + 10 + public function renderValueForHeaderView() { 11 + $rr = $this->getReleephRequest(); 12 + if (!$rr->getInBranch()) { 13 + return null; 14 + } 15 + 16 + $c_phid = $rr->getCommitPHID(); 17 + $c_id = $rr->getCommitIdentifier(); 18 + 19 + if ($c_phid) { 20 + $handles = $rr->getHandles(); 21 + $val = $handles[$c_phid]->renderLink(); 22 + } else if ($c_id) { 23 + $val = $c_id; 24 + } else { 25 + $val = '???'; 26 + } 27 + return $val; 28 + } 29 + 30 + }
+46
src/applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephCommitMessageFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return '__only_for_commit_message!'; 8 + } 9 + 10 + public function shouldAppearOnCommitMessage() { 11 + return true; 12 + } 13 + 14 + public function renderLabelForCommitMessage() { 15 + return $this->renderCommonLabel(); 16 + } 17 + 18 + public function renderValueForCommitMessage() { 19 + return $this->renderCommonValue( 20 + DifferentialReleephRequestFieldSpecification::ACTION_PICKS); 21 + } 22 + 23 + public function shouldAppearOnRevertMessage() { 24 + return true; 25 + } 26 + 27 + public function renderLabelForRevertMessage() { 28 + return $this->renderCommonLabel(); 29 + } 30 + 31 + public function renderValueForRevertMessage() { 32 + return $this->renderCommonValue( 33 + DifferentialReleephRequestFieldSpecification::ACTION_REVERTS); 34 + } 35 + 36 + private function renderCommonLabel() { 37 + return id(new DifferentialReleephRequestFieldSpecification()) 38 + ->renderLabelForCommitMessage(); 39 + } 40 + 41 + private function renderCommonValue($action) { 42 + $rq = 'RQ'.$this->getReleephRequest()->getID(); 43 + return "{$action} {$rq}"; 44 + } 45 + 46 + }
+77
src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephDiffChurnFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + const REJECTIONS_WEIGHT = 30; 7 + const COMMENTS_WEIGHT = 7; 8 + const UPDATES_WEIGHT = 10; 9 + const MAX_POINTS = 100; 10 + 11 + public function getName() { 12 + return 'Churn'; 13 + } 14 + 15 + public function renderValueForHeaderView() { 16 + $diff_rev = $this->getReleephRequest()->loadDifferentialRevision(); 17 + if (!$diff_rev) { 18 + return null; 19 + } 20 + 21 + $diff_rev = $this->getReleephRequest()->loadDifferentialRevision(); 22 + $comments = $diff_rev->loadRelatives( 23 + new DifferentialComment(), 24 + 'revisionID'); 25 + 26 + $counts = array(); 27 + foreach ($comments as $comment) { 28 + $action = $comment->getAction(); 29 + if (!isset($counts[$action])) { 30 + $counts[$action] = 0; 31 + } 32 + $counts[$action] += 1; 33 + } 34 + 35 + // 'none' action just means a plain comment 36 + $comments = idx($counts, 'none', 0); 37 + $rejections = idx($counts, 'reject', 0); 38 + $updates = idx($counts, 'update', 0); 39 + 40 + $points = 41 + self::REJECTIONS_WEIGHT * $rejections + 42 + self::COMMENTS_WEIGHT * $comments + 43 + self::UPDATES_WEIGHT * $updates; 44 + 45 + if ($points === 0) { 46 + $points = 0.15 * self::MAX_POINTS; 47 + $blurb = 'Silent diff'; 48 + } else { 49 + $parts = array(); 50 + if ($rejections) { 51 + $parts[] = pht('%d rejection(s)', $rejections); 52 + } 53 + if ($comments) { 54 + $parts[] = pht('%d comment(s)', $comments); 55 + } 56 + if ($updates) { 57 + $parts[] = pht('%d update(s)', $updates); 58 + } 59 + 60 + if (count($parts) === 0) { 61 + $blurb = ''; 62 + } else if (count($parts) === 1) { 63 + $blurb = head($parts); 64 + } else { 65 + $last = array_pop($parts); 66 + $blurb = implode(', ', $parts).' and '.$last; 67 + } 68 + } 69 + 70 + return id(new AphrontProgressBarView()) 71 + ->setValue($points) 72 + ->setMax(self::MAX_POINTS) 73 + ->setCaption($blurb) 74 + ->render(); 75 + } 76 + 77 + }
+37
src/applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephDiffMessageFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Message'; 8 + } 9 + 10 + public function renderLabelForHeaderView() { 11 + return null; 12 + } 13 + 14 + public function renderValueForHeaderView() { 15 + $commit_data = $this 16 + ->getReleephRequest() 17 + ->loadPhabricatorRepositoryCommitData(); 18 + if (!$commit_data) { 19 + return ''; 20 + } 21 + 22 + $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); 23 + $engine->setConfig('viewer', $this->getUser()); 24 + $markup = phutil_tag( 25 + 'div', 26 + array( 27 + 'class' => 'phabricator-remarkup', 28 + ), 29 + $engine->markupText($commit_data->getCommitMessage())); 30 + 31 + return id(new AphrontNoteView()) 32 + ->setTitle('Commit Message') 33 + ->appendChild($markup) 34 + ->render(); 35 + } 36 + 37 + }
+112
src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php
··· 1 + <?php 2 + 3 + /** 4 + * While this class could take advantage of bulkLoad(), in practice 5 + * loadRelatives fixes all that for us. 6 + */ 7 + final class ReleephDiffSizeFieldSpecification 8 + extends ReleephFieldSpecification { 9 + 10 + const LINES_WEIGHT = 1; 11 + const PATHS_WEIGHT = 30; 12 + const MAX_POINTS = 1000; 13 + 14 + public function getName() { 15 + return 'Size'; 16 + } 17 + 18 + public function renderValueForHeaderView() { 19 + $diff_rev = $this->getReleephRequest()->loadDifferentialRevision(); 20 + if (!$diff_rev) { 21 + return ''; 22 + } 23 + 24 + $diffs = $diff_rev->loadRelatives( 25 + new DifferentialDiff(), 26 + 'revisionID', 27 + 'getID', 28 + 'creationMethod <> "commit"'); 29 + 30 + $all_changesets = array(); 31 + $most_recent_changesets = null; 32 + foreach ($diffs as $diff) { 33 + $changesets = $diff->loadRelatives(new DifferentialChangeset(), 'diffID'); 34 + $all_changesets += $changesets; 35 + $most_recent_changesets = $changesets; 36 + } 37 + 38 + // The score is based on all changesets for all versions of this diff 39 + $all_changes = $this->countLinesAndPaths($all_changesets); 40 + $points = 41 + self::LINES_WEIGHT * $all_changes['code']['lines'] + 42 + self::PATHS_WEIGHT * count($all_changes['code']['paths']); 43 + 44 + // The blurb is just based on the most recent version of the diff 45 + $mr_changes = $this->countLinesAndPaths($most_recent_changesets); 46 + 47 + $test_tag = ''; 48 + if ($mr_changes['tests']['paths']) { 49 + Javelin::initBehavior('phabricator-tooltips'); 50 + require_celerity_resource('aphront-tooltip-css'); 51 + 52 + $test_blurb = 53 + pht('%d line(s)', $mr_changes['tests']['lines']).' and '. 54 + pht('%d path(s)', count($mr_changes['tests']['paths'])). 55 + " contain changes to test code:\n"; 56 + foreach ($mr_changes['tests']['paths'] as $mr_test_path) { 57 + $test_blurb .= pht("%s\n", $mr_test_path); 58 + } 59 + 60 + $test_tag = javelin_tag( 61 + 'span', 62 + array( 63 + 'sigil' => 'has-tooltip', 64 + 'meta' => array( 65 + 'tip' => $test_blurb, 66 + 'align' => 'E', 67 + 'size' => 'auto'), 68 + 'style' => ''), 69 + ' + tests'); 70 + } 71 + 72 + $blurb = hsprintf("%s%s.", 73 + pht('%d line(s)', $mr_changes['code']['lines']).' and '. 74 + pht('%d path(s)', count($mr_changes['code']['paths'])).' over '. 75 + pht('%d diff(s)', count($diffs)), 76 + $test_tag); 77 + 78 + return id(new AphrontProgressBarView()) 79 + ->setValue($points) 80 + ->setMax(self::MAX_POINTS) 81 + ->setCaption($blurb) 82 + ->render(); 83 + } 84 + 85 + private function countLinesAndPaths(array $changesets) { 86 + assert_instances_of($changesets, 'DifferentialChangeset'); 87 + $lines = 0; 88 + $paths_touched = array(); 89 + $test_lines = 0; 90 + $test_paths_touched = array(); 91 + 92 + foreach ($changesets as $ch) { 93 + if ($this->getReleephProject()->isTestFile($ch->getFilename())) { 94 + $test_lines += $ch->getAddLines() + $ch->getDelLines(); 95 + $test_paths_touched[] = $ch->getFilename(); 96 + } else { 97 + $lines += $ch->getAddLines() + $ch->getDelLines(); 98 + $paths_touched[] = $ch->getFilename(); 99 + } 100 + } 101 + return array( 102 + 'code' => array( 103 + 'lines' => $lines, 104 + 'paths' => array_unique($paths_touched), 105 + ), 106 + 'tests' => array( 107 + 'lines' => $test_lines, 108 + 'paths' => array_unique($test_paths_touched), 109 + ) 110 + ); 111 + } 112 + }
+359
src/applications/releeph/field/specification/ReleephFieldSpecification.php
··· 1 + <?php 2 + 3 + abstract class ReleephFieldSpecification { 4 + 5 + abstract public function getName(); 6 + 7 + /* -( Storage )------------------------------------------------------------ */ 8 + 9 + public function getStorageKey() { 10 + return null; 11 + } 12 + 13 + final public function isEditable() { 14 + return $this->getStorageKey() !== null; 15 + } 16 + 17 + /** 18 + * This will be called many times if you are using **Selecting**. In 19 + * particular, for N selecting fields, selectReleephRequests() is called 20 + * N-squared times, each time for R ReleephRequests. 21 + */ 22 + final public function getValue() { 23 + $key = $this->getRequiredStorageKey(); 24 + return $this->getReleephRequest()->getDetail($key); 25 + } 26 + 27 + final public function setValue($value) { 28 + $key = $this->getRequiredStorageKey(); 29 + return $this->getReleephRequest()->setDetail($key, $value); 30 + } 31 + 32 + /** 33 + * @throws ReleephFieldParseException, to show an error. 34 + */ 35 + public function validate($value) { 36 + return; 37 + } 38 + 39 + 40 + /* -( Header View )-------------------------------------------------------- */ 41 + 42 + /** 43 + * Return a label for use in rendering the fields table. If you return null, 44 + * the renderLabelForHeaderView data will span both columns. 45 + */ 46 + public function renderLabelForHeaderView() { 47 + return $this->getName(); 48 + } 49 + 50 + public function renderValueForHeaderView() { 51 + $key = $this->getRequiredStorageKey(); 52 + return $this->getReleephRequest()->getDetail($key); 53 + } 54 + 55 + 56 + /* -( Edit View )---------------------------------------------------------- */ 57 + 58 + public function renderEditControl(AphrontRequest $request) { 59 + throw new ReleephFieldSpecificationIncompleteException($this); 60 + } 61 + 62 + public function setValueFromAphrontRequest(AphrontRequest $request) { 63 + $data = $request->getRequestData(); 64 + $value = idx($data, $this->getRequiredStorageKey()); 65 + $this->validate($value); 66 + $this->setValue($value); 67 + } 68 + 69 + 70 + /* -( Conduit )------------------------------------------------------------ */ 71 + 72 + public function getKeyForConduit() { 73 + return $this->getRequiredStorageKey(); 74 + } 75 + 76 + public function getValueForConduit() { 77 + return $this->getValue(); 78 + } 79 + 80 + public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) { 81 + $value = idx( 82 + $request->getValue('fields', array()), 83 + $this->getRequiredStorageKey()); 84 + $this->validate($value); 85 + $this->setValue($value); 86 + } 87 + 88 + 89 + /* -( Arcanist )----------------------------------------------------------- */ 90 + 91 + public function renderHelpForArcanist() { 92 + return ''; 93 + } 94 + 95 + 96 + /* -( Context )------------------------------------------------------------ */ 97 + 98 + private $releephProject; 99 + private $releephBranch; 100 + private $releephRequest; 101 + private $user; 102 + 103 + final public function setReleephProject(ReleephProject $rp) { 104 + $this->releephProject = $rp; 105 + return $this; 106 + } 107 + 108 + final public function setReleephBranch(ReleephBranch $rb) { 109 + $this->releephRequest = $rb; 110 + return $this; 111 + } 112 + 113 + final public function setReleephRequest(ReleephRequest $rr) { 114 + $this->releephRequest = $rr; 115 + return $this; 116 + } 117 + 118 + final public function setUser(PhabricatorUser $user) { 119 + $this->user = $user; 120 + return $this; 121 + } 122 + 123 + final public function getReleephProject() { 124 + return $this->releephProject; 125 + } 126 + 127 + final public function getReleephBranch() { 128 + return $this->releephBranch; 129 + } 130 + 131 + final public function getReleephRequest() { 132 + return $this->releephRequest; 133 + } 134 + 135 + final public function getUser() { 136 + return $this->user; 137 + } 138 + 139 + 140 + /* -( Bulk loading )------------------------------------------------------- */ 141 + 142 + public function bulkLoad(array $releeph_requests) { 143 + } 144 + 145 + 146 + /* -( Selecting )---------------------------------------------------------- */ 147 + 148 + /** 149 + * Append select controls to the given form. 150 + * 151 + * You are given: 152 + * 153 + * - the AphrontFormView to append to; 154 + * 155 + * - the AphrontRequest, so you can make use of the value currently selected 156 + * in the form; 157 + * 158 + * - $all_releeph_requests: an array of all the ReleephRequests without any 159 + * selection based filtering; and 160 + * 161 + * - $all_releeph_requests_without_this_field: an array of ReleephRequests 162 + * that have been selected by all the other select controls on this page. 163 + * 164 + * The example in ReleephLevelFieldSpecification shows how to use these. 165 + * $all_releeph_requests lets you find out all the values of a field in all 166 + * ReleephRequests, so you can render controls for every known value. 167 + * 168 + * $all_releeph_requests_without_this_field lets you count how many 169 + * ReleephRequests could be affected by this field's select control, after 170 + * all the other fields have made their selections. 171 + * ReleephLevelFieldSpecification uses this to render a preview count for 172 + * each select button, and disables the button completely (but still renders 173 + * it) if it couldn't possibly select anything. 174 + */ 175 + protected function appendSelectControls( 176 + AphrontFormView $form, 177 + AphrontRequest $request, 178 + array $all_releeph_requests, 179 + array $all_releeph_requests_without_this_field) { 180 + 181 + return null; 182 + } 183 + 184 + /** 185 + * Filter the $releeph_requests using the data you set with your form 186 + * controls, and which is now available in the provided AphrontRequest. 187 + */ 188 + protected function selectReleephRequests(AphrontRequest $request, 189 + array &$releeph_requests) { 190 + return null; 191 + } 192 + 193 + /** 194 + * If you have PHIDs that can be used in an AphrontFormTokenizerControl, 195 + * return true here, return the PHIDs in getSelectablePHIDs(), and return the 196 + * URL the Tokenizer should use for the form control in 197 + * getSelectTokenizerDatasource(). 198 + * 199 + * This is a cheap alternative to implementing appendSelectControls() and 200 + * selectReleephRequests() in full. 201 + */ 202 + protected function hasSelectablePHIDs() { 203 + return false; 204 + } 205 + 206 + protected function getSelectablePHIDs() { 207 + throw new ReleephFieldSpecificationIncompleteException($this); 208 + } 209 + 210 + protected function getSelectTokenizerDatasource() { 211 + throw new ReleephFieldSpecificationIncompleteException($this); 212 + } 213 + 214 + 215 + /* -( Commit Messages )---------------------------------------------------- */ 216 + 217 + public function shouldAppearOnCommitMessage() { 218 + return false; 219 + } 220 + 221 + public function renderLabelForCommitMessage() { 222 + throw new ReleephFieldSpecificationIncompleteException($this); 223 + } 224 + 225 + public function renderValueForCommitMessage() { 226 + throw new ReleephFieldSpecificationIncompleteException($this); 227 + } 228 + 229 + public function shouldAppearOnRevertMessage() { 230 + return false; 231 + } 232 + 233 + public function renderLabelForRevertMessage() { 234 + return $this->renderLabelForCommitMessage(); 235 + } 236 + 237 + public function renderValueForRevertMessage() { 238 + return $this->renderValueForCommitMessage(); 239 + } 240 + 241 + 242 + /* -( Implementation )----------------------------------------------------- */ 243 + 244 + protected function getRequiredStorageKey() { 245 + $key = $this->getStorageKey(); 246 + if ($key === null) { 247 + throw new ReleephFieldSpecificationIncompleteException($this); 248 + } 249 + if (strpos($key, '.') !== false) { 250 + /** 251 + * Storage keys are reused for form controls, and periods in form control 252 + * names break HTML forms. 253 + */ 254 + throw new Exception( 255 + "You can't use '.' in storage keys!"); 256 + } 257 + return $key; 258 + } 259 + 260 + /** 261 + * The "hook" functions ##appendSelectControlsHook()## and 262 + * ##selectReleephRequestsHook()## are used with ##hasSelectablePHIDs()##, to 263 + * use the tokenizing helpers if ##hasSelectablePHIDs()## returns true. 264 + */ 265 + public function appendSelectControlsHook( 266 + AphrontFormView $form, 267 + AphrontRequest $request, 268 + array $all_releeph_requests, 269 + array $all_releeph_requests_without_this_field) { 270 + 271 + if ($this->hasSelectablePHIDs()) { 272 + $this->appendTokenizingSelectControl( 273 + $form, 274 + $request, 275 + $all_releeph_requests, 276 + $all_releeph_requests_without_this_field); 277 + } else { 278 + $this->appendSelectControls( 279 + $form, 280 + $request, 281 + $all_releeph_requests, 282 + $all_releeph_requests_without_this_field); 283 + } 284 + } 285 + 286 + // See above 287 + public function selectReleephRequestsHook(AphrontRequest $request, 288 + array &$releeph_requests) { 289 + 290 + if ($this->hasSelectablePHIDs()) { 291 + $this->selectReleephRequestsFromTokens( 292 + $request, 293 + $releeph_requests); 294 + } else { 295 + $this->selectReleephRequests( 296 + $request, 297 + $releeph_requests); 298 + } 299 + } 300 + 301 + private function appendTokenizingSelectControl( 302 + AphrontFormView $form, 303 + AphrontRequest $request, 304 + array $all_releeph_requests, 305 + array $all_releeph_requests_without_this_field) { 306 + 307 + $key = urlencode(strtolower($this->getName())); 308 + $selected_phids = $request->getArr($key); 309 + $handles = id(new PhabricatorObjectHandleData($selected_phids)) 310 + ->setViewer($request->getUser()) 311 + ->loadHandles(); 312 + 313 + $tokens = array(); 314 + foreach ($selected_phids as $phid) { 315 + $tokens[$phid] = $handles[$phid]->getFullName(); 316 + } 317 + 318 + $datasource = $this->getSelectTokenizerDatasource(); 319 + $control = 320 + id(new AphrontFormTokenizerControl()) 321 + ->setDatasource($datasource) 322 + ->setName($key) 323 + ->setLabel($this->getName()) 324 + ->setValue($tokens); 325 + 326 + $form->appendChild($control); 327 + } 328 + 329 + private function selectReleephRequestsFromTokens(AphrontRequest $request, 330 + array &$releeph_requests) { 331 + 332 + $key = urlencode(strtolower($this->getName())); 333 + $selected_phids = $request->getArr($key); 334 + if (!$selected_phids) { 335 + return; 336 + } 337 + 338 + $selected_phid_lookup = array(); 339 + foreach ($selected_phids as $phid) { 340 + $selected_phid_lookup[$phid] = $phid; 341 + } 342 + 343 + $filtered = array(); 344 + foreach ($releeph_requests as $releeph_request) { 345 + $rq_phids = $this 346 + ->setReleephRequest($releeph_request) 347 + ->getSelectablePHIDs(); 348 + foreach ($rq_phids as $rq_phid) { 349 + if (idx($selected_phid_lookup, $rq_phid)) { 350 + $filtered[] = $releeph_request; 351 + break; 352 + } 353 + } 354 + } 355 + 356 + $releeph_requests = $filtered; 357 + } 358 + 359 + }
+81
src/applications/releeph/field/specification/ReleephIntentFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephIntentFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Intent'; 8 + } 9 + 10 + public function renderValueForHeaderView() { 11 + return id(new ReleephRequestIntentsView()) 12 + ->setReleephRequest($this->getReleephRequest()) 13 + ->setReleephProject($this->getReleephProject()) 14 + ->render(); 15 + } 16 + 17 + public function shouldAppearOnCommitMessage() { 18 + return true; 19 + } 20 + 21 + public function shouldAppearOnRevertMessage() { 22 + return true; 23 + } 24 + 25 + public function renderLabelForCommitMessage() { 26 + return "Approved By"; 27 + } 28 + 29 + public function renderLabelForRevertMessage() { 30 + return "Rejected By"; 31 + } 32 + 33 + public function renderValueForCommitMessage() { 34 + return $this->renderIntentsForCommitMessage(ReleephRequest::INTENT_WANT); 35 + } 36 + 37 + public function renderValueForRevertMessage() { 38 + return $this->renderIntentsForCommitMessage(ReleephRequest::INTENT_PASS); 39 + } 40 + 41 + private function renderIntentsForCommitMessage($print_intent) { 42 + $intents = $this->getReleephRequest()->getUserIntents(); 43 + 44 + $requestor = $this->getReleephRequest()->getRequestUserPHID(); 45 + $pusher_phids = $this->getReleephProject()->getPushers(); 46 + 47 + $phids = array_unique($pusher_phids + array_keys($intents)); 48 + $handles = id(new PhabricatorObjectHandleData($phids)) 49 + ->setViewer($this->getUser()) 50 + ->loadHandles(); 51 + 52 + $tokens = array(); 53 + foreach ($phids as $phid) { 54 + $intent = idx($intents, $phid); 55 + if ($intent == $print_intent) { 56 + $name = $handles[$phid]->getName(); 57 + $is_pusher = in_array($phid, $pusher_phids); 58 + $is_requestor = $phid == $requestor; 59 + 60 + if ($is_pusher) { 61 + if ($is_requestor) { 62 + $token = "{$name} (pusher and requestor)"; 63 + } else { 64 + $token = "{$name} (pusher)"; 65 + } 66 + } else { 67 + if ($is_requestor) { 68 + $token = "{$name} (requestor)"; 69 + } else { 70 + $token = $name; 71 + } 72 + } 73 + 74 + $tokens[] = $token; 75 + } 76 + } 77 + 78 + return implode(', ', $tokens); 79 + } 80 + 81 + }
+228
src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php
··· 1 + <?php 2 + 3 + /** 4 + * Provides a convenient field for storing a set of levels that you can use to 5 + * filter requests on. 6 + * 7 + * Levels are rendered with names and descriptions in the edit UI, and are 8 + * automatically documented via the "arc request" interface. 9 + * 10 + * See ReleephSeverityFieldSpecification for an example. 11 + */ 12 + abstract class ReleephLevelFieldSpecification 13 + extends ReleephFieldSpecification { 14 + 15 + private $error; 16 + 17 + abstract public function getLevels(); 18 + abstract public function getDefaultLevel(); 19 + abstract public function getNameForLevel($level); 20 + abstract public function getDescriptionForLevel($level); 21 + 22 + /** 23 + * Use getCanonicalLevel() to convert old, unsupported levels to new ones. 24 + */ 25 + protected function getCanonicalLevel($misc_level) { 26 + return $misc_level; 27 + } 28 + 29 + public function getStorageKey() { 30 + $class = get_class($this); 31 + throw new ReleephFieldSpecificationIncompleteException( 32 + $this, 33 + "You must implement getStorageKey() for children of {$class}!"); 34 + } 35 + 36 + public function renderValueForHeaderView() { 37 + $raw_level = $this->getValue(); 38 + $level = $this->getCanonicalLevel($raw_level); 39 + return $this->getNameForLevel($level); 40 + } 41 + 42 + public function renderEditControl(AphrontRequest $request) { 43 + $control_name = $this->getRequiredStorageKey(); 44 + $all_levels = $this->getLevels(); 45 + 46 + $level = $request->getStr($control_name); 47 + 48 + if (!$level) { 49 + $level = $this->getCanonicalLevel($this->getValue()); 50 + } 51 + 52 + if (!$level) { 53 + $level = $this->getDefaultLevel(); 54 + } 55 + 56 + $control = id(new AphrontFormRadioButtonControl()) 57 + ->setLabel('Level') 58 + ->setName($control_name) 59 + ->setValue($level); 60 + 61 + if ($this->error) { 62 + $control->setError($this->error); 63 + } elseif ($this->getDefaultLevel()) { 64 + $control->setError(true); 65 + } 66 + 67 + foreach ($all_levels as $level) { 68 + $name = $this->getNameForLevel($level); 69 + $description = $this->getDescriptionForLevel($level); 70 + $control->addButton($level, $name, $description); 71 + } 72 + 73 + return $control; 74 + } 75 + 76 + public function renderHelpForArcanist() { 77 + $text = ''; 78 + $levels = $this->getLevels(); 79 + $default = $this->getDefaultLevel(); 80 + foreach ($levels as $level) { 81 + $name = $this->getNameForLevel($level); 82 + $description = $this->getDescriptionForLevel($level); 83 + $default_marker = ' '; 84 + if ($level === $default) { 85 + $default_marker = '*'; 86 + } 87 + $text .= " {$default_marker} **{$name}**\n"; 88 + $text .= phutil_console_wrap($description."\n", 8); 89 + } 90 + return $text; 91 + } 92 + 93 + public function validate($value) { 94 + if ($value === null) { 95 + $this->error = 'Required'; 96 + $label = $this->getName(); 97 + throw new ReleephFieldParseException( 98 + $this, 99 + "You must provide a {$label} level"); 100 + } 101 + 102 + $levels = $this->getLevels(); 103 + if (!in_array($value, $levels)) { 104 + $label = $this->getName(); 105 + throw new ReleephFieldParseException( 106 + $this, 107 + "Level '{$value}' is not a valid {$label} level in this project."); 108 + } 109 + } 110 + 111 + public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) { 112 + $key = $this->getRequiredStorageKey(); 113 + $label = $this->getName(); 114 + $name = idx($request->getValue('fields', array()), $key); 115 + 116 + if (!$name) { 117 + $level = $this->getDefaultLevel(); 118 + if (!$level) { 119 + throw new ReleephFieldParseException( 120 + $this, 121 + "No value given for {$label}, ". 122 + "and no default is given for this level!"); 123 + } 124 + } else { 125 + $level = $this->getLevelByName($name); 126 + } 127 + 128 + if (!$level) { 129 + throw new ReleephFieldParseException( 130 + $this, 131 + "Unknown {$label} level name '{$name}'"); 132 + } 133 + $this->setValue($level); 134 + } 135 + 136 + private $nameMap = array(); 137 + 138 + public function getLevelByName($name) { 139 + // Build this once 140 + if (!$this->nameMap) { 141 + foreach ($this->getLevels() as $level) { 142 + $level_name = $this->getNameForLevel($level); 143 + $this->nameMap[$level_name] = $level; 144 + } 145 + } 146 + return idx($this->nameMap, $name); 147 + } 148 + 149 + protected function appendSelectControls( 150 + AphrontFormView $form, 151 + AphrontRequest $request, 152 + array $all_releeph_requests, 153 + array $all_releeph_requests_without_this_field) { 154 + 155 + $buttons = array(null => 'All'); 156 + 157 + // Add in known level/names 158 + foreach ($this->getLevels() as $level) { 159 + $name = $this->getNameForLevel($level); 160 + $buttons[$name] = $name; 161 + } 162 + 163 + // Add in any names we've seen in the wild, as well. 164 + foreach ($all_releeph_requests as $releeph_request) { 165 + $raw_level = $this->setReleephRequest($releeph_request)->getValue(); 166 + if (!$raw_level) { 167 + // The ReleephRequest might not have a level set 168 + continue; 169 + } 170 + $level = $this->getCanonicalLevel($raw_level); 171 + $name = $this->getNameForLevel($level); 172 + $buttons[$name] = $name; 173 + } 174 + 175 + $key = $this->getRequiredStorageKey(); 176 + $current = $request->getStr($key); 177 + 178 + $counters = array(null => count($all_releeph_requests_without_this_field)); 179 + foreach ($all_releeph_requests_without_this_field as $releeph_request) { 180 + $raw_level = $this->setReleephRequest($releeph_request)->getValue(); 181 + if (!$raw_level) { 182 + // The ReleephRequest might not have a level set 183 + continue; 184 + } 185 + $level = $this->getCanonicalLevel($raw_level); 186 + $name = $this->getNameForLevel($level); 187 + 188 + if (!isset($counters[$name])) { 189 + $counters[$name] = 0; 190 + } 191 + $counters[$name]++; 192 + } 193 + 194 + $control = id(new AphrontFormCountedToggleButtonsControl()) 195 + ->setLabel($this->getName()) 196 + ->setValue($current) 197 + ->setBaseURI($request->getRequestURI(), $key) 198 + ->setButtons($buttons) 199 + ->setCounters($counters); 200 + 201 + $form 202 + ->appendChild($control) 203 + ->addHiddenInput($key, $current); 204 + } 205 + 206 + protected function selectReleephRequests(AphrontRequest $request, 207 + array &$releeph_requests) { 208 + $key = $this->getRequiredStorageKey(); 209 + $current = $request->getStr($key); 210 + 211 + if (!$current) { 212 + return; 213 + } 214 + 215 + $filtered = array(); 216 + foreach ($releeph_requests as $releeph_request) { 217 + $raw_level = $this->setReleephRequest($releeph_request)->getValue(); 218 + $level = $this->getCanonicalLevel($raw_level); 219 + $name = $this->getNameForLevel($level); 220 + if ($name == $current) { 221 + $filtered[] = $releeph_request; 222 + } 223 + } 224 + 225 + $releeph_requests = $filtered; 226 + } 227 + 228 + }
+16
src/applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephOriginalCommitFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Commit'; 8 + } 9 + 10 + public function renderValueForHeaderView() { 11 + $rr = $this->getReleephRequest(); 12 + $handles = $rr->getHandles(); 13 + return $handles[$rr->getRequestCommitPHID()]->renderLink(); 14 + } 15 + 16 + }
+78
src/applications/releeph/field/specification/ReleephReasonFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephReasonFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Reason'; 8 + } 9 + 10 + public function getStorageKey() { 11 + return 'reason'; 12 + } 13 + 14 + public function renderLabelForHeaderView() { 15 + return null; 16 + } 17 + 18 + public function renderValueForHeaderView() { 19 + $reason = $this->getValue(); 20 + if (!$reason) { 21 + return ''; 22 + } 23 + 24 + $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); 25 + $engine->setConfig('viewer', $this->getUser()); 26 + $markup = phutil_tag( 27 + 'div', 28 + array( 29 + 'class' => 'phabricator-remarkup', 30 + ), 31 + $engine->markupText($reason)); 32 + 33 + return id(new AphrontNoteView()) 34 + ->setTitle('Reason') 35 + ->appendChild($markup) 36 + ->render(); 37 + } 38 + 39 + private $error = true; 40 + 41 + public function renderEditControl(AphrontRequest $request) { 42 + $reason = $request->getStr('reason', $this->getValue()); 43 + return id(new AphrontFormTextAreaControl()) 44 + ->setLabel('Reason') 45 + ->setName('reason') 46 + ->setError($this->error) 47 + ->setValue($reason); 48 + } 49 + 50 + public function validate($reason) { 51 + if (!$reason) { 52 + $this->error = 'Required'; 53 + throw new ReleephFieldParseException( 54 + $this, 55 + "You must give a reason for your request."); 56 + } 57 + } 58 + 59 + public function renderHelpForArcanist() { 60 + $text = 61 + "Fully explain why you are requesting this code be included ". 62 + "in the next release.\n"; 63 + return phutil_console_wrap($text, 8); 64 + } 65 + 66 + public function shouldAppearOnCommitMessage() { 67 + return true; 68 + } 69 + 70 + public function renderLabelForCommitMessage() { 71 + return 'Request Reason'; 72 + } 73 + 74 + public function renderValueForCommitMessage() { 75 + return $this->getValue(); 76 + } 77 + 78 + }
+59
src/applications/releeph/field/specification/ReleephRequestorFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephRequestorFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function bulkLoad(array $releeph_requests) { 7 + $phids = mpull($releeph_requests, 'getRequestUserPHID'); 8 + ReleephUserView::getNewInstance() 9 + ->setUser($this->getUser()) 10 + ->setReleephProject($this->getReleephProject()) 11 + ->load($phids); 12 + } 13 + 14 + public function getName() { 15 + return 'Requestor'; 16 + } 17 + 18 + public function renderValueForHeaderView() { 19 + $phid = $this->getReleephRequest()->getRequestUserPHID(); 20 + return ReleephUserView::getNewInstance() 21 + ->setRenderUserPHID($phid) 22 + ->render(); 23 + } 24 + 25 + public function hasSelectablePHIDs() { 26 + return true; 27 + } 28 + 29 + public function getSelectTokenizerDatasource() { 30 + return '/typeahead/common/users/'; 31 + } 32 + 33 + public function getSelectablePHIDs() { 34 + return array( 35 + $this->getReleephRequest()->getRequestUserPHID(), 36 + ); 37 + } 38 + 39 + public function shouldAppearOnCommitMessage() { 40 + return true; 41 + } 42 + 43 + public function shouldAppearOnRevertMessage() { 44 + return true; 45 + } 46 + 47 + public function renderLabelForCommitMessage() { 48 + return "Requested By"; 49 + } 50 + 51 + public function renderValueForCommitMessage() { 52 + $phid = $this->getReleephRequest()->getRequestUserPHID(); 53 + $handles = id(new PhabricatorObjectHandleData(array($phid))) 54 + ->setViewer($this->getUser()) 55 + ->loadHandles(); 56 + return $handles[$phid]->getName(); 57 + } 58 + 59 + }
+37
src/applications/releeph/field/specification/ReleephRevisionFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephRevisionFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Revision'; 8 + } 9 + 10 + public function renderValueForHeaderView() { 11 + $data = $this 12 + ->getReleephRequest() 13 + ->loadPhabricatorRepositoryCommitData(); 14 + if (!$data) { 15 + return null; 16 + } 17 + 18 + $phid = $data->getCommitDetail('differential.revisionPHID'); 19 + if (!$phid) { 20 + return null; 21 + } 22 + 23 + $handles = $this->getReleephRequest()->getHandles(); 24 + $handle = $handles[$phid]; 25 + $link = $handle 26 + // Hack to remove the strike-through rendering of diff links 27 + ->setStatus(null) 28 + ->renderLink(); 29 + return phutil_tag( 30 + 'div', 31 + array( 32 + 'class' => 'releeph-header-text-truncated', 33 + ), 34 + $link); 35 + } 36 + 37 + }
+63
src/applications/releeph/field/specification/ReleephRiskFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephRiskFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + static $defaultRisks = array( 7 + 'NONE' => 'Completely safe to pick this request.', 8 + 'SOME' => 'There is some risk this could break things, but not much.', 9 + 'HIGH' => 'This is pretty risky, but is also very important.', 10 + ); 11 + 12 + public function getName() { 13 + return 'Riskiness'; 14 + } 15 + 16 + public function getStorageKey() { 17 + return 'risk'; 18 + } 19 + 20 + public function renderLabelForHeaderView() { 21 + return 'Riskiness'; 22 + } 23 + 24 + private $error = true; 25 + 26 + public function renderEditControl(AphrontRequest $request) { 27 + $value = $request->getStr('risk', $this->getValue()); 28 + $buttons = id(new AphrontFormRadioButtonControl()) 29 + ->setLabel('Riskiness') 30 + ->setName('risk') 31 + ->setError($this->error) 32 + ->setValue($value); 33 + foreach (self::$defaultRisks as $value => $description) { 34 + $buttons->addButton($value, $value, $description); 35 + } 36 + return $buttons; 37 + } 38 + 39 + public function validate($risk) { 40 + if (!$risk) { 41 + $this->error = 'Required'; 42 + throw new ReleephFieldParseException( 43 + $this, 44 + "No risk was given, which probably means we've changed the set ". 45 + "of valid risks since you made this request. Please pick one."); 46 + } 47 + if (!idx(self::$defaultRisks, $risk)) { 48 + throw new ReleephFieldParseException( 49 + $this, 50 + "Unknown risk '{$risk}'."); 51 + } 52 + } 53 + 54 + public function renderHelpForArcanist() { 55 + $help = ''; 56 + foreach (self::$defaultRisks as $name => $description) { 57 + $help .= " **{$name}**\n"; 58 + $help .= phutil_console_wrap($description."\n", 8); 59 + } 60 + return $help; 61 + } 62 + 63 + }
+46
src/applications/releeph/field/specification/ReleephSeverityFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephSeverityFieldSpecification 4 + extends ReleephLevelFieldSpecification { 5 + 6 + const HOTFIX = 'HOTFIX'; 7 + const RELEASE = 'RELEASE'; 8 + 9 + public function getName() { 10 + return 'Severity'; 11 + } 12 + 13 + public function getStorageKey() { 14 + return 'releeph:severity'; 15 + } 16 + 17 + public function getLevels() { 18 + return array( 19 + self::HOTFIX, 20 + self::RELEASE, 21 + ); 22 + } 23 + 24 + public function getDefaultLevel() { 25 + return self::RELEASE; 26 + } 27 + 28 + public function getNameForLevel($level) { 29 + static $names = array( 30 + self::HOTFIX => 'HOTFIX', 31 + self::RELEASE => 'RELEASE', 32 + ); 33 + return idx($names, $level, $level); 34 + } 35 + 36 + public function getDescriptionForLevel($level) { 37 + static $descriptions = array( 38 + self::HOTFIX => 39 + 'Needs merging and fixing right now.', 40 + self::RELEASE => 41 + 'Required for the currently rolling release.', 42 + ); 43 + return idx($descriptions, $level); 44 + } 45 + 46 + }
+89
src/applications/releeph/field/specification/ReleephStatusFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephStatusFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + public function getName() { 7 + return 'Status'; 8 + } 9 + 10 + public function renderValueForHeaderView() { 11 + return id(new ReleephRequestStatusView()) 12 + ->setReleephRequest($this->getReleephRequest()) 13 + ->render(); 14 + } 15 + 16 + private static $filters = array( 17 + 'req' => ReleephRequest::STATUS_REQUESTED, 18 + 'app' => ReleephRequest::STATUS_NEEDS_PICK, 19 + 'rej' => ReleephRequest::STATUS_REJECTED, 20 + 'abn' => ReleephRequest::STATUS_ABANDONED, 21 + 'mer' => ReleephRequest::STATUS_PICKED, 22 + 'rrq' => ReleephRequest::STATUS_NEEDS_REVERT, 23 + 'rev' => ReleephRequest::STATUS_REVERTED, 24 + ); 25 + 26 + protected function appendSelectControls( 27 + AphrontFormView $form, 28 + AphrontRequest $request, 29 + array $all_releeph_requests, 30 + array $all_releeph_requests_without_this_field) { 31 + 32 + $filter_names = array( 33 + null => 'All', 34 + ); 35 + 36 + foreach (self::$filters as $code => $status) { 37 + $name = ReleephRequest::getStatusDescriptionFor($status); 38 + $filter_names[$code] = $name; 39 + } 40 + 41 + $key = 'status'; 42 + $code = $request->getStr($key); 43 + $current_status = idx(self::$filters, $code); 44 + 45 + $codes = array_flip(self::$filters); 46 + 47 + $counters = array(null => count($all_releeph_requests_without_this_field)); 48 + foreach ($all_releeph_requests_without_this_field as $releeph_request) { 49 + $this_status = $releeph_request->getStatus(); 50 + $this_code = idx($codes, $this_status); 51 + if (!isset($counters[$this_code])) { 52 + $counters[$this_code] = 0; 53 + } 54 + $counters[$this_code]++; 55 + } 56 + 57 + $control = id(new AphrontFormCountedToggleButtonsControl()) 58 + ->setLabel($this->getName()) 59 + ->setValue($code) 60 + ->setBaseURI($request->getRequestURI(), $key) 61 + ->setButtons($filter_names) 62 + ->setCounters($counters); 63 + 64 + $form 65 + ->appendChild($control) 66 + ->addHiddenInput($key, $code); 67 + } 68 + 69 + protected function selectReleephRequests(AphrontRequest $request, 70 + array &$releeph_requests) { 71 + 72 + $key = 'status'; 73 + $code = $request->getStr($key); 74 + if (!$code) { 75 + return; 76 + } 77 + 78 + $current_status = idx(self::$filters, $code); 79 + 80 + $filtered = array(); 81 + foreach ($releeph_requests as $releeph_request) { 82 + if ($releeph_request->getStatus() == $current_status) { 83 + $filtered[] = $releeph_request; 84 + } 85 + } 86 + $releeph_requests = $filtered; 87 + } 88 + 89 + }
+46
src/applications/releeph/field/specification/ReleephSummaryFieldSpecification.php
··· 1 + <?php 2 + 3 + final class ReleephSummaryFieldSpecification 4 + extends ReleephFieldSpecification { 5 + 6 + const MAX_SUMMARY_LENGTH = 60; 7 + 8 + public function getName() { 9 + return 'Summary'; 10 + } 11 + 12 + public function getStorageKey() { 13 + return 'summary'; 14 + } 15 + 16 + private $error = false; 17 + 18 + public function renderEditControl(AphrontRequest $request) { 19 + $summary = $request->getStr('summary', $this->getValue()); 20 + return id(new AphrontFormTextControl()) 21 + ->setLabel('Summary') 22 + ->setName('summary') 23 + ->setError($this->error) 24 + ->setValue($summary) 25 + ->setCaption( 26 + 'Leave this blank to use the original commit title'); 27 + } 28 + 29 + public function renderHelpForArcanist() { 30 + $text = 31 + "A one-line title summarizing this request. ". 32 + "Leave blank to use the original commit title.\n"; 33 + return phutil_console_wrap($text, 8); 34 + } 35 + 36 + public function validate($summary) { 37 + if ($summary && strlen($summary) > self::MAX_SUMMARY_LENGTH) { 38 + $this->error = 'Too long!'; 39 + throw new ReleephFieldParseException( 40 + $this, sprintf( 41 + 'Please keep your summary to under %d characters.', 42 + self::MAX_SUMMARY_LENGTH)); 43 + } 44 + } 45 + 46 + }
+154
src/applications/releeph/storage/ReleephBranch.php
··· 1 + <?php 2 + 3 + final class ReleephBranch extends ReleephDAO { 4 + 5 + protected $phid; 6 + protected $releephProjectID; 7 + protected $isActive; 8 + protected $createdByUserPHID; 9 + 10 + // The immutable name of this branch ('releases/foo-2013.01.24') 11 + protected $name; 12 + protected $basename; 13 + 14 + // The symbolic name of this branch (LATEST, PRODUCTION, RC, ...) 15 + // See SYMBOLIC_NAME_NOTE below 16 + protected $symbolicName; 17 + 18 + // Where to cut the branch 19 + protected $cutPointCommitIdentifier; 20 + protected $cutPointCommitPHID; 21 + 22 + protected $details = array(); 23 + 24 + public function getConfiguration() { 25 + return array( 26 + self::CONFIG_AUX_PHID => true, 27 + self::CONFIG_SERIALIZATION => array( 28 + 'details' => self::SERIALIZATION_JSON, 29 + ), 30 + ) + parent::getConfiguration(); 31 + } 32 + 33 + public function generatePHID() { 34 + return PhabricatorPHID::generateNewPHID( 35 + ReleephPHIDConstants::PHID_TYPE_REBR); 36 + } 37 + 38 + public function getDetail($key, $default = null) { 39 + return idx($this->getDetails(), $key, $default); 40 + } 41 + 42 + public function setDetail($key, $value) { 43 + $this->details[$key] = $value; 44 + return $this; 45 + } 46 + 47 + public function willWriteData(array &$data) { 48 + // If symbolicName is omitted, set it to the basename. 49 + // 50 + // This means that we can enforce symbolicName as a UNIQUE column in the 51 + // DB. We'll interpret symbolicName === basename as meaning "no symbolic 52 + // name". 53 + // 54 + // SYMBOLIC_NAME_NOTE 55 + if (!$data['symbolicName']) { 56 + $data['symbolicName'] = $data['basename']; 57 + } 58 + parent::willWriteData($data); 59 + } 60 + 61 + public function getSymbolicName() { 62 + // See SYMBOLIC_NAME_NOTE above for why this is needed 63 + if ($this->symbolicName == $this->getBasename()) { 64 + return ''; 65 + } 66 + return $this->symbolicName; 67 + } 68 + 69 + public function setSymbolicName($name) { 70 + if ($name) { 71 + parent::setSymbolicName($name); 72 + } else { 73 + parent::setSymbolicName($this->getBasename()); 74 + } 75 + return $this; 76 + } 77 + 78 + public function getDisplayName() { 79 + if ($sn = $this->getSymbolicName()) { 80 + return $sn; 81 + } 82 + return $this->getBasename(); 83 + } 84 + 85 + public function getDisplayNameWithDetail() { 86 + $n = $this->getBasename(); 87 + if ($sn = $this->getSymbolicName()) { 88 + return "{$sn} ({$n})"; 89 + } else { 90 + return $n; 91 + } 92 + } 93 + 94 + public function getURI($path = null) { 95 + $components = array( 96 + '/releeph', 97 + rawurlencode($this->loadReleephProject()->getName()), 98 + rawurlencode($this->getBasename()), 99 + $path 100 + ); 101 + return PhabricatorEnv::getProductionURI(implode('/', $components)); 102 + } 103 + 104 + public function loadReleephProject() { 105 + return $this->loadOneRelative( 106 + new ReleephProject(), 107 + 'id', 108 + 'getReleephProjectID'); 109 + } 110 + 111 + private function loadReleephRequestHandles(PhabricatorUser $user, $reqs) { 112 + $phids_to_phetch = array(); 113 + foreach ($reqs as $rr) { 114 + $phids_to_phetch[] = $rr->getRequestCommitPHID(); 115 + $phids_to_phetch[] = $rr->getRequestUserPHID(); 116 + $phids_to_phetch[] = $rr->getCommitPHID(); 117 + 118 + $intents = $rr->getUserIntents(); 119 + if ($intents) { 120 + foreach ($intents as $user_phid => $intent) { 121 + $phids_to_phetch[] = $user_phid; 122 + } 123 + } 124 + 125 + $request_commit = $rr->loadPhabricatorRepositoryCommit(); 126 + if ($request_commit) { 127 + $phids_to_phetch[] = $request_commit->getAuthorPHID(); 128 + $phids_to_phetch[] = $rr->loadRequestCommitDiffPHID(); 129 + } 130 + } 131 + $handles = id(new PhabricatorObjectHandleData($phids_to_phetch)) 132 + ->setViewer($user) 133 + ->loadHandles(); 134 + return $handles; 135 + } 136 + 137 + public function populateReleephRequestHandles(PhabricatorUser $user, $reqs) { 138 + $handles = $this->loadReleephRequestHandles($user, $reqs); 139 + foreach ($reqs as $req) { 140 + $req->setHandles($handles); 141 + } 142 + } 143 + 144 + public function loadReleephRequests(PhabricatorUser $user) { 145 + $reqs = $this->loadRelatives(new ReleephRequest(), 'branchID'); 146 + $this->populateReleephRequestHandles($user, $reqs); 147 + return $reqs; 148 + } 149 + 150 + public function isActive() { 151 + return $this->getIsActive(); 152 + } 153 + 154 + }
+9
src/applications/releeph/storage/ReleephDAO.php
··· 1 + <?php 2 + 3 + abstract class ReleephDAO extends PhabricatorLiskDAO { 4 + 5 + public function getApplicationName() { 6 + return 'releeph'; 7 + } 8 + 9 + }
+176
src/applications/releeph/storage/ReleephProject.php
··· 1 + <?php 2 + 3 + final class ReleephProject extends ReleephDAO { 4 + 5 + const DEFAULT_BRANCH_NAMESPACE = 'releeph-releases'; 6 + const SYSTEM_AGENT_USERNAME_PREFIX = 'releeph-agent-'; 7 + 8 + const COMMIT_AUTHOR_NONE = 'commit-author-none'; 9 + const COMMIT_AUTHOR_FROM_DIFF = 'commit-author-is-from-diff'; 10 + const COMMIT_AUTHOR_REQUESTOR = 'commit-author-is-requestor'; 11 + 12 + protected $phid; 13 + protected $name; 14 + 15 + // Specifying the place to pick from is a requirement for svn, though not 16 + // for git. It's always useful though for reasoning about what revs have 17 + // been picked and which haven't. 18 + protected $trunkBranch; 19 + 20 + protected $repositoryID; 21 + protected $repositoryPHID; 22 + protected $isActive; 23 + protected $createdByUserPHID; 24 + protected $arcanistProjectID; 25 + protected $projectID; 26 + 27 + protected $details = array(); 28 + 29 + public function getConfiguration() { 30 + return array( 31 + self::CONFIG_AUX_PHID => true, 32 + self::CONFIG_SERIALIZATION => array( 33 + 'details' => self::SERIALIZATION_JSON, 34 + ), 35 + ) + parent::getConfiguration(); 36 + } 37 + 38 + public function generatePHID() { 39 + return PhabricatorPHID::generateNewPHID( 40 + ReleephPHIDConstants::PHID_TYPE_REPR); 41 + } 42 + 43 + public function getDetail($key, $default = null) { 44 + return idx($this->details, $key, $default); 45 + } 46 + 47 + public function getURI($path = null) { 48 + $components = array( 49 + '/releeph/project', 50 + $this->getID(), 51 + $path 52 + ); 53 + return PhabricatorEnv::getProductionURI(implode('/', $components)); 54 + } 55 + 56 + public function setDetail($key, $value) { 57 + $this->details[$key] = $value; 58 + return $this; 59 + } 60 + 61 + public function willSaveObject() { 62 + // Do this first, to generate the PHID 63 + parent::willSaveObject(); 64 + 65 + $banned_names = $this->getBannedNames(); 66 + if (in_array($this->name, $banned_names)) { 67 + throw new Exception(sprintf( 68 + "The name '%s' is in the list of banned project names!", 69 + $this->name, 70 + implode(', ', $banned_names))); 71 + } 72 + 73 + if (!$this->getDetail('releaseCounter')) { 74 + $this->setDetail('releaseCounter', 0); 75 + } 76 + } 77 + 78 + public function loadPhabricatorProject() { 79 + if ($id = $this->getProjectID()) { 80 + return id(new PhabricatorProject())->load($id); 81 + } 82 + return id(new PhabricatorProject())->makeEphemeral(); // dummy 83 + } 84 + 85 + public function loadArcanistProject() { 86 + return $this->loadOneRelative( 87 + new PhabricatorRepositoryArcanistProject(), 88 + 'id', 89 + 'getArcanistProjectID'); 90 + } 91 + 92 + public function getPushers() { 93 + return $this->getDetail('pushers', array()); 94 + } 95 + 96 + public function isPusherPHID($phid) { 97 + $pusher_phids = $this->getDetail('pushers', array()); 98 + return in_array($phid, $pusher_phids); 99 + } 100 + 101 + public function isPusher(PhabricatorUser $user) { 102 + return $this->isPusherPHID($user->getPHID()); 103 + } 104 + 105 + public function loadPhabricatorRepository() { 106 + return $this->loadOneRelative( 107 + new PhabricatorRepository(), 108 + 'id', 109 + 'getRepositoryID'); 110 + } 111 + 112 + public function getCurrentReleaseNumber() { 113 + $current_release_numbers = array(); 114 + 115 + // From the project... 116 + $current_release_numbers[] = $this->getDetail('releaseCounter', 0); 117 + 118 + // From any branches... 119 + $branches = id(new ReleephBranch())->loadAllWhere( 120 + 'releephProjectID = %d', $this->getID()); 121 + if ($branches) { 122 + $release_numbers = array(); 123 + foreach ($branches as $branch) { 124 + $current_release_numbers[] = $branch->getDetail('releaseNumber', 0); 125 + } 126 + } 127 + 128 + return max($current_release_numbers); 129 + } 130 + 131 + public function getReleephFieldSelector() { 132 + $class = $this->getDetail('field_selector'); 133 + if (!$class) { 134 + $key = 'releeph.field-selector'; 135 + $class = PhabricatorEnv::getEnvConfig($key); 136 + } 137 + 138 + if ($class) { 139 + return newv($class, array()); 140 + } else { 141 + return new ReleephDefaultFieldSelector(); 142 + } 143 + } 144 + 145 + /** 146 + * Wrapper to setIsActive() that logs who deactivated a project 147 + */ 148 + public function deactivate(PhabricatorUser $actor) { 149 + return $this 150 + ->setIsActive(0) 151 + ->setDetail('last_deactivated_user', $actor->getPHID()) 152 + ->setDetail('last_deactivated_time', time()); 153 + } 154 + 155 + // Hide this from the public 156 + private function setIsActive($v) { 157 + return parent::setIsActive($v); 158 + } 159 + 160 + private function getBannedNames() { 161 + return array( 162 + 'branch', // no one's tried this... yet! 163 + ); 164 + } 165 + 166 + public function isTestFile($filename) { 167 + $test_paths = $this->getDetail('testPaths', array()); 168 + 169 + foreach ($test_paths as $test_path) { 170 + if (preg_match($test_path, $filename)) { 171 + return true; 172 + } 173 + } 174 + return false; 175 + } 176 + }
+309
src/applications/releeph/storage/ReleephRequest.php
··· 1 + <?php 2 + 3 + final class ReleephRequest extends ReleephDAO { 4 + 5 + protected $phid; 6 + protected $branchID; 7 + protected $requestUserPHID; 8 + protected $details = array(); 9 + protected $userIntents = array(); 10 + protected $inBranch; 11 + protected $pickStatus; 12 + 13 + // Information about the thing being requested 14 + protected $requestCommitIdentifier; 15 + protected $requestCommitPHID; 16 + protected $requestCommitOrdinal; 17 + 18 + // Information about the last commit to the releeph branch 19 + protected $commitIdentifier; 20 + protected $committedByUserPHID; 21 + protected $commitPHID; 22 + 23 + // Pre-populated handles that we'll bulk load in ReleephBranch 24 + private $handles; 25 + 26 + 27 + /* -( Constants and helper methods )--------------------------------------- */ 28 + 29 + const INTENT_WANT = 'want'; 30 + const INTENT_PASS = 'pass'; 31 + 32 + const PICK_PENDING = 1; // old 33 + const PICK_FAILED = 2; 34 + const PICK_OK = 3; 35 + const PICK_MANUAL = 4; // old 36 + const REVERT_OK = 5; 37 + const REVERT_FAILED = 6; 38 + 39 + const STATUS_REQUESTED = 1; 40 + const STATUS_NEEDS_PICK = 2; // aka approved 41 + const STATUS_REJECTED = 3; 42 + const STATUS_ABANDONED = 4; 43 + const STATUS_PICKED = 5; 44 + const STATUS_REVERTED = 6; 45 + const STATUS_NEEDS_REVERT = 7; // aka revert requested 46 + 47 + public function shouldBeInBranch() { 48 + return 49 + $this->getPusherIntent() == self::INTENT_WANT && 50 + /** 51 + * We use "!= pass" instead of "== want" in case the requestor intent is 52 + * not present. In other words, only revert if the requestor explicitly 53 + * passed. 54 + */ 55 + $this->getRequestorIntent() != self::INTENT_PASS; 56 + } 57 + 58 + /** 59 + * Will return INTENT_WANT if any pusher wants this request, and no pusher 60 + * passes on this request. 61 + */ 62 + public function getPusherIntent() { 63 + $project = $this->loadReleephProject(); 64 + if (!$project->getPushers()) { 65 + return self::INTENT_WANT; 66 + } 67 + 68 + $found_pusher_want = false; 69 + foreach ($this->userIntents as $phid => $intent) { 70 + if ($project->isPusherPHID($phid)) { 71 + if ($intent == self::INTENT_PASS) { 72 + return self::INTENT_PASS; 73 + } 74 + 75 + $found_pusher_want = true; 76 + } 77 + } 78 + 79 + if ($found_pusher_want) { 80 + return self::INTENT_WANT; 81 + } else { 82 + return null; 83 + } 84 + } 85 + 86 + public function getRequestorIntent() { 87 + return idx($this->userIntents, $this->requestUserPHID); 88 + } 89 + 90 + public function getStatus() { 91 + return $this->calculateStatus(); 92 + } 93 + 94 + private function calculateStatus() { 95 + if ($this->shouldBeInBranch()) { 96 + if ($this->getInBranch()) { 97 + return self::STATUS_PICKED; 98 + } else { 99 + return self::STATUS_NEEDS_PICK; 100 + } 101 + } else { 102 + if ($this->getInBranch()) { 103 + return self::STATUS_NEEDS_REVERT; 104 + } else { 105 + $has_been_in_branch = $this->getCommitIdentifier(); 106 + // Regardless of why we reverted something, always say reverted if it 107 + // was once in the branch. 108 + if ($has_been_in_branch) { 109 + return self::STATUS_REVERTED; 110 + } elseif ($this->getPusherIntent() === ReleephRequest::INTENT_PASS) { 111 + // Otherwise, if it has never been in the branch, explicitly say why: 112 + return self::STATUS_REJECTED; 113 + } elseif ($this->getRequestorIntent() === ReleephRequest::INTENT_WANT) { 114 + return self::STATUS_REQUESTED; 115 + } else { 116 + return self::STATUS_ABANDONED; 117 + } 118 + } 119 + } 120 + } 121 + 122 + public static function getStatusDescriptionFor($status) { 123 + static $descriptions = array( 124 + self::STATUS_REQUESTED => 'Requested', 125 + self::STATUS_REJECTED => 'Rejected', 126 + self::STATUS_ABANDONED => 'Abandoned', 127 + self::STATUS_PICKED => 'Picked', 128 + self::STATUS_REVERTED => 'Reverted', 129 + self::STATUS_NEEDS_PICK => 'Needs Pick', 130 + self::STATUS_NEEDS_REVERT => 'Needs Revert', 131 + ); 132 + return idx($descriptions, $status, '??'); 133 + } 134 + 135 + public static function getStatusClassSuffixFor($status) { 136 + $description = self::getStatusDescriptionFor($status); 137 + $class = str_replace(' ', '-', strtolower($description)); 138 + return $class; 139 + } 140 + 141 + 142 + /* -( Lisk mechanics )----------------------------------------------------- */ 143 + 144 + public function getConfiguration() { 145 + return array( 146 + self::CONFIG_AUX_PHID => true, 147 + self::CONFIG_SERIALIZATION => array( 148 + 'details' => self::SERIALIZATION_JSON, 149 + 'userIntents' => self::SERIALIZATION_JSON, 150 + ), 151 + ) + parent::getConfiguration(); 152 + } 153 + 154 + public function generatePHID() { 155 + return PhabricatorPHID::generateNewPHID( 156 + ReleephPHIDConstants::PHID_TYPE_RERQ); 157 + } 158 + 159 + 160 + /* -( Helpful accessors )--------------------------------------------------- */ 161 + 162 + public function setHandles($handles) { 163 + $this->handles = $handles; 164 + return $this; 165 + } 166 + 167 + public function getHandles() { 168 + if (!$this->handles) { 169 + throw new Exception( 170 + "You must call ReleephBranch::populateReleephRequestHandles() first"); 171 + } 172 + return $this->handles; 173 + } 174 + 175 + public function getDetail($key, $default = null) { 176 + return idx($this->getDetails(), $key, $default); 177 + } 178 + 179 + public function setDetail($key, $value) { 180 + $this->details[$key] = $value; 181 + return $this; 182 + } 183 + 184 + public function getReason() { 185 + // Backward compatibility: reason used to be called comments 186 + $reason = $this->getDetail('reason'); 187 + if (!$reason) { 188 + return $this->getDetail('comments'); 189 + } 190 + return $reason; 191 + } 192 + 193 + public function getSummary() { 194 + /** 195 + * Instead, you can use: 196 + * - getDetail('summary') // the actual user-chosen summary 197 + * - getSummaryForDisplay() // falls back to the original commit title 198 + * 199 + * Or for the fastidious: 200 + * - id(new ReleephSummaryFieldSpecification()) 201 + * ->setReleephRequest($rr) 202 + * ->getValue() // programmatic equivalent to getDetail() 203 + */ 204 + throw new Exception( 205 + "getSummary() has been deprecated!"); 206 + } 207 + 208 + /** 209 + * Allow a null summary, and fall back to the title of the commit. 210 + */ 211 + public function getSummaryForDisplay() { 212 + $summary = $this->getDetail('summary'); 213 + 214 + if (!$summary) { 215 + $pr_commit_data = $this->loadPhabricatorRepositoryCommitData(); 216 + if ($pr_commit_data) { 217 + $message_lines = explode("\n", $pr_commit_data->getCommitMessage()); 218 + $message_lines = array_filter($message_lines); 219 + $summary = head($message_lines); 220 + } 221 + } 222 + 223 + if (!$summary) { 224 + $summary = '(no summary given and commit message empty or unparsed)'; 225 + } 226 + 227 + return $summary; 228 + } 229 + 230 + public function loadRequestCommitDiffPHID() { 231 + $commit_data = $this->loadPhabricatorRepositoryCommitData(); 232 + if (!$commit_data) { 233 + return null; 234 + } 235 + return $commit_data->getCommitDetail('differential.revisionPHID'); 236 + } 237 + 238 + 239 + /* -( Loading external objects )------------------------------------------- */ 240 + 241 + public function loadReleephBranch() { 242 + return $this->loadOneRelative( 243 + new ReleephBranch(), 244 + 'id', 245 + 'getBranchID'); 246 + } 247 + 248 + public function loadReleephProject() { 249 + return $this->loadReleephBranch()->loadReleephProject(); 250 + } 251 + 252 + public function loadEvents() { 253 + return $this->loadRelatives( 254 + new ReleephRequestEvent(), 255 + 'releephRequestID', 256 + 'getID', 257 + '(1 = 1) ORDER BY dateCreated, id'); 258 + } 259 + 260 + public function loadPhabricatorRepositoryCommit() { 261 + return $this->loadOneRelative( 262 + new PhabricatorRepositoryCommit(), 263 + 'phid', 264 + 'getRequestCommitPHID'); 265 + } 266 + 267 + public function loadPhabricatorRepositoryCommitData() { 268 + return $this->loadOneRelative( 269 + new PhabricatorRepositoryCommitData(), 270 + 'commitID', 271 + 'getRequestCommitOrdinal'); 272 + } 273 + 274 + public function loadDifferentialRevision() { 275 + return $this->loadOneRelative( 276 + new DifferentialRevision(), 277 + 'phid', 278 + 'loadRequestCommitDiffPHID'); 279 + } 280 + 281 + 282 + /* -( State change helpers )----------------------------------------------- */ 283 + 284 + public function setUserIntent(PhabricatorUser $user, $intent) { 285 + $this->userIntents[$user->getPHID()] = $intent; 286 + return $this; 287 + } 288 + 289 + 290 + /* -( Migrating to status-less ReleephRequests )--------------------------- */ 291 + 292 + protected function didReadData() { 293 + if ($this->userIntents === null) { 294 + $this->userIntents = array(); 295 + } 296 + } 297 + 298 + public function setStatus($value) { 299 + throw new Exception('`status` is now deprecated!'); 300 + } 301 + 302 + 303 + /* -( Make magic Lisk methods private )------------------------------------ */ 304 + 305 + private function setUserIntents(array $ar) { 306 + return parent::setUserIntents($ar); 307 + } 308 + 309 + }
+39
src/applications/releeph/storage/event/ReleephEvent.php
··· 1 + <?php 2 + 3 + final class ReleephEvent extends ReleephDAO { 4 + 5 + const TYPE_BRANCH_CREATE = 'branch-create'; 6 + const TYPE_BRANCH_ACCESS = 'branch-access-change'; 7 + 8 + protected $releephProjectID; 9 + protected $releephBranchID; 10 + protected $type; 11 + protected $epoch; 12 + protected $actorPHID; 13 + protected $details = array(); 14 + 15 + public function getConfiguration() { 16 + return array( 17 + self::CONFIG_SERIALIZATION => array( 18 + 'details' => self::SERIALIZATION_JSON, 19 + ), 20 + ) + parent::getConfiguration(); 21 + } 22 + 23 + public function getDetail($key, $default = null) { 24 + return idx($this->details, $key, $default); 25 + } 26 + 27 + public function setDetail($key, $value) { 28 + $this->details[$key] = $value; 29 + return $this; 30 + } 31 + 32 + protected function willSaveObject() { 33 + parent::willSaveObject(); 34 + if (!$this->epoch) { 35 + $this->epoch = $this->dateCreated; 36 + } 37 + } 38 + 39 + }
+94
src/applications/releeph/storage/request/ReleephRequestEvent.php
··· 1 + <?php 2 + 3 + final class ReleephRequestEvent extends ReleephDAO { 4 + 5 + const TYPE_CREATE = 'create'; 6 + const TYPE_STATUS = 'status'; // old events 7 + const TYPE_USER_INTENT = 'user-intent'; 8 + const TYPE_PICK_STATUS = 'pick-status'; 9 + const TYPE_COMMIT = 'commit'; 10 + const TYPE_MANUAL_ACTION = 'manual-action'; 11 + const TYPE_DISCOVERY = 'discovery'; 12 + const TYPE_COMMENT = 'comment'; 13 + 14 + protected $releephRequestID; 15 + protected $type; 16 + protected $actorPHID; 17 + protected $details = array(); 18 + 19 + public function getConfiguration() { 20 + return array( 21 + self::CONFIG_SERIALIZATION => array( 22 + 'details' => self::SERIALIZATION_JSON, 23 + ), 24 + ) + parent::getConfiguration(); 25 + } 26 + 27 + public function getDetail($key, $default = null) { 28 + return idx($this->details, $key, $default); 29 + } 30 + 31 + public function setDetail($key, $value) { 32 + $this->details[$key] = $value; 33 + return $this; 34 + } 35 + 36 + private function setDetails(array $details) { 37 + throw new Exception('Use setDetail()!'); 38 + } 39 + 40 + public function setStatusBefore($status) { 41 + return $this->setDetail('oldStatus', $status); 42 + } 43 + 44 + public function setStatusAfter($status) { 45 + return $this->setDetail('newStatus', $status); 46 + } 47 + 48 + public function getStatusBefore() { 49 + return $this->getDetail('oldStatus'); 50 + } 51 + 52 + public function getStatusAfter() { 53 + return $this->getDetail('newStatus'); 54 + } 55 + 56 + public function getComment() { 57 + return $this->getDetail('comment'); 58 + } 59 + 60 + public function extractPHIDs() { 61 + $phids = array(); 62 + $phids[] = $this->actorPHID; 63 + foreach ($this->details as $key => $value) { 64 + if (strpos($key, 'PHID') !== false || strpos($key, 'phid') !== false) { 65 + $phids[] = $value; 66 + } 67 + } 68 + return $phids; 69 + } 70 + 71 + public function canGroupWith(ReleephRequestEvent $next) { 72 + if ($this->getActorPHID() != $next->getActorPHID()) { 73 + return false; 74 + } 75 + 76 + if ($this->getComment() && $next->getComment()) { 77 + return false; 78 + } 79 + 80 + // Break the chain if the next event changes the status 81 + if ($next->getStatusBefore() != $next->getStatusAfter()) { 82 + return false; 83 + } 84 + 85 + // Don't group if the next event starts off with a different status to the 86 + // one we ended with. This probably shouldn't ever happen. 87 + if ($this->getStatusAfter() != $next->getStatusBefore()) { 88 + return false; 89 + } 90 + 91 + return true; 92 + } 93 + 94 + }
+3
src/applications/releeph/storage/request/exception/ReleephRequestException.php
··· 1 + <?php 2 + 3 + final class ReleephRequestException extends Exception {}
+155
src/applications/releeph/view/ReleephProjectView.php
··· 1 + <?php 2 + 3 + final class ReleephProjectView extends AphrontView { 4 + 5 + private $showOpenBranches = true; 6 + private $releephProject; 7 + private $releephBranches; 8 + 9 + public function setShowOpenBranches($active) { 10 + $this->showOpenBranches = $active; 11 + return $this; 12 + } 13 + 14 + public function setReleephProject($releeph_project) { 15 + $this->releephProject = $releeph_project; 16 + return $this; 17 + } 18 + 19 + public function setBranches($branches) { 20 + $this->releephBranches = $branches; 21 + return $this; 22 + } 23 + 24 + public function render() { 25 + $releeph_project = $this->releephProject; 26 + 27 + if ($this->showOpenBranches) { 28 + $releeph_branches = mfilter($this->releephBranches, 'getIsActive'); 29 + } else { 30 + $releeph_branches = mfilter($this->releephBranches, 'getIsActive', true); 31 + } 32 + 33 + // Load all relevant PHID handles 34 + $phids = array_merge( 35 + array( 36 + $this->releephProject->getPHID(), 37 + $this->releephProject->getRepositoryPHID(), 38 + ), 39 + mpull($releeph_branches, 'getCreatedByUserPHID'), 40 + mpull($releeph_branches, 'getCutPointCommitPHID'), 41 + $releeph_project->getPushers()); 42 + $handles = id(new PhabricatorObjectHandleData($phids)) 43 + ->setViewer($this->getUser()) 44 + ->loadHandles(); 45 + 46 + // Sort branches, which requires the handles above 47 + $releeph_branches = self::sortBranches($releeph_branches, $handles); 48 + 49 + // The header 50 + $repository_phid = $releeph_project->getRepositoryPHID(); 51 + 52 + $header = hsprintf( 53 + '%s in %s repository', 54 + $releeph_project->getName(), 55 + $handles[$repository_phid]->renderLink()); 56 + 57 + if ($this->showOpenBranches) { 58 + $view_other_link = phutil_tag( 59 + 'a', 60 + array( 61 + 'href' => $releeph_project->getURI('closedbranches/'), 62 + ), 63 + 'View closed branches'); 64 + } else { 65 + $view_other_link = phutil_tag( 66 + 'a', 67 + array( 68 + 'href' => $releeph_project->getURI(), 69 + ), 70 + 'View open branches'); 71 + } 72 + 73 + $header = hsprintf("%s &middot; %s", $header, $view_other_link); 74 + 75 + // The "create branch" button 76 + $create_branch_url = $releeph_project->getURI('cutbranch/'); 77 + 78 + // Pushers info 79 + $pushers_info = array(); 80 + $pushers = $releeph_project->getPushers(); 81 + require_celerity_resource('releeph-project'); 82 + if ($pushers) { 83 + $pushers_info[] = phutil_tag('h2', array(), 'Pushers'); 84 + foreach ($pushers as $user_phid) { 85 + $handle = $handles[$user_phid]; 86 + $div = phutil_tag( 87 + 'div', 88 + array( 89 + 'class' => 'releeph-pusher', 90 + 'style' => 'background-image: url('.$handle->getImageURI().');', 91 + ), 92 + phutil_tag( 93 + 'div', 94 + array( 95 + 'class' => 'releeph-pusher-body', 96 + ), 97 + $handles[$user_phid]->renderLink())); 98 + $pushers_info[] = $div; 99 + } 100 + 101 + $pushers_info[] = hsprintf('<div style="clear: both;"></div>'); 102 + } 103 + 104 + // Put it all together 105 + $panel = id(new AphrontPanelView()) 106 + ->setHeader($header) 107 + ->appendChild(phutil_implode_html('', $pushers_info)); 108 + 109 + foreach ($releeph_branches as $ii => $releeph_branch) { 110 + $box = id(new ReleephBranchBoxView()) 111 + ->setUser($this->user) 112 + ->setHandles($handles) 113 + ->setReleephBranch($releeph_branch) 114 + ->setNamed(); 115 + 116 + if ($ii === 0) { 117 + $box->setLatest(); 118 + } 119 + $panel->appendChild($box); 120 + } 121 + 122 + return $panel->render(); 123 + } 124 + 125 + /** 126 + * Sort branches by the point at which they were cut, newest cut points 127 + * first. 128 + * 129 + * If branches share a cut point, sort newest branch first. 130 + */ 131 + private static function sortBranches($branches, $handles) { 132 + // Group by commit phid 133 + $groups = mgroup($branches, 'getCutPointCommitPHID'); 134 + 135 + // Convert commit phid to a commit timestamp 136 + $ar = array(); 137 + foreach ($groups as $cut_phid => $group) { 138 + $handle = $handles[$cut_phid]; 139 + // Pack (timestamp, group-with-this-timestamp) pairs into $ar 140 + $ar[] = array( 141 + $handle->getTimestamp(), 142 + msort($group, 'getDateCreated') 143 + ); 144 + } 145 + 146 + $branches = array(); 147 + // Sort by timestamp, pull groups, and flatten into one big group 148 + foreach (ipull(isort($ar, 0), 1) as $group) { 149 + $branches = array_merge($branches, $group); 150 + } 151 + 152 + return array_reverse($branches); 153 + } 154 + 155 + }
+225
src/applications/releeph/view/branch/ReleephBranchBoxView.php
··· 1 + <?php 2 + 3 + final class ReleephBranchBoxView extends AphrontView { 4 + 5 + private $releephBranch; 6 + private $isLatest = false; 7 + private $isNamed = false; 8 + private $handles; 9 + 10 + public function setReleephBranch(ReleephBranch $br) { 11 + $this->releephBranch = $br; 12 + return $this; 13 + } 14 + 15 + // Primary highlighted branch 16 + public function setLatest() { 17 + $this->isLatest = true; 18 + return $this; 19 + } 20 + 21 + // Secondary highlighted branch(es) 22 + public function setNamed() { 23 + $this->isNamed = true; 24 + return $this; 25 + } 26 + 27 + public function setHandles($handles) { 28 + $this->handles = $handles; 29 + return $this; 30 + } 31 + 32 + public function render() { 33 + $br = $this->releephBranch; 34 + 35 + require_celerity_resource('releeph-branch'); 36 + return phutil_tag( 37 + 'div', 38 + array( 39 + 'class' => 'releeph-branch-box'. 40 + ($this->isNamed ? ' releeph-branch-box-named' : ''). 41 + ($this->isLatest ? ' releeph-branch-box-latest' : ''), 42 + ), 43 + array( 44 + $this->renderNames(), 45 + $this->renderDatesTable(), 46 + // "float: right" means the ordering here is weird 47 + $this->renderButtons(), 48 + $this->renderStatisticsTable(), 49 + phutil_tag( 50 + 'div', 51 + array( 52 + 'style' => 'clear:both;', 53 + ), 54 + ''))); 55 + } 56 + 57 + private function renderNames() { 58 + $br = $this->releephBranch; 59 + 60 + return phutil_tag( 61 + 'div', 62 + array( 63 + 'class' => 'names', 64 + ), 65 + array( 66 + phutil_tag( 67 + 'h1', 68 + array(), 69 + $br->getDisplayName()), 70 + phutil_tag( 71 + 'h2', 72 + array(), 73 + $br->getName()))); 74 + } 75 + 76 + private function renderDatesTable() { 77 + $br = $this->releephBranch; 78 + $branch_commit_handle = $this->handles[$br->getCutPointCommitPHID()]; 79 + 80 + $properties = array(); 81 + $properties['Created by'] = 82 + 83 + $cut_age = phabricator_format_relative_time( 84 + time() - $branch_commit_handle->getTimestamp()); 85 + 86 + return phutil_tag( 87 + 'div', 88 + array( 89 + 'class' => 'date-info', 90 + ), 91 + array( 92 + $this->handles[$br->getCreatedByUserPHID()]->renderLink(), 93 + phutil_tag('br'), 94 + phutil_tag( 95 + 'a', 96 + array( 97 + 'href' => $branch_commit_handle->getURI(), 98 + ), 99 + $cut_age.' old'))); 100 + } 101 + 102 + private function renderStatisticsTable() { 103 + $statistics = array(); 104 + 105 + $requests = $this->releephBranch->loadReleephRequests($this->getUser()); 106 + foreach ($requests as $request) { 107 + $status = $request->getStatus(); 108 + if (!isset($statistics[$status])) { 109 + $statistics[$status] = 0; 110 + } 111 + $statistics[$status]++; 112 + } 113 + 114 + static $col_groups = 3; 115 + 116 + $cells = array(); 117 + foreach ($statistics as $status => $count) { 118 + $description = ReleephRequest::getStatusDescriptionFor($status); 119 + $cells[] = phutil_tag('th', array(), $count); 120 + $cells[] = phutil_tag('td', array(), $description); 121 + } 122 + 123 + $rows = array(); 124 + while ($cells) { 125 + $row_cells = array(); 126 + for ($ii = 0; $ii < 2 * $col_groups; $ii++) { 127 + $row_cells[] = array_shift($cells); 128 + } 129 + $rows[] = phutil_tag('tr', array(), $row_cells); 130 + } 131 + 132 + if (!$rows) { 133 + $rows = hsprintf('<tr><th></th><td>%s</td></tr>', 'none'); 134 + } 135 + 136 + return phutil_tag( 137 + 'div', 138 + array( 139 + 'class' => 'request-statistics', 140 + ), 141 + phutil_tag( 142 + 'table', 143 + array(), 144 + $rows)); 145 + } 146 + 147 + private function renderButtons() { 148 + $br = $this->releephBranch; 149 + 150 + $buttons = array(); 151 + 152 + $buttons[] = phutil_tag( 153 + 'a', 154 + array( 155 + 'class' => 'small grey button', 156 + 'href' => $br->getURI(), 157 + ), 158 + 'View Requests'); 159 + 160 + $repo = $br->loadReleephProject()->loadPhabricatorRepository(); 161 + if (!$repo) { 162 + $buttons[] = phutil_tag( 163 + 'a', 164 + array( 165 + 'class' => 'small button disabled', 166 + ), 167 + "Diffusion \xE2\x86\x97"); 168 + } else { 169 + $diffusion_request = DiffusionRequest::newFromDictionary(array( 170 + 'repository' => $repo, 171 + )); 172 + $diffusion_branch_uri = $diffusion_request->generateURI(array( 173 + 'action' => 'branch', 174 + 'branch' => $br->getName(), 175 + )); 176 + $diffusion_button_class = 'small grey button'; 177 + 178 + $buttons[] = phutil_tag( 179 + 'a', 180 + array( 181 + 'class' => $diffusion_button_class, 182 + 'target' => '_blank', 183 + 'href' => $diffusion_branch_uri, 184 + ), 185 + "Diffusion \xE2\x86\x97"); 186 + } 187 + 188 + $releeph_project = $br->loadReleephProject(); 189 + if (!$releeph_project->getPushers() || 190 + $releeph_project->isPusher($this->user)) { 191 + 192 + $buttons[] = phutil_tag( 193 + 'a', 194 + array( 195 + 'class' => 'small blue button', 196 + 'href' => $br->getURI('edit/'), 197 + ), 198 + 'Edit'); 199 + 200 + if ($br->isActive()) { 201 + $button_text = "Close"; 202 + $href = $br->getURI('close/'); 203 + } else { 204 + $button_text = "Re-open"; 205 + $href = $br->getURI('re-open/'); 206 + } 207 + $buttons[] = javelin_tag( 208 + 'a', 209 + array( 210 + 'class' => 'small blue button', 211 + 'href' => $href, 212 + 'sigil' => 'workflow', 213 + ), 214 + $button_text); 215 + } 216 + 217 + return phutil_tag( 218 + 'div', 219 + array( 220 + 'class' => 'buttons', 221 + ), 222 + $buttons); 223 + } 224 + 225 + }
+60
src/applications/releeph/view/branch/ReleephBranchPreviewView.php
··· 1 + <?php 2 + 3 + final class ReleephBranchPreviewView extends AphrontFormControl { 4 + 5 + private $statics = array(); 6 + private $dynamics = array(); 7 + 8 + public function addControl($param_name, AphrontFormControl $control) { 9 + $celerity_id = celerity_generate_unique_node_id(); 10 + $control->setID($celerity_id); 11 + $this->dynamics[$param_name] = $celerity_id; 12 + return $this; 13 + } 14 + 15 + public function addStatic($param_name, $value) { 16 + $this->statics[$param_name] = $value; 17 + return $this; 18 + } 19 + 20 + public function getCustomControlClass() { 21 + require_celerity_resource('releeph-preview-branch'); 22 + return 'releeph-preview-branch'; 23 + } 24 + 25 + public function renderInput() { 26 + static $required_params = array( 27 + 'arcProjectID', 28 + 'projectName', 29 + 'isSymbolic', 30 + 'template', 31 + ); 32 + 33 + $all_params = array_merge($this->statics, $this->dynamics); 34 + foreach ($required_params as $param_name) { 35 + if (idx($all_params, $param_name) === null) { 36 + throw new Exception( 37 + "'{$param_name}' is not set as either a static or dynamic!"); 38 + } 39 + } 40 + 41 + $output_id = celerity_generate_unique_node_id(); 42 + 43 + Javelin::initBehavior('releeph-preview-branch', array( 44 + 'uri' => '/releeph/branch/preview/', 45 + 'outputID' => $output_id, 46 + 'params' => array( 47 + 'static' => $this->statics, 48 + 'dynamic' => $this->dynamics, 49 + ) 50 + )); 51 + 52 + return phutil_tag( 53 + 'div', 54 + array( 55 + 'id' => $output_id, 56 + ), 57 + ''); 58 + } 59 + 60 + }
+241
src/applications/releeph/view/branch/ReleephBranchTemplate.php
··· 1 + <?php 2 + 3 + final class ReleephBranchTemplate { 4 + 5 + const KEY = 'releeph.default-branch-template'; 6 + 7 + public static function getDefaultTemplate() { 8 + return PhabricatorEnv::getEnvConfig(self::KEY); 9 + } 10 + 11 + public static function getRequiredDefaultTemplate() { 12 + $template = self::getDefaultTemplate(); 13 + if (!$template) { 14 + throw new Exception(sprintf( 15 + "Config setting '%s' must be set, ". 16 + "or you must provide a branch-template for each project!", 17 + self::KEY)); 18 + } 19 + return $template; 20 + } 21 + 22 + public static function getFakeCommitHandleFor($arc_project_id) { 23 + $arc_project = id(new PhabricatorRepositoryArcanistProject()) 24 + ->load($arc_project_id); 25 + if (!$arc_project) { 26 + throw new Exception( 27 + "No Arc project found with id '{$arc_project_id}'!"); 28 + } 29 + 30 + $repository = $arc_project->loadRepository(); 31 + return id(new PhabricatorObjectHandle()) 32 + ->setName($repository->formatCommitName('100000000000')); 33 + } 34 + 35 + private $commitHandle; 36 + private $branchDate = null; 37 + private $projectName; 38 + private $isSymbolic; 39 + 40 + public function setCommitHandle(PhabricatorObjectHandle $handle) { 41 + $this->commitHandle = $handle; 42 + return $this; 43 + } 44 + 45 + public function setBranchDate($branch_date) { 46 + $this->branchDate = $branch_date; 47 + return $this; 48 + } 49 + 50 + public function setReleephProjectName($project_name) { 51 + $this->projectName = $project_name; 52 + return $this; 53 + } 54 + 55 + public function setSymbolic($is_symbolic) { 56 + $this->isSymbolic = $is_symbolic; 57 + return $this; 58 + } 59 + 60 + public function interpolate($template) { 61 + if (!$this->projectName) { 62 + return array('', array()); 63 + } 64 + 65 + list($name, $name_errors) = $this->interpolateInner( 66 + $template, 67 + $this->isSymbolic); 68 + 69 + if ($this->isSymbolic) { 70 + return array($name, $name_errors); 71 + } else { 72 + $validate_errors = $this->validateAsBranchName($name); 73 + $errors = array_merge($name_errors, $validate_errors); 74 + return array($name, $errors); 75 + } 76 + } 77 + 78 + public static function getHelpRemarkup() { 79 + return <<<EOTEXT 80 + 81 + ==== Interpolations ==== 82 + 83 + | Code | Meaning 84 + | ----- | ------- 85 + | `%P` | The name of your project, with spaces changed to "-". 86 + | `%p` | Like %P, but all lowercase. 87 + | `%Y` | The four digit year associated with the branch date. 88 + | `%m` | The two digit month. 89 + | `%d` | The two digit day. 90 + | `%v` | The handle of the commit where the branch was cut ("rXYZa4b3c2d1"). 91 + | `%V` | The abbreviated commit id where the branch was cut ("a4b3c2d1"). 92 + | `%..` | Any other sequence interpreted by `strftime()`. 93 + | `%%` | A literal percent sign. 94 + 95 + 96 + ==== Tips for Branch Templates ==== 97 + 98 + Use a directory to separate your release branches from other branches: 99 + 100 + lang=none 101 + releases/%Y-%M-%d-%v 102 + => releases/2012-30-16-rHERGE32cd512a52b7 103 + 104 + Include a second hierarchy if you share your repository with other projects: 105 + 106 + lang=none 107 + releases/%P/%p-release-%Y%m%d-%V 108 + => releases/Tintin/tintin-release-20121116-32cd512a52b7 109 + 110 + Keep your branch names simple, avoiding strange punctuation, most of which is 111 + forbidden or escaped anyway: 112 + 113 + lang=none, counterexample 114 + releases//..clown-releases..//`date --iso=seconds`-$(sudo halt) 115 + 116 + Include the date early in your template, in an order which sorts properly: 117 + 118 + lang=none 119 + releases/%Y%m%d-%v 120 + => releases/20121116-rHERGE32cd512a52b7 (good!) 121 + 122 + releases/%V-%m.%d.%Y 123 + => releases/32cd512a52b7-11.16.2012 (awful!) 124 + 125 + 126 + EOTEXT 127 + ; 128 + } 129 + 130 + /* 131 + * xsprintf() would be useful here, but that's for formatting concrete lists 132 + * of things in a certain way... 133 + * 134 + * animal_printf('%A %A %A', $dog1, $dog2, $dog3); 135 + * 136 + * ...rather than interpolating percent-control-strings like strftime does. 137 + */ 138 + private function interpolateInner($template, $is_symbolic) { 139 + $name = $template; 140 + $errors = array(); 141 + 142 + $safe_project_name = str_replace(' ', '-', $this->projectName); 143 + $short_commit_id = last( 144 + preg_split('/r[A-Z]+/', $this->commitHandle->getName())); 145 + 146 + $interpolations = array(); 147 + for ($ii = 0; $ii < strlen($name); $ii++) { 148 + $char = substr($name, $ii, 1); 149 + $prev = null; 150 + if ($ii > 0) { 151 + $prev = substr($name, $ii - 1, 1); 152 + } 153 + $next = substr($name, $ii + 1, 1); 154 + if ($next && $char == '%' && $prev != '%') { 155 + $interpolations[$ii] = $next; 156 + } 157 + } 158 + 159 + $variable_interpolations = array(); 160 + 161 + $reverse_interpolations = $interpolations; 162 + krsort($reverse_interpolations); 163 + 164 + if ($this->branchDate) { 165 + $branch_date = $this->branchDate; 166 + } else { 167 + $branch_date = $this->commitHandle->getTimestamp(); 168 + } 169 + 170 + foreach ($reverse_interpolations as $position => $code) { 171 + $replacement = null; 172 + switch ($code) { 173 + case 'v': 174 + $replacement = $this->commitHandle->getName(); 175 + $is_variable = true; 176 + break; 177 + 178 + case 'V': 179 + $replacement = $short_commit_id; 180 + $is_variable = true; 181 + break; 182 + 183 + case 'P': 184 + $replacement = $safe_project_name; 185 + $is_variable = false; 186 + break; 187 + 188 + case 'p': 189 + $replacement = strtolower($safe_project_name); 190 + $is_variable = false; 191 + break; 192 + 193 + default: 194 + // Format anything else using strftime() 195 + $replacement = strftime("%{$code}", $branch_date); 196 + $is_variable = true; 197 + break; 198 + } 199 + 200 + if ($is_variable) { 201 + $variable_interpolations[] = $code; 202 + } 203 + $name = substr_replace($name, $replacement, $position, 2); 204 + } 205 + 206 + if (!$is_symbolic && !$variable_interpolations) { 207 + $errors[] = "Include additional interpolations that aren't static!"; 208 + } 209 + 210 + return array($name, $errors); 211 + } 212 + 213 + private function validateAsBranchName($name) { 214 + $errors = array(); 215 + 216 + if (preg_match('{^/}', $name) || preg_match('{/$}', $name)) { 217 + $errors[] = "Branches cannot begin or end with '/'"; 218 + } 219 + 220 + if (preg_match('{//+}', $name)) { 221 + $errors[] = "Branches cannot contain multiple consective '/'"; 222 + } 223 + 224 + $parts = array_filter(explode('/', $name)); 225 + foreach ($parts as $index => $part) { 226 + $part_error = null; 227 + if (preg_match('{^\.}', $part) || preg_match('{\.$}', $part)) { 228 + $errors[] = "Path components cannot begin or end with '.'"; 229 + } elseif (preg_match('{^(?!\w)}', $part)) { 230 + $errors[] = "Path components must begin with an alphanumeric"; 231 + } elseif (!preg_match('{^\w ([\w-_%\.]* [\w-_%])?$}x', $part)) { 232 + $errors[] = 233 + "Path components may only contain alphanumerics ". 234 + "or '-', '_', or '.'"; 235 + } 236 + } 237 + 238 + return $errors; 239 + } 240 + 241 + }
+102
src/applications/releeph/view/project/list/ReleephActiveProjectListView.php
··· 1 + <?php 2 + 3 + final class ReleephActiveProjectListView extends AphrontView { 4 + 5 + private $releephProjects; 6 + 7 + public function setReleephProjects(array $releeph_projects) { 8 + $this->releephProjects = $releeph_projects; 9 + return $this; 10 + } 11 + 12 + public function render() { 13 + $rows = array(); 14 + foreach ($this->releephProjects as $releeph_project) { 15 + $project_uri = $releeph_project->getURI(); 16 + 17 + $name_link = phutil_tag( 18 + 'a', 19 + array( 20 + 'href' => $project_uri, 21 + 'style' => 'font-weight: bold;', 22 + ), 23 + $releeph_project->getName()); 24 + 25 + $edit_button = phutil_tag( 26 + 'a', 27 + array( 28 + 'href' => $releeph_project->getURI('edit/'), 29 + 'class' => 'small grey button', 30 + ), 31 + 'Edit'); 32 + 33 + $deactivate_button = javelin_tag( 34 + 'a', 35 + array( 36 + 'href' => $releeph_project->getURI('action/deactivate/'), 37 + 'class' => 'small grey button', 38 + 'sigil' => 'workflow', 39 + ), 40 + 'Remove'); 41 + 42 + $arc_project = $releeph_project->loadArcanistProject(); 43 + if ($arc_project) { 44 + $arc_project_name = $arc_project->getName(); 45 + } else { 46 + $arc_project_name = phutil_tag( 47 + 'i', 48 + array(), 49 + 'Deleted Arcanist Project'); 50 + } 51 + 52 + $repo = $releeph_project->loadPhabricatorRepository(); 53 + 54 + if ($repo) { 55 + $vcs_type = 56 + PhabricatorRepositoryType::getNameForRepositoryType( 57 + $repo->getVersionControlSystem()); 58 + 59 + $rows[] = array( 60 + $name_link, 61 + $repo->getName(), 62 + $arc_project_name, 63 + $vcs_type, 64 + $edit_button, 65 + $deactivate_button, 66 + ); 67 + } else { 68 + $rows[] = array( 69 + $name_link, 70 + phutil_tag('i', array(), 'Deleted Repository'), 71 + $arc_project_name, 72 + null, 73 + null, 74 + $deactivate_button, 75 + ); 76 + } 77 + } 78 + 79 + $table = new AphrontTableView($rows); 80 + 81 + $table->setHeaders(array( 82 + 'Name', 83 + 'Repository', 84 + 'Arcanist Project', 85 + 'Type', 86 + '', 87 + '' 88 + )); 89 + 90 + $table->setColumnClasses(array( 91 + null, 92 + null, 93 + 'wide', 94 + null, 95 + 'action', 96 + 'action' 97 + )); 98 + 99 + return $table->render(); 100 + } 101 + 102 + }
+112
src/applications/releeph/view/project/list/ReleephInactiveProjectListView.php
··· 1 + <?php 2 + 3 + final class ReleephInactiveProjectListView extends AphrontView { 4 + 5 + private $releephProjects; 6 + 7 + public function setReleephProjects(array $releeph_projects) { 8 + $this->releephProjects = $releeph_projects; 9 + return $this; 10 + } 11 + 12 + public function render() { 13 + $rows = array(); 14 + 15 + $phids = array(); 16 + foreach ($this->releephProjects as $releeph_project) { 17 + $phids[] = $releeph_project->getCreatedByUserPHID(); 18 + if ($phid = $releeph_project->getDetail('last_deactivated_user')) { 19 + $phids[] = $phid; 20 + } 21 + } 22 + 23 + $handles = id(new PhabricatorObjectHandleData($phids)) 24 + ->setViewer($this->getUser()) 25 + ->loadHandles(); 26 + 27 + foreach ($this->releephProjects as $releeph_project) { 28 + $repository = $releeph_project->loadPhabricatorRepository(); 29 + 30 + if (!$repository) { 31 + // Ignore projects referring to repositories that have been deleted. 32 + continue; 33 + } 34 + 35 + $activate_link = javelin_tag( 36 + 'a', 37 + array( 38 + 'href' => $releeph_project->getURI('action/activate/'), 39 + 'class' => 'small grey button', 40 + 'sigil' => 'workflow', 41 + ), 42 + 'Revive'); 43 + 44 + $delete_link = javelin_tag( 45 + 'a', 46 + array( 47 + 'href' => $releeph_project->getURI('action/delete/'), 48 + 'class' => 'small grey button', 49 + 'sigil' => 'workflow', 50 + ), 51 + 'Delete'); 52 + 53 + $rows[] = array( 54 + $releeph_project->getName(), 55 + $repository->getName(), 56 + $this->renderCreationInfo($releeph_project, $handles), 57 + $this->renderDeletionInfo($releeph_project, $handles), 58 + $activate_link, 59 + $delete_link, 60 + ); 61 + } 62 + 63 + $table = new AphrontTableView($rows); 64 + 65 + $table->setHeaders(array( 66 + 'Name', 67 + 'Repository', 68 + 'Created', 69 + 'Deleted', 70 + '', 71 + '', 72 + )); 73 + 74 + $table->setColumnClasses(array( 75 + null, 76 + null, 77 + null, 78 + 'wide', 79 + 'action', 80 + 'action', 81 + )); 82 + 83 + return $table->render(); 84 + } 85 + 86 + private function renderCreationInfo($releeph_project, $handles) { 87 + $creator = $handles[$releeph_project->getCreatedByUserPHID()]; 88 + $when = $releeph_project->getDateCreated(); 89 + return hsprintf( 90 + '%s by %s', 91 + phabricator_relative_date($when, $this->user), 92 + $creator->getName()); 93 + } 94 + 95 + private function renderDeletionInfo($releeph_project, $handles) { 96 + $deleted_on = $releeph_project->getDetail('last_deactivated_time'); 97 + 98 + $deleted_by_name = null; 99 + $deleted_by_phid = $releeph_project->getDetail('last_deactivated_user'); 100 + if ($deleted_by_phid) { 101 + $deleted_by_name = $handles[$deleted_by_phid]->getName(); 102 + } else { 103 + $deleted_by_name = 'unknown'; 104 + } 105 + 106 + return hsprintf( 107 + '%s by %s', 108 + phabricator_relative_date($deleted_on, $this->user), 109 + $deleted_by_name); 110 + } 111 + 112 + }
+104
src/applications/releeph/view/request/ReleephRequestIntentsView.php
··· 1 + <?php 2 + 3 + final class ReleephRequestIntentsView extends AphrontView { 4 + 5 + private $releephRequest; 6 + private $releephProject; 7 + 8 + public function setReleephRequest(ReleephRequest $rq) { 9 + $this->releephRequest = $rq; 10 + return $this; 11 + } 12 + 13 + public function setReleephProject(ReleephProject $rp) { 14 + $this->releephProject = $rp; 15 + return $this; 16 + } 17 + 18 + public function render() { 19 + require_celerity_resource('releeph-intents'); 20 + 21 + return phutil_tag( 22 + 'div', 23 + array( 24 + 'class' => 'releeph-intents', 25 + ), 26 + array( 27 + $this->renderIntentList(ReleephRequest::INTENT_WANT), 28 + $this->renderIntentList(ReleephRequest::INTENT_PASS) 29 + )); 30 + } 31 + 32 + private function renderIntentList($render_intent) { 33 + if (!$this->releephProject) { 34 + throw new Exception("Must call setReleephProject() first!"); 35 + } 36 + 37 + $project = $this->releephProject; 38 + $request = $this->releephRequest; 39 + $handles = $request->getHandles(); 40 + 41 + $is_want = $render_intent == ReleephRequest::INTENT_WANT; 42 + $should = $request->shouldBeInBranch(); 43 + 44 + $pusher_links = array(); 45 + $user_links = array(); 46 + 47 + $intents = $request->getUserIntents(); 48 + foreach ($intents as $user_phid => $user_intent) { 49 + if ($user_intent == $render_intent) { 50 + $is_pusher = $project->isPusherPHID($user_phid); 51 + 52 + if ($is_pusher) { 53 + $pusher_links[] = phutil_tag( 54 + 'span', 55 + array( 56 + 'class' => 'pusher' 57 + ), 58 + $handles[$user_phid]->renderLink()); 59 + } else { 60 + $class = 'bystander'; 61 + if ($request->getRequestUserPHID() == $user_phid) { 62 + $class = 'requestor'; 63 + } 64 + $user_links[] = phutil_tag( 65 + 'span', 66 + array( 67 + 'class' => $class, 68 + ), 69 + $handles[$user_phid]->renderLink()); 70 + } 71 + } 72 + } 73 + 74 + // Don't render anything 75 + if (!$pusher_links && !$user_links) { 76 + return null; 77 + } 78 + 79 + $links = array_merge($pusher_links, $user_links); 80 + if ($links) { 81 + $markup = $links; 82 + } else { 83 + $markup = array('&nbsp;'); 84 + } 85 + 86 + // Stick an arrow up front 87 + $arrow_class = 'arrow '.$render_intent; 88 + array_unshift($markup, phutil_tag( 89 + 'div', 90 + array( 91 + 'class' => $arrow_class, 92 + ), 93 + '')); 94 + 95 + return phutil_tag( 96 + 'div', 97 + array( 98 + 'class' => 'intents', 99 + ), 100 + $markup); 101 + } 102 + 103 + 104 + }
+53
src/applications/releeph/view/request/ReleephRequestStatusView.php
··· 1 + <?php 2 + 3 + final class ReleephRequestStatusView extends AphrontView { 4 + 5 + private $releephRequest; 6 + 7 + public function setReleephRequest(ReleephRequest $rq) { 8 + $this->releephRequest = $rq; 9 + return $this; 10 + } 11 + 12 + public function render() { 13 + require_celerity_resource('releeph-status'); 14 + 15 + $request = $this->releephRequest; 16 + $status = $request->getStatus(); 17 + $pick_status = $request->getPickStatus(); 18 + 19 + $description = ReleephRequest::getStatusDescriptionFor($status); 20 + 21 + $warning = null; 22 + 23 + if ($status == ReleephRequest::STATUS_NEEDS_PICK) { 24 + if ($pick_status == ReleephRequest::PICK_FAILED) { 25 + $warning = 'Last pick failed!'; 26 + } 27 + } elseif ($status == ReleephRequest::STATUS_NEEDS_REVERT) { 28 + if ($pick_status == ReleephRequest::REVERT_FAILED) { 29 + $warning = 'Last revert failed!'; 30 + } 31 + } 32 + 33 + return phutil_tag( 34 + 'div', 35 + array( 36 + 'class' => 'releeph-status', 37 + ), 38 + array( 39 + phutil_tag( 40 + 'div', 41 + array( 42 + 'class' => 'description', 43 + ), 44 + $description), 45 + phutil_tag( 46 + 'div', 47 + array( 48 + 'class' => 'warning', 49 + ), 50 + $warning))); 51 + } 52 + 53 + }
+58
src/applications/releeph/view/request/ReleephRequestTypeaheadControl.php
··· 1 + <?php 2 + 3 + final class ReleephRequestTypeaheadControl extends AphrontFormControl { 4 + 5 + private $repo; 6 + private $startTime; 7 + 8 + public function setRepo(PhabricatorRepository $repo) { 9 + $this->repo = $repo; 10 + return $this; 11 + } 12 + 13 + public function setStartTime($epoch) { 14 + $this->startTime = $epoch; 15 + return $this; 16 + } 17 + 18 + public function getCustomControlClass() { 19 + return 'releeph-request-typeahead'; 20 + } 21 + 22 + public function renderInput() { 23 + $id = celerity_generate_unique_node_id(); 24 + 25 + $div = phutil_tag( 26 + 'div', 27 + array( 28 + 'style' => 'position: relative;', 29 + 'id' => $id, 30 + ), 31 + phutil_tag( 32 + 'input', 33 + array( 34 + 'autocomplete' => 'off', 35 + 'type' => 'text', 36 + 'name' => $this->getName(), 37 + ), 38 + '')); 39 + 40 + require_celerity_resource('releeph-request-typeahead-css'); 41 + 42 + Javelin::initBehavior('releeph-request-typeahead', array( 43 + 'id' => $id, 44 + 'src' => '/releeph/request/typeahead/', 45 + 'placeholder' => 'Type a commit id or first line of commit message...', 46 + 'value' => $this->getValue(), 47 + 'aux' => array( 48 + 'repo' => $this->repo->getID(), 49 + 'callsign' => $this->repo->getCallsign(), 50 + 'since' => $this->startTime, 51 + 'limit' => 16, 52 + ) 53 + )); 54 + 55 + return $div; 56 + } 57 + 58 + }
+113
src/applications/releeph/view/request/header/ReleephRequestHeaderListView.php
··· 1 + <?php 2 + 3 + final class ReleephRequestHeaderListView 4 + extends AphrontView { 5 + 6 + private $originType; 7 + private $releephProject; 8 + private $releephBranch; 9 + private $releephRequests; 10 + private $aphrontRequest; 11 + private $reload = false; 12 + 13 + private $errors = array(); 14 + 15 + public function setOriginType($origin) { 16 + $this->originType = $origin; 17 + return $this; 18 + } 19 + 20 + public function setReleephProject(ReleephProject $rp) { 21 + $this->releephProject = $rp; 22 + return $this; 23 + } 24 + 25 + public function setReleephBranch(ReleephBranch $rb) { 26 + $this->releephBranch = $rb; 27 + return $this; 28 + } 29 + 30 + public function setReleephRequests(array $requests) { 31 + assert_instances_of($requests, 'ReleephRequest'); 32 + $this->releephRequests = $requests; 33 + return $this; 34 + } 35 + 36 + public function setAphrontRequest(AphrontRequest $request) { 37 + $this->aphrontRequest = $request; 38 + return $this; 39 + } 40 + 41 + public function setReloadOnStateChange($bool) { 42 + $this->reload = $bool; 43 + return $this; 44 + } 45 + 46 + public function render() { 47 + $views = $this->renderInner(); 48 + require_celerity_resource('phabricator-notification-css'); 49 + Javelin::initBehavior('releeph-request-state-change', array( 50 + 'reload' => $this->reload, 51 + )); 52 + 53 + $error_view = null; 54 + if ($this->errors) { 55 + $error_view = id(new AphrontErrorView()) 56 + ->setTitle('Bulk load errors') 57 + ->setSeverity(AphrontErrorView::SEVERITY_WARNING) 58 + ->setErrors($this->errors) 59 + ->render(); 60 + } 61 + 62 + $list = phutil_tag( 63 + 'div', 64 + array( 65 + 'data-sigil' => 'releeph-request-header-list', 66 + ), 67 + $views); 68 + 69 + return $this->renderSingleView(array( 70 + $error_view, 71 + $list)); 72 + } 73 + 74 + /** 75 + * Required for generating markup for ReleephRequestActionController. 76 + * 77 + * That controller just needs the markup, and doesn't need to start the 78 + * javelin behavior. 79 + */ 80 + public function renderInner() { 81 + $selector = $this->releephProject->getReleephFieldSelector(); 82 + $fields = $selector->getFieldSpecifications(); 83 + foreach ($fields as $field) { 84 + $field 85 + ->setReleephProject($this->releephProject) 86 + ->setReleephBranch($this->releephBranch) 87 + ->setUser($this->user); 88 + try { 89 + $field->bulkLoad($this->releephRequests); 90 + } catch (Exception $ex) { 91 + $this->errors[] = $ex; 92 + } 93 + } 94 + 95 + $field_groups = $selector->arrangeFieldsForHeaderView($fields); 96 + 97 + $views = array(); 98 + foreach ($this->releephRequests as $releeph_request) { 99 + $views[] = id(new ReleephRequestHeaderView()) 100 + ->setUser($this->user) 101 + ->setAphrontRequest($this->aphrontRequest) 102 + ->setOriginType($this->originType) 103 + ->setReleephProject($this->releephProject) 104 + ->setReleephBranch($this->releephBranch) 105 + ->setReleephRequest($releeph_request) 106 + ->setReleephFieldGroups($field_groups) 107 + ->render(); 108 + } 109 + 110 + return $views; 111 + } 112 + 113 + }
+334
src/applications/releeph/view/request/header/ReleephRequestHeaderView.php
··· 1 + <?php 2 + 3 + final class ReleephRequestHeaderView extends AphrontView { 4 + 5 + const THROW_PARAM = '__releeph_throw'; 6 + 7 + private $aphrontRequest; 8 + private $releephRequest; 9 + private $releephBranch; 10 + private $releephProject; 11 + private $originType; 12 + private $fieldGroups; 13 + 14 + public function setAphrontRequest(AphrontRequest $request) { 15 + $this->aphrontRequest = $request; 16 + return $this; 17 + } 18 + 19 + public function setReleephProject(ReleephProject $rp) { 20 + $this->releephProject = $rp; 21 + return $this; 22 + } 23 + 24 + public function setReleephBranch(ReleephBranch $rb) { 25 + $this->releephBranch = $rb; 26 + return $this; 27 + } 28 + 29 + public function setReleephRequest(ReleephRequest $rr) { 30 + $this->releephRequest = $rr; 31 + return $this; 32 + } 33 + 34 + public function setOriginType($origin) { 35 + // For the Edit controller 36 + $this->originType = $origin; 37 + return $this; 38 + } 39 + 40 + public function setReleephFieldGroups(array $field_groups) { 41 + $this->fieldGroups = $field_groups; 42 + return $this; 43 + } 44 + 45 + protected function getOrigin() { 46 + return $this->originType; 47 + } 48 + 49 + public function render() { 50 + require_celerity_resource('releeph-core'); 51 + $all_properties_table = $this->renderFields(); 52 + 53 + require_celerity_resource('releeph-colors'); 54 + $status = $this->releephRequest->getStatus(); 55 + $rr_div_class = 56 + 'releeph-request-header '. 57 + 'releeph-request-header-border '. 58 + 'releeph-border-color-'.ReleephRequest::getStatusClassSuffixFor($status); 59 + 60 + $hidden_link = phutil_tag( 61 + 'a', 62 + array( 63 + 'href' => '/RQ'.$this->releephRequest->getID(), 64 + 'target' => '_blank', 65 + 'data-sigil' => 'hidden-link', 66 + ), 67 + ''); 68 + 69 + $focus_char = phutil_tag( 70 + 'div', 71 + array( 72 + 'class' => 'focus-char', 73 + 'data-sigil' => 'focus-char', 74 + ), 75 + "\xE2\x98\x86"); 76 + 77 + $rr_div = phutil_tag( 78 + 'div', 79 + array( 80 + 'data-sigil' => 'releeph-request-header', 81 + 'class' => $rr_div_class, 82 + ), 83 + array( 84 + phutil_tag( 85 + 'div', 86 + array(), 87 + array( 88 + phutil_tag( 89 + 'h1', 90 + array(), 91 + array( 92 + $focus_char, 93 + $this->renderTitleLink(), 94 + $hidden_link 95 + )), 96 + $all_properties_table, 97 + )), 98 + phutil_tag( 99 + 'div', 100 + array( 101 + 'class' => 'button-divider', 102 + ), 103 + $this->renderActionButtonsTable()))); 104 + 105 + return $rr_div; 106 + } 107 + 108 + private function renderFields() { 109 + $field_row_groups = $this->fieldGroups; 110 + 111 + $trs = array(); 112 + foreach ($field_row_groups as $field_column_group) { 113 + $tds = array(); 114 + foreach ($field_column_group as $side => $fields) { 115 + $rows = array(); 116 + foreach ($fields as $field) { 117 + $rows[] = $this->renderOneField($field); 118 + } 119 + $pane = phutil_tag( 120 + 'table', 121 + array( 122 + 'class' => 'fields', 123 + ), 124 + $rows); 125 + $tds[] = phutil_tag( 126 + 'td', 127 + array( 128 + 'class' => 'side '.$side, 129 + ), 130 + $pane); 131 + } 132 + $trs[] = phutil_tag( 133 + 'tr', 134 + array(), 135 + $tds); 136 + } 137 + 138 + return phutil_tag( 139 + 'table', 140 + array( 141 + 'class' => 'panes', 142 + ), 143 + $trs); 144 + } 145 + 146 + private function renderOneField(ReleephFieldSpecification $field) { 147 + $field 148 + ->setUser($this->user) 149 + ->setReleephProject($this->releephProject) 150 + ->setReleephBranch($this->releephBranch) 151 + ->setReleephRequest($this->releephRequest); 152 + 153 + $label = $field->renderLabelForHeaderView(); 154 + try { 155 + $value = $field->renderValueForHeaderView(); 156 + } catch (Exception $ex) { 157 + if ($this->aphrontRequest->getInt(self::THROW_PARAM)) { 158 + throw $ex; 159 + } else { 160 + $value = $this->renderExceptionIcon($ex); 161 + } 162 + } 163 + 164 + if ($value) { 165 + if (!$label) { 166 + return phutil_tag( 167 + 'tr', 168 + array(), 169 + phutil_tag('td', array('colspan' => 2), $value)); 170 + } else { 171 + return phutil_tag( 172 + 'tr', 173 + array(), 174 + array( 175 + phutil_tag('th', array(), $label), 176 + phutil_tag('td', array(), $value))); 177 + } 178 + } 179 + } 180 + 181 + private function renderExceptionIcon(Exception $ex) { 182 + Javelin::initBehavior('phabricator-tooltips'); 183 + require_celerity_resource('aphront-tooltip-css'); 184 + $throw_uri = $this 185 + ->aphrontRequest 186 + ->getRequestURI() 187 + ->setQueryParam(self::THROW_PARAM, 1); 188 + 189 + $message = $ex->getMessage(); 190 + if (!$message) { 191 + $message = get_class($ex).' with no message.'; 192 + } 193 + 194 + return javelin_tag( 195 + 'a', 196 + array( 197 + 'class' => 'releeph-field-error', 198 + 'sigil' => 'has-tooltip', 199 + 'meta' => array( 200 + 'tip' => $message, 201 + 'size' => 400, 202 + 'align' => 'E', 203 + ), 204 + 'href' => $throw_uri, 205 + ), 206 + '!!!'); 207 + } 208 + 209 + private function renderTitleLink() { 210 + $rq_id = $this->releephRequest->getID(); 211 + $summary = $this->releephRequest->getSummaryForDisplay(); 212 + return phutil_tag( 213 + 'a', 214 + array( 215 + 'href' => '/RQ'.$rq_id, 216 + ), 217 + hsprintf( 218 + 'RQ%d: %s', 219 + $rq_id, 220 + $summary)); 221 + } 222 + 223 + private function renderActionButtonsTable() { 224 + $left_buttons = array(); 225 + $right_buttons = array(); 226 + 227 + $user_phid = $this->user->getPHID(); 228 + $is_pusher = $this->releephProject->isPusherPHID($user_phid); 229 + $is_requestor = $this->releephRequest->getRequestUserPHID() === $user_phid; 230 + 231 + $current_intent = idx( 232 + $this->releephRequest->getUserIntents(), 233 + $this->user->getPHID()); 234 + 235 + if ($is_pusher) { 236 + $left_buttons[] = $this->renderIntentButton(true, 'Approve', 'green'); 237 + $left_buttons[] = $this->renderIntentButton(false, 'Reject'); 238 + } else { 239 + if ($is_requestor) { 240 + $right_buttons[] = $this->renderIntentButton(true, 'Request'); 241 + $right_buttons[] = $this->renderIntentButton(false, 'Remove'); 242 + } else { 243 + $right_buttons[] = $this->renderIntentButton(true, 'Want'); 244 + $right_buttons[] = $this->renderIntentButton(false, 'Pass'); 245 + } 246 + } 247 + 248 + // Allow the pusher to mark a request as manually picked or reverted. 249 + if ($is_pusher || $is_requestor) { 250 + if ($this->releephRequest->getInBranch()) { 251 + $left_buttons[] = $this->renderActionButton( 252 + 'Mark Manually Reverted', 253 + 'mark-manually-reverted'); 254 + } else { 255 + $left_buttons[] = $this->renderActionButton( 256 + 'Mark Manually Picked', 257 + 'mark-manually-picked'); 258 + } 259 + } 260 + 261 + $right_buttons[] = phutil_tag( 262 + 'a', 263 + array( 264 + 'href' => '/releeph/request/edit/'.$this->releephRequest->getID(). 265 + '?origin='.$this->originType, 266 + 'class' => 'small blue button', 267 + ), 268 + 'Edit'); 269 + 270 + if (!$left_buttons && !$right_buttons) { 271 + return; 272 + } 273 + 274 + $cells = array(); 275 + foreach ($left_buttons as $button) { 276 + $cells[] = phutil_tag('td', array('align' => 'left'), $button); 277 + } 278 + $cells[] = phutil_tag('td', array('class' => 'wide'), ''); 279 + foreach ($right_buttons as $button) { 280 + $cells[] = phutil_tag('td', array('align' => 'right'), $button); 281 + } 282 + 283 + $table = phutil_tag( 284 + 'table', 285 + array( 286 + 'class' => 'buttons', 287 + ), 288 + phutil_tag( 289 + 'tr', 290 + array(), 291 + $cells)); 292 + 293 + return $table; 294 + } 295 + 296 + private function renderIntentButton($want, $name, $class = null) { 297 + $current_intent = idx( 298 + $this->releephRequest->getUserIntents(), 299 + $this->user->getPHID()); 300 + 301 + if ($current_intent) { 302 + // If this is a "want" button, and they already want it, disable the 303 + // button (and vice versa for the "pass" case.) 304 + if (($want && $current_intent == ReleephRequest::INTENT_WANT) || 305 + (!$want && $current_intent == ReleephRequest::INTENT_PASS)) { 306 + 307 + $class .= ' disabled'; 308 + } 309 + } 310 + 311 + $action = $want ? 'want' : 'pass'; 312 + return $this->renderActionButton($name, $action, $class); 313 + } 314 + 315 + private function renderActionButton($name, $action, $class=null) { 316 + $attributes = array( 317 + 'class' => 'small button '.$class, 318 + 'sigil' => 'releeph-request-state-change '.$action, 319 + 'meta' => null, 320 + ); 321 + 322 + if ($class != 'disabled') { 323 + // NB the trailing slash on $uri is critical, otherwise the URI will 324 + // redirect to one with a slash, which will turn our GET into a POST. 325 + $attributes['meta'] = sprintf( 326 + '/releeph/request/action/%s/%d/', 327 + $action, 328 + $this->releephRequest->getID()); 329 + } 330 + 331 + return javelin_tag('a', $attributes, $name); 332 + } 333 + 334 + }
+266
src/applications/releeph/view/requestevent/ReleephRequestEventListView.php
··· 1 + <?php 2 + 3 + final class ReleephRequestEventListView extends AphrontView { 4 + 5 + private $events; 6 + private $handles; 7 + 8 + public function setEvents(array $events) { 9 + assert_instances_of($events, 'ReleephRequestEvent'); 10 + $this->events = $events; 11 + return $this; 12 + } 13 + 14 + public function setHandles(array $handles) { 15 + assert_instances_of($handles, 'PhabricatorObjectHandle'); 16 + $this->handles = $handles; 17 + return $this; 18 + } 19 + 20 + public function render() { 21 + $views = array(); 22 + 23 + $discovered_commits = array(); 24 + foreach ($this->events as $event) { 25 + $commit_id = $event->getDetail('newCommitIdentifier'); 26 + switch ($event->getType()) { 27 + case ReleephRequestEvent::TYPE_DISCOVERY: 28 + $discovered_commits[$commit_id] = true; 29 + break; 30 + } 31 + } 32 + 33 + $markup_engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); 34 + $markup_engine->setConfig('viewer', $this->getUser()); 35 + 36 + foreach ($this->events as $event) { 37 + $description = $this->describeEvent($event); 38 + if (!$description) { 39 + continue; 40 + } 41 + 42 + if ($event->getType() === ReleephRequestEvent::TYPE_COMMIT) { 43 + $commit_id = $event->getDetail('newCommitIdentifier'); 44 + if (idx($discovered_commits, $commit_id)) { 45 + continue; 46 + } 47 + } 48 + 49 + $actor_handle = $this->handles[$event->getActorPHID()]; 50 + $description = $this->describeEvent($event); 51 + $action = phutil_tag( 52 + 'div', 53 + array(), 54 + array( 55 + $actor_handle->renderLink(), 56 + ' ', 57 + $description)); 58 + 59 + $view = id(new PhabricatorTransactionView()) 60 + ->setUser($this->user) 61 + ->setImageURI($actor_handle->getImageURI()) 62 + ->setEpoch($event->getDateCreated()) 63 + ->setActions(array($action)) 64 + ->addClass($this->getTransactionClass($event)); 65 + 66 + $comment = $this->getEventComment($event); 67 + if ($comment) { 68 + $markup = phutil_tag( 69 + 'div', 70 + array( 71 + 'class' => 'phabricator-remarkup', 72 + ), 73 + phutil_safe_html( 74 + $markup_engine->markupText($comment))); 75 + $view->appendChild($markup); 76 + } 77 + 78 + $views[] = $view; 79 + } 80 + 81 + return phutil_tag( 82 + 'div', 83 + array( 84 + 'class' => 'releeph-request-event-list', 85 + ), 86 + $views); 87 + } 88 + 89 + public function renderForEmail() { 90 + $items = array(); 91 + foreach ($this->events as $event) { 92 + $description = $this->describeEvent($event); 93 + if (!$description) { 94 + continue; 95 + } 96 + $actor = $this->handles[$event->getActorPHID()]->getName(); 97 + $items[] = $actor.' '.$description; 98 + 99 + $comment = $this->getEventComment($event); 100 + if ($comment) { 101 + $items[] = preg_replace('/^/m', ' ', $comment); 102 + } 103 + } 104 + 105 + return implode("\n\n", $items); 106 + } 107 + 108 + private function describeEvent(ReleephRequestEvent $event) { 109 + $type = $event->getType(); 110 + 111 + switch ($type) { 112 + case ReleephRequestEvent::TYPE_CREATE: 113 + return "created this request."; 114 + break; 115 + 116 + case ReleephRequestEvent::TYPE_STATUS: 117 + $status = $event->getStatusAfter(); 118 + return sprintf( 119 + "updated status to %s.", 120 + ReleephRequest::getStatusDescriptionFor($status)); 121 + break; 122 + 123 + case ReleephRequestEvent::TYPE_USER_INTENT: 124 + $intent = $event->getDetail('newIntent'); 125 + $was_pusher = $event->getDetail('wasPusher'); 126 + if ($intent == ReleephRequest::INTENT_WANT) { 127 + if ($was_pusher) { 128 + $verb = "approved"; 129 + } else { 130 + $verb = "wanted"; 131 + } 132 + } else { 133 + if ($was_pusher) { 134 + $verb = "rejected"; 135 + } else { 136 + $verb = "passed on"; 137 + } 138 + } 139 + return "{$verb} this request."; 140 + break; 141 + 142 + case ReleephRequestEvent::TYPE_PICK_STATUS: 143 + $pick_status = $event->getDetail('newPickStatus'); 144 + switch ($pick_status) { 145 + case ReleephRequest::PICK_FAILED: 146 + return "found a conflict when picking."; 147 + break; 148 + 149 + case ReleephRequest::REVERT_FAILED: 150 + return "found a conflict when reverting."; 151 + break; 152 + 153 + case ReleephRequest::PICK_OK: 154 + case ReleephRequest::REVERT_OK: 155 + // (nothing) 156 + break; 157 + 158 + default: 159 + return "changed pick-status to {$pick_status}."; 160 + break; 161 + } 162 + break; 163 + 164 + case ReleephRequestEvent::TYPE_MANUAL_ACTION: 165 + $action = $event->getDetail('action'); 166 + return "claimed to have manually {$action}ed this request."; 167 + break; 168 + 169 + case ReleephRequestEvent::TYPE_COMMIT: 170 + $action = $event->getDetail('action'); 171 + if ($action) { 172 + return "{$action}ed this request."; 173 + } else { 174 + return "did something with this request."; 175 + } 176 + break; 177 + 178 + case ReleephRequestEvent::TYPE_DISCOVERY: 179 + $action = $event->getDetail('action'); 180 + if ($action) { 181 + return "{$action}ed this request."; 182 + } else { 183 + // It's unlikely we'll have action-less TYPE_DISCOVERY events, but I 184 + // used this during testing and I guess it's a useful safety net. 185 + return "discovered this request in the branch."; 186 + } 187 + break; 188 + 189 + case ReleephRequestEvent::TYPE_COMMENT: 190 + return "commented on this request."; 191 + break; 192 + 193 + default: 194 + return "did event of type {$type}."; 195 + break; 196 + } 197 + } 198 + 199 + private function getEventComment(ReleephRequestEvent $event) { 200 + switch ($event->getType()) { 201 + case ReleephRequestEvent::TYPE_CREATE: 202 + $commit_phid = $event->getDetail('commitPHID'); 203 + return sprintf( 204 + "Commit %s was requested.", 205 + $this->handles[$commit_phid]->getName()); 206 + break; 207 + 208 + case ReleephRequestEvent::TYPE_STATUS: 209 + case ReleephRequestEvent::TYPE_USER_INTENT: 210 + case ReleephRequestEvent::TYPE_PICK_STATUS: 211 + case ReleephRequestEvent::TYPE_MANUAL_ACTION: 212 + // no comment! 213 + break; 214 + 215 + case ReleephRequestEvent::TYPE_COMMIT: 216 + return sprintf( 217 + "Closed by commit %s.", 218 + $event->getDetail('newCommitIdentifier')); 219 + break; 220 + 221 + case ReleephRequestEvent::TYPE_DISCOVERY: 222 + $author_phid = $event->getDetail('authorPHID'); 223 + $commit_phid = $event->getDetail('newCommitPHID'); 224 + if ($author_phid && $author_phid != $event->getActorPHID()) { 225 + return sprintf( 226 + "Closed by commit %s (with author set to @%s).", 227 + $this->handles[$commit_phid]->getName(), 228 + $this->handles[$author_phid]->getName()); 229 + } else { 230 + return sprintf( 231 + 'Closed by commit %s.', 232 + $this->handles[$commit_phid]->getName()); 233 + } 234 + break; 235 + 236 + case ReleephRequestEvent::TYPE_COMMENT: 237 + return $event->getComment(); 238 + break; 239 + } 240 + } 241 + 242 + private function getTransactionClass($event) { 243 + switch ($event->getType()) { 244 + case ReleephRequestEvent::TYPE_COMMIT: 245 + case ReleephRequestEvent::TYPE_DISCOVERY: 246 + $action = $event->getDetail('action'); 247 + if ($action == 'pick') { 248 + return 'releeph-border-color-picked'; 249 + } else { 250 + return 'releeph-border-color-abandoned'; 251 + } 252 + break; 253 + 254 + case ReleephRequestEvent::TYPE_COMMENT: 255 + return 'releeph-border-color-comment'; 256 + break; 257 + 258 + default: 259 + $status_after = $event->getStatusAfter(); 260 + $class_suffix = ReleephRequest::getStatusClassSuffixFor($status_after); 261 + return ' releeph-border-color-'.$class_suffix; 262 + break; 263 + } 264 + } 265 + 266 + }
+9
src/applications/releeph/view/user/ReleephDefaultUserView.php
··· 1 + <?php 2 + 3 + final class ReleephDefaultUserView extends ReleephUserView { 4 + 5 + public function render() { 6 + return $this->getHandle()->renderLink(); 7 + } 8 + 9 + }
+74
src/applications/releeph/view/user/ReleephUserView.php
··· 1 + <?php 2 + 3 + abstract class ReleephUserView extends AphrontView { 4 + 5 + /** 6 + * This function should bulk load everything you need to render all the given 7 + * user phids. 8 + * 9 + * Many parts of Releeph load users for rendering. Accordingly, this 10 + * function will be called multiple times for each part of the UI that 11 + * renders users, so you should accumulate your results on each call. 12 + * 13 + * You should also implement render() (from AphrontView) to render each 14 + * user's PHID. 15 + */ 16 + protected function loadInner(array $phids) { 17 + // This is a hook! 18 + } 19 + 20 + final public static function getNewInstance() { 21 + $key = 'releeph.user-view'; 22 + $class = PhabricatorEnv::getEnvConfig($key); 23 + return newv($class, array()); 24 + } 25 + 26 + private static $handles = array(); 27 + private static $seen = array(); 28 + 29 + final public function load(array $phids) { 30 + $todo = array(); 31 + 32 + foreach ($phids as $key => $phid) { 33 + if (!idx(self::$seen, $phid)) { 34 + $todo[$key] = $phid; 35 + self::$seen[$phid] = true; 36 + } 37 + } 38 + 39 + if ($todo) { 40 + self::$handles = array_merge( 41 + self::$handles, 42 + id(new PhabricatorObjectHandleData($todo)) 43 + ->setViewer($this->getUser()) 44 + ->loadHandles()); 45 + $this->loadInner($todo); 46 + } 47 + } 48 + 49 + private $phid; 50 + private $releephProject; 51 + 52 + final public function setRenderUserPHID($phid) { 53 + $this->phid = $phid; 54 + return $this; 55 + } 56 + 57 + final public function setReleephProject(ReleephProject $project) { 58 + $this->releephProject = $project; 59 + return $this; 60 + } 61 + 62 + final protected function getRenderUserPHID() { 63 + return $this->phid; 64 + } 65 + 66 + final protected function getReleephProject() { 67 + return $this->releephProject; 68 + } 69 + 70 + final protected function getHandle() { 71 + return self::$handles[$this->phid]; 72 + } 73 + 74 + }
+6
src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
··· 32 32 'COMMIT(S)' => array('COMMIT', 'COMMITS'), 33 33 34 34 '%d line(s)' => array('%d line', '%d lines'), 35 + '%d path(s)' => array('%d path', '%d paths'), 36 + '%d diff(s)' => array('%d diff', '%d diffs'), 35 37 36 38 'added %d commit(s): %s' => array( 37 39 'added commit: %2$s', ··· 285 287 '%s added inline comments.', 286 288 ), 287 289 ), 290 + 291 + '%d comment(s)' => array('%d comment', '%d comments'), 292 + '%d rejection(s)' => array('%d rejection', '%d rejections'), 293 + '%d update(s)' => array('%d update', '%d updates'), 288 294 289 295 ); 290 296 }
+8
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 171 171 'type' => 'db', 172 172 'name' => 'token', 173 173 ), 174 + 'db.releeph' => array( 175 + 'type' => 'db', 176 + 'name' => 'releeph', 177 + ), 174 178 '0000.legacy.sql' => array( 175 179 'type' => 'sql', 176 180 'name' => $this->getPatchPath('0000.legacy.sql'), ··· 1164 1168 '20130304.lintauthor.sql' => array( 1165 1169 'type' => 'sql', 1166 1170 'name' => $this->getPatchPath('20130304.lintauthor.sql'), 1171 + ), 1172 + 'releeph.sql' => array( 1173 + 'type' => 'sql', 1174 + 'name' => $this->getPatchPath('releeph.sql'), 1167 1175 ), 1168 1176 ); 1169 1177 }
+79
webroot/rsrc/css/application/releeph/releeph-branch.css
··· 1 + /** 2 + * @provides releeph-branch 3 + */ 4 + 5 + .releeph-branch-box { 6 + margin-bottom: .5em; 7 + padding: .5em .5em .5em; 8 + 9 + border: 2px solid #d5d5d5; 10 + /*border-top-color: #D5D5D5; 11 + border-right-color: #BBB; 12 + border-bottom-color: #A4A4A4; 13 + border-left-color: #BBB;*/ 14 + 15 + background: #bbb; 16 + } 17 + 18 + /* Types of branch */ 19 + 20 + .releeph-branch-box-named { 21 + background: #ddd; 22 + } 23 + 24 + .releeph-branch-box-latest { 25 + background: #ffd; 26 + } 27 + 28 + /* Branch symbolic name and full name */ 29 + 30 + .releeph-branch-box .names { 31 + width: 25em; 32 + float: left; 33 + margin-bottom: 1em; 34 + } 35 + 36 + .releeph-branch-box .names h1 { 37 + font-size: 125%; 38 + padding: 0px; 39 + } 40 + 41 + .releeph-branch-box .names h2 { 42 + font-weight: normal; 43 + font-size: 85%; 44 + } 45 + 46 + /* Date info */ 47 + 48 + .releeph-branch-box .date-info { 49 + width: 10%; 50 + float: left; 51 + color: #555; 52 + margin-bottom: .3em; 53 + } 54 + 55 + /* Statistics table */ 56 + 57 + .releeph-branch-box .request-statistics { 58 + float: right; 59 + padding-right: 2em; 60 + font-size: 85%; 61 + } 62 + 63 + .releeph-branch-box .request-statistics th { 64 + width: 1em; 65 + text-align: right; 66 + padding-right: .4em; 67 + padding-left: .4em; 68 + } 69 + 70 + .releeph-branch-box .request-statistics td { 71 + white-space: nowrap; 72 + font-style: italic; 73 + } 74 + 75 + /* Buttons */ 76 + 77 + .releeph-branch-box .buttons { 78 + float: right; 79 + }
+35
webroot/rsrc/css/application/releeph/releeph-colors.css
··· 1 + /** 2 + * @provides releeph-colors 3 + */ 4 + 5 + .releeph-border-color-failed { 6 + border-color: #d2d; 7 + } 8 + 9 + .releeph-border-color-requested { 10 + border-color: #ddd; 11 + } 12 + 13 + .releeph-border-color-comment { 14 + border-color: #ddd; 15 + } 16 + 17 + .releeph-border-color-needs-pick { 18 + border-color: #096; 19 + } 20 + 21 + .releeph-border-color-rejected { 22 + border-color: #d00; 23 + } 24 + 25 + .releeph-border-color-needs-revert { 26 + border-color: #d00; 27 + } 28 + 29 + .releeph-border-color-abandoned { 30 + border-color: #222; 31 + } 32 + 33 + .releeph-border-color-picked { 34 + border-color: #069; 35 + }
+199
webroot/rsrc/css/application/releeph/releeph-core.css
··· 1 + /** 2 + * @provides releeph-core 3 + */ 4 + 5 + .releeph-request-header { 6 + margin: .5em 2em 3em; 7 + 8 + /** 9 + * Copied from the old .differential-panel, present in commit 10 + * f04d8ab1a747dc9719d378d9286088b677ce224c 11 + * 12 + * (As is the <h1> code below) 13 + */ 14 + max-width: 1120px; 15 + border: 1px solid #666622; 16 + background: #efefdf; 17 + padding: 15px 20px; 18 + font-size: 13px; 19 + } 20 + 21 + .releeph-request-header h1 { 22 + width: 100%; 23 + border-bottom: 1px solid #aaaa99; 24 + padding-bottom: 8px; 25 + margin-bottom: 8px; 26 + position: relative; 27 + } 28 + 29 + .releeph-request-header .focus-char { 30 + left: -10px; 31 + display: none; 32 + float: left; 33 + position: absolute; 34 + top: 0px; 35 + left: -1em; 36 + 37 + font-weight: bold; 38 + 39 + color: #880; 40 + font-family: "Hiragino Kaku Gothic Pro", "Osaka", "Zapf Dingbats"; 41 + } 42 + 43 + .releeph-request-header.focus .focus-char { 44 + display: block; 45 + } 46 + 47 + .releeph-request-header-border { 48 + border-width: 1px 10px 1px; 49 + border-color: #ddd; 50 + } 51 + 52 + 53 + /* Laying out properties / fields */ 54 + 55 + .releeph-request-header table.panes { 56 + width: 100%; 57 + } 58 + 59 + .releeph-request-header table.panes td.side { 60 + width: 50%; 61 + max-width: 1em; 62 + } 63 + 64 + .releeph-request-header table.panes td.side.left { 65 + padding-right: 20px; 66 + border-right: 3px solid #bbb; 67 + } 68 + 69 + .releeph-request-header table.panes td.side.right { 70 + padding-left: 20px; 71 + } 72 + 73 + .releeph-request-header table.panes td.side table.fields { 74 + width: 100%; 75 + } 76 + 77 + .releeph-request-header table.panes td.side table.fields tr { 78 + vertical-align: middle; 79 + } 80 + 81 + .releeph-request-header table.panes td.side table.fields th { 82 + font-weight: bold; 83 + text-align: right; 84 + padding-right: 1em; 85 + white-space: nowrap; 86 + } 87 + 88 + .releeph-request-header table.panes td.side table.fields td { 89 + width: 100%; /* wide! */ 90 + max-width: 1em; 91 + } 92 + 93 + 94 + /* Buttons */ 95 + 96 + .releeph-request-header .button-divider { 97 + clear: both; 98 + margin-top: 1.5em; 99 + border-top: 1px solid #bbb; 100 + } 101 + 102 + .releeph-request-header .buttons { 103 + width: 100%; 104 + } 105 + 106 + .releeph-request-header .buttons tr { 107 + padding: 1em; 108 + margin: 3em; 109 + } 110 + 111 + .releeph-request-header .buttons td { 112 + padding: 1em .5em 0.2em; 113 + } 114 + 115 + .releeph-request-header .buttons td.wide { 116 + width: 100%; 117 + } 118 + 119 + /* Colors: match differential colors */ 120 + 121 + .releeph-request-comment { 122 + border-color: #ddd; 123 + } 124 + 125 + .releeph-request-comment-pusher { 126 + background: #8DEE8D; 127 + border-color: #096; 128 + } 129 + 130 + .releeph-request-comment-pusher div { 131 + background: #8DEE8D; 132 + } 133 + 134 + /* The diff size bar */ 135 + 136 + .releeph-request-header .diff-bar { 137 + border: 0px; 138 + } 139 + 140 + .releeph-request-header .diff-bar div { 141 + width: 100px; 142 + border: 1px solid; 143 + border-top-color: #A4A4A4; 144 + border-right-color: #BBB; 145 + border-bottom-color: #D5D5D5; 146 + border-left-color: #BBB; 147 + background: white; 148 + float: left; 149 + margin-right: 1em; 150 + } 151 + 152 + .releeph-request-header .diff-bar div div { 153 + height: 10px; 154 + } 155 + 156 + .releeph-request-header .diff-bar span { 157 + color: #555; 158 + } 159 + 160 + /* Rendering pick / commit errors, etc. */ 161 + 162 + .releeph-request-pick-failed-event h1:before { 163 + content: '\2014 '; 164 + } 165 + 166 + .releeph-request-pick-failed-event h1:after { 167 + content: ' \2014'; 168 + } 169 + 170 + .releeph-request-pick-failed-event h1 { 171 + padding: 3px 10px 3px; 172 + margin-bottom: 0.5em; 173 + background: #ffb; 174 + font-size: small; 175 + } 176 + 177 + .releeph-request-pick-failed-event div { 178 + font-family: monospace; 179 + margin-bottom: 1.5em; 180 + padding-left: 1em; 181 + width: 70em; 182 + } 183 + 184 + /* History view of request */ 185 + 186 + .releeph-request-event-list { 187 + margin: .5em 2em .5em; 188 + } 189 + 190 + 191 + /* Shorten long header-text */ 192 + 193 + .releeph-header-text-truncated { 194 + width: 100%; 195 + float: left; 196 + white-space: nowrap; 197 + overflow: hidden; 198 + text-overflow: ellipsis; 199 + }
+37
webroot/rsrc/css/application/releeph/releeph-intents.css
··· 1 + /** 2 + * @provides releeph-intents 3 + */ 4 + 5 + .releeph-intents .intents { 6 + clear: left; 7 + width: 100%; 8 + margin-top: 3px; 9 + } 10 + 11 + .releeph-intents .arrow { 12 + float: left; 13 + clear: left; 14 + margin-right: 0.4em; 15 + padding: 8px; 16 + background: transparent 0 0 no-repeat; 17 + } 18 + 19 + .releeph-intents .arrow.want { 20 + background-image: url('/rsrc/custom/image/icon/tango/go-next.png'); 21 + } 22 + 23 + .releeph-intents .arrow.pass { 24 + background-image: url('/rsrc/custom/image/icon/tango/go-previous-gray.png'); 25 + } 26 + 27 + .releeph-intents a { 28 + margin-right: 0.4em; 29 + } 30 + 31 + .releeph-intents .pusher { 32 + font-weight: bold; 33 + } 34 + 35 + .releeph-intents .requestor { 36 + font-weight: normal; 37 + }
+29
webroot/rsrc/css/application/releeph/releeph-preview-branch.css
··· 1 + /** 2 + * @provides releeph-preview-branch 3 + */ 4 + 5 + .releeph-preview-branch { 6 + min-height: 4em; 7 + position: relative; 8 + } 9 + 10 + .releeph-preview-branch .error { 11 + padding-left: 22px; 12 + background-repeat: no-repeat; 13 + background-size: 16px auto; 14 + background-image: url(/rsrc/custom/image/releeph/releeph_warning.png); 15 + float: left; 16 + position: absolute; 17 + top: 2.5em; 18 + } 19 + 20 + .releeph-preview-branch .name { 21 + clear: both; 22 + float: left; 23 + position: absolute; 24 + font-family: monospace; 25 + font-size: 9pt !important; 26 + background: white; 27 + top: 0.7em; 28 + padding: 2px; 29 + }
+25
webroot/rsrc/css/application/releeph/releeph-project.css
··· 1 + /** 2 + * @provides releeph-project 3 + */ 4 + 5 + /** 6 + * ...from aphront-transaction.css 7 + */ 8 + 9 + .releeph-pusher { 10 + background: 2px 2px no-repeat; 11 + margin-top: 1em; 12 + margin-bottom: 1.25em; 13 + margin-right: 1em; 14 + min-height: 50px; 15 + padding: 2px 0px; 16 + 17 + background-color: white; 18 + border: 2px solid gray; 19 + float: left; 20 + } 21 + 22 + .releeph-pusher-body { 23 + margin-left: 54px; 24 + padding: 1em; 25 + }
+17
webroot/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css
··· 1 + /** 2 + * @provides releeph-request-differential-create-dialog 3 + */ 4 + 5 + .releeph-request-differential-create-dialog h1 { 6 + color: gray; 7 + font-style: italic; 8 + font-size: 16px; 9 + margin-top: 0.8em; 10 + } 11 + 12 + .releeph-request-differential-create-dialog a { 13 + font-weight: bold; 14 + margin-left: 2em; 15 + display: block; 16 + margin-top: 1em; 17 + }
+27
webroot/rsrc/css/application/releeph/releeph-request-typeahead.css
··· 1 + /** 2 + * @provides releeph-request-typeahead-css 3 + */ 4 + 5 + .releeph-request-typeahead .commit-id { 6 + color: #aaf; /* blue... */ 7 + font-family: monospace; 8 + font-size: 100%; 9 + display: block; 10 + float: left; 11 + } 12 + 13 + .releeph-request-typeahead .author-info { 14 + color: #080; /* ...and green, for search results! */ 15 + text-align: right; 16 + display: block; 17 + float: right; 18 + padding-left: 1em; 19 + } 20 + 21 + .releeph-request-typeahead .focused .author-info { 22 + color: #8b8; 23 + } 24 + 25 + .releeph-request-typeahead .summary { 26 + clear: both; 27 + }
+26
webroot/rsrc/css/application/releeph/releeph-status.css
··· 1 + /** 2 + * @provides releeph-status 3 + */ 4 + 5 + .releeph-status .description { 6 + background: #d3d3d3; 7 + padding: 2px 6px 3px; 8 + margin-right: 4px; 9 + margin-bottom: 5px; 10 + display: block; 11 + float: left; 12 + border-radius: 8px; 13 + -moz-border-radius: 8px; 14 + -webkit-border-radius: 8px; 15 + text-decoration: none; 16 + } 17 + 18 + .releeph-status .warning { 19 + margin-top: 2px; 20 + margin-left: 0.8em; 21 + float: left; 22 + padding-left: 22px; 23 + background-repeat: no-repeat; 24 + background-size: 16px auto; 25 + background-image: url(/rsrc/custom/image/releeph/releeph_warning.png); 26 + }
+49
webroot/rsrc/js/application/releeph/releeph-preview-branch.js
··· 1 + /** 2 + * @provides javelin-behavior-releeph-preview-branch 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-stratcom 6 + * javelin-uri 7 + * javelin-util 8 + */ 9 + 10 + JX.behavior('releeph-preview-branch', function(config) { 11 + 12 + var uri = JX.$U(config.uri); 13 + for (param_name in config.params.static) { 14 + var value = config.params.static[param_name]; 15 + uri.setQueryParam(param_name, value); 16 + } 17 + 18 + var output = JX.$(config.outputID); 19 + 20 + var dynamics = config.params.dynamic; 21 + 22 + function renderPreview() { 23 + for (param_name in dynamics) { 24 + var node_id = dynamics[param_name]; 25 + var input = JX.$(node_id); 26 + uri.setQueryParam(param_name, input.value); 27 + } 28 + var request = new JX.Request(uri, function(response) { 29 + JX.DOM.setContent(output, JX.$H(response.markup)); 30 + }); 31 + request.send(); 32 + } 33 + 34 + renderPreview(); 35 + 36 + for (ii in dynamics) { 37 + var node_id = dynamics[ii]; 38 + var input = JX.$(node_id); 39 + JX.DOM.listen( 40 + input, 41 + ['keyup', 'click', 'change'], 42 + null, 43 + function(e) { 44 + renderPreview(); 45 + } 46 + ); 47 + } 48 + 49 + });
+145
webroot/rsrc/js/application/releeph/releeph-request-state-change.js
··· 1 + /** 2 + * @provides javelin-behavior-releeph-request-state-change 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-stratcom 6 + * javelin-util 7 + * phabricator-keyboard-shortcut 8 + * phabricator-notification 9 + */ 10 + 11 + JX.behavior('releeph-request-state-change', function(config) { 12 + var root = JX.DOM.find(document, 'div', 'releeph-request-header-list'); 13 + 14 + function getRequestHeaderNodes() { 15 + return JX.DOM.scry(root, 'div', 'releeph-request-header'); 16 + } 17 + 18 + /** 19 + * Keyboard navigation 20 + */ 21 + var keynav_cursor = -1; 22 + var notification = new JX.Notification(); 23 + 24 + function keynavJump(manager, delta) { 25 + // Calculate this everytime, because the DOM changes. 26 + var headers = getRequestHeaderNodes(); 27 + keynav_cursor += delta; 28 + 29 + if (keynav_cursor < 0) { 30 + keynav_cursor = -1; 31 + window.scrollTo(0); 32 + keynavMarkup(); 33 + return; 34 + } 35 + 36 + if (keynav_cursor >= headers.length) { 37 + keynav_cursor = headers.length - 1; 38 + } 39 + 40 + var focus = headers[keynav_cursor]; 41 + manager.scrollTo(focus); 42 + 43 + keynavMarkup(); 44 + } 45 + 46 + function keynavMarkup() { 47 + var headers = getRequestHeaderNodes(); 48 + for (ii in headers) { 49 + JX.DOM.alterClass(headers[ii], 'focus', ii == keynav_cursor); 50 + } 51 + } 52 + 53 + function keynavAction(manager, action_name) { 54 + var headers = getRequestHeaderNodes(); 55 + var header = headers[keynav_cursor]; 56 + 57 + if (keynav_cursor < 0) { 58 + return; 59 + } 60 + 61 + var sigil = action_name; 62 + var button = JX.DOM.find(header, 'a', sigil); 63 + if (button) { 64 + button.click(); 65 + } 66 + } 67 + 68 + function keynavNavigateToRequestPage() { 69 + var headers = getRequestHeaderNodes(); 70 + var header = headers[keynav_cursor]; 71 + JX.DOM.find(header, 'a', 'hidden-link').click(); 72 + } 73 + 74 + new JX.KeyboardShortcut('j', 'Jump to next request.') 75 + .setHandler(function(manager) { 76 + keynavJump(manager, +1); 77 + }) 78 + .register(); 79 + 80 + new JX.KeyboardShortcut('k', 'Jump to previous request.') 81 + .setHandler(function(manager) { 82 + keynavJump(manager, -1); 83 + }) 84 + .register(); 85 + 86 + new JX.KeyboardShortcut('a', 'Approve the selected request.') 87 + .setHandler(function(manager) { 88 + keynavAction(manager, 'want'); 89 + }) 90 + .register(); 91 + 92 + new JX.KeyboardShortcut('r', 'Reject the selected request.') 93 + .setHandler(function(manager) { 94 + keynavAction(manager, 'pass'); 95 + }) 96 + .register(); 97 + 98 + new JX.KeyboardShortcut('g', "Open selected request's page in a new tab.") 99 + .setHandler(function(manager) { 100 + keynavNavigateToRequestPage(); 101 + }) 102 + .register(); 103 + 104 + 105 + /** 106 + * AJAXy state changes for request buttons. 107 + */ 108 + function request_action(node, url) { 109 + var request = new JX.Request(url, function(response) { 110 + if (config.reload) { 111 + window.location.reload(); 112 + } else { 113 + var markup = JX.$H(response.markup); 114 + JX.DOM.replace(node, markup); 115 + keynavMarkup(); 116 + } 117 + }); 118 + 119 + request.send(); 120 + } 121 + 122 + JX.Stratcom.listen( 123 + 'click', 124 + 'releeph-request-state-change', 125 + function(e) { 126 + var button = e.getNode('releeph-request-state-change'); 127 + var node = e.getNode('releeph-request-header'); 128 + var url = e.getNodeData('releeph-request-state-change'); 129 + 130 + // If this button has no action, or we've already responded to the first 131 + // click... 132 + if (!url || button.disabled) { 133 + return; 134 + } 135 + 136 + // There's a race condition here though :( 137 + 138 + JX.DOM.alterClass(button, 'disabled', true); 139 + button.disabled = true; 140 + 141 + e.prevent(); 142 + request_action(node, url); 143 + } 144 + ); 145 + });
+84
webroot/rsrc/js/application/releeph/releeph-request-typeahead.js
··· 1 + /** 2 + * @provides javelin-behavior-releeph-request-typeahead 3 + * @requires javelin-behavior 4 + * javelin-util 5 + * javelin-dom 6 + * javelin-typeahead 7 + * javelin-tokenizer 8 + * javelin-typeahead-preloaded-source 9 + * javelin-typeahead-ondemand-source 10 + * javelin-dom 11 + * javelin-stratcom 12 + * javelin-util 13 + */ 14 + 15 + JX.behavior('releeph-request-typeahead', function(config) { 16 + var root = JX.$(config.id); 17 + var datasource = new JX.TypeaheadOnDemandSource(config.src); 18 + var callsign = config.aux.callsign; 19 + 20 + datasource.setAuxiliaryData(config.aux); 21 + 22 + datasource.setTransformer( 23 + function(object) { 24 + var full_commit_id = object[0]; 25 + var short_commit_id = object[1]; 26 + var author = object[2]; 27 + var ago = object[3]; 28 + var summary = object[4]; 29 + 30 + var callsign_commit_id = 'r' + callsign + short_commit_id; 31 + 32 + var box = 33 + JX.$N( 34 + 'div', 35 + {}, 36 + [ 37 + JX.$N( 38 + 'div', 39 + { className: 'commit-id' }, 40 + callsign_commit_id 41 + ), 42 + JX.$N( 43 + 'div', 44 + { className: 'author-info' }, 45 + ago + ' ago by ' + author 46 + ), 47 + JX.$N( 48 + 'div', 49 + { className: 'summary' }, 50 + summary 51 + ), 52 + ] 53 + ); 54 + 55 + return { 56 + name: callsign_commit_id, 57 + tokenizable: callsign_commit_id + ' '+ short_commit_id + ' ' + summary, 58 + display: box, 59 + uri: null, 60 + id: full_commit_id 61 + }; 62 + }); 63 + 64 + /** 65 + * The default normalizer removes useful control characters that would help 66 + * out search. For example, I was just trying to search for a commit with 67 + * the string "a_file" in the message, which was normalized to "afile". 68 + */ 69 + datasource.setNormalizer(function(query) { 70 + return query; 71 + }); 72 + 73 + datasource.setMaximumResultCount(config.aux.limit); 74 + 75 + var typeahead = new JX.Typeahead(root); 76 + typeahead.setDatasource(datasource); 77 + 78 + var placeholder = config.value || config.placeholder; 79 + if (placeholder) { 80 + typeahead.setPlaceholder(placeholder); 81 + } 82 + 83 + typeahead.start(); 84 + });