diff --git a/externals/octicons/LICENSE b/externals/octicons/LICENSE
new file mode 100644
index 0000000000..4cf2020ce7
--- /dev/null
+++ b/externals/octicons/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2012-2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/externals/octicons/README.md b/externals/octicons/README.md
new file mode 100644
index 0000000000..84edd84dce
--- /dev/null
+++ b/externals/octicons/README.md
@@ -0,0 +1,194 @@
+# GitHub Octicons
+
+[![NPM version](https://img.shields.io/npm/v/octicons.svg)](https://www.npmjs.org/package/octicons)
+[![Build Status](https://travis-ci.org/primer/octicons.svg?branch=master)](https://travis-ci.org/primer/octicons)
+
+> Octicons are a scalable set of icons handcrafted with <3 by GitHub.
+
+## Install
+
+**NOTE:** The compiled files are located in `/build/`. This directory is located in the published npm package. Which means you can access it when you `npm install octicons`. You can also build this directory by following the [building octicons directions](#building-octicons). The files in the `/lib/` directory are the raw source files and are not compiled or optimized.
+
+#### NPM
+
+This repository is distributed with [npm][npm]. After [installing npm][install-npm], you can install `octicons` with this command.
+
+```
+$ npm install --save octicons
+```
+
+## Usage
+
+For all the usages, we recommend using the CSS located in `./build/octicons.css`. This is some simple CSS to normalize the icons and inherit colors.
+
+### Spritesheet
+
+With a [SVG sprite icon system](https://css-tricks.com/svg-sprites-use-better-icon-fonts/) you can include the sprite sheet located `./build/sprite.octicons.svg` after you [build the icons](#building-octicons) or from the npm package. There is a demo of how to use the spritesheet in the build directory also.
+
+### Node
+
+After installing `npm install octicons` you can access the icons like this.
+
+```js
+var octicons = require("octicons")
+octicons.alert
+// { keywords: [ 'warning', 'triangle', 'exclamation', 'point' ],
+// path: ' ',
+// height: '16',
+// width: '16',
+// symbol: 'alert',
+// options:
+// { version: '1.1',
+// width: '16',
+// height: '16',
+// viewBox: '0 0 16 16',
+// class: 'octicon octicon-alert',
+// 'aria-hidden': 'true' },
+// toSVG: [Function] }
+```
+
+There will be a key for every icon, with `keywords` and `svg`.
+
+#### `octicons.alert.symbol`
+
+Returns the string of the symbol name
+
+```js
+octicons.x.symbol
+// "x"
+```
+
+#### `octicons.person.path`
+
+Path returns the string representation of the path of the icon.
+
+```js
+octicons.x.path
+//
+```
+
+#### `octicons.issue.options`
+
+This is a json object of all the `options` that will be added to the output tag.
+
+```js
+octicons.x.options
+// { version: '1.1', width: '12', height: '16', viewBox: '0 0 12 16', class: 'octicon octicon-x', 'aria-hidden': 'true' }
+```
+
+#### `octicons.alert.width`
+
+Width is the icon's true width. Based on the svg view box width. _Note, this doesn't change if you scale it up with size options, it only is the natural width of the icon_
+
+#### `octicons.alert.height`
+
+Height is the icon's true height. Based on the svg view box height. _Note, this doesn't change if you scale it up with size options, it only is the natural height of the icon_
+
+#### `keywords`
+
+Returns an array of keywords for the icon. The data [comes from the octicons repository](https://github.com/primer/octicons/blob/master/lib/data.json). Consider contributing more aliases for the icons.
+
+```js
+octicons.x.keywords
+// ["remove", "close", "delete"]
+```
+
+#### `octicons.alert.toSVG()`
+
+Returns a string of the svg tag
+
+```js
+octicons.x.toSVG()
+//
+```
+
+The `.toSVG()` method accepts an optional `options` object. This is used to add CSS classnames, a11y options, and sizing.
+
+##### class
+
+Add more CSS classes to the `` tag.
+
+```js
+octicons.x.toSVG({ "class": "close" })
+//
+```
+
+##### aria-label
+
+Add accessibility `aria-label` to the icon.
+
+```js
+octicons.x.toSVG({ "aria-label": "Close the window" })
+//
+```
+
+##### width & height
+
+Size the SVG icon larger using `width` & `height` independently or together.
+
+```js
+octicons.x.toSVG({ "width": 45 })
+//
+```
+
+#### `octicons.alert.toSVGUse()`
+
+Returns a string of the svg tag with the `` tag, for use with the spritesheet located in the /build/ directory.
+
+```js
+octicons.x.toSVGUse()
+//
+```
+
+### Ruby
+
+If your environment is Ruby on Rails, we have a [octicons_helper](https://github.com/primer/octicons_helper) gem available that renders SVG in your page. The octicons_helper uses the [octicons_gem](https://github.com/primer/octicons_gem) to do the computing and reading of the SVG files.
+
+### Jekyll
+
+For jekyll, there's a [jekyll-octicons](https://github.com/primer/jekyll-octicons) plugin available. This works exactly like the octicons_helper.
+
+## Changing, adding, or deleting icons
+
+1. Open the [Sketch document][sketch-document] in `/lib/`. Each icon exists as an artboard within our master Sketch document. If you’re adding an icon, duplicate one of the artboards and add your shapes to it. Be sure to give your artboard a name that makes sense.
+2. Once you’re happy with your icon set, choose File > Export…
+3. Choose all the artboards you’d like to export and then press “Export”
+4. Export to `/lib/svg/`
+
+You’ll next need to build your Octicons.
+
+## Building Octicons
+
+All the files you need will be in the `/build/` directory already, but if you’ve made changes to the `/lib/` directory and need to regenerate, follow these steps:
+
+1. Open the Octicons directory in Terminal
+2. `npm install` to install all dependencies for the project.
+3. Run the command `npm run build`. This will run the grunt task to build the SVGs, placing them in the `/build/` directory.
+
+## Publishing
+
+If you have access to publish this repository, these are the steps to publishing. If you need access, contact [#design-systems](https://github.slack.com/archives/design-systems).
+
+1. Update the [CHANGELOG.md](./CHANGELOG.md) with relevant version number and any updates made to the repository.
+2. `npm version ` Run [npm version](https://docs.npmjs.com/cli/version) inputing the relevant version type. The versioning is [semver](http://semver.org/), so version appropriately based on what has changed.
+3. `npm publish` This will publish the new version to npmjs.org
+4. `git push && git push --tags` Push all these changes to origin.
+
+## License
+
+(c) 2012-2016 GitHub, Inc.
+
+When using the GitHub logos, be sure to follow the [GitHub logo guidelines](https://github.com/logos).
+
+_SVG License:_ [SIL OFL 1.1](http://scripts.sil.org/OFL)
+Applies to all SVG files
+
+_Code License:_ [MIT](./LICENSE)
+Applies to all other files
+
+[primer]: https://github.com/primer/primer
+[docs]: http://primercss.io/
+[npm]: https://www.npmjs.com/
+[install-npm]: https://docs.npmjs.com/getting-started/installing-node
+[sass]: http://sass-lang.com/
+[sketch-document]: https://github.com/primer/octicons/blob/master/lib/octicons-master.sketch
diff --git a/resources/builtin/repo/building.png b/resources/builtin/repo/building.png
new file mode 100644
index 0000000000..d3ab581fe8
Binary files /dev/null and b/resources/builtin/repo/building.png differ
diff --git a/resources/builtin/repo/cloud.png b/resources/builtin/repo/cloud.png
new file mode 100644
index 0000000000..51a40d6cbe
Binary files /dev/null and b/resources/builtin/repo/cloud.png differ
diff --git a/resources/builtin/repo/code.png b/resources/builtin/repo/code.png
new file mode 100644
index 0000000000..aeedf4a6ce
Binary files /dev/null and b/resources/builtin/repo/code.png differ
diff --git a/resources/builtin/repo/commit.png b/resources/builtin/repo/commit.png
new file mode 100644
index 0000000000..e6d251f095
Binary files /dev/null and b/resources/builtin/repo/commit.png differ
diff --git a/resources/builtin/repo/database.png b/resources/builtin/repo/database.png
new file mode 100644
index 0000000000..4c9ec543fa
Binary files /dev/null and b/resources/builtin/repo/database.png differ
diff --git a/resources/builtin/repo/desktop.png b/resources/builtin/repo/desktop.png
new file mode 100644
index 0000000000..f75cc54e43
Binary files /dev/null and b/resources/builtin/repo/desktop.png differ
diff --git a/resources/builtin/repo/gears.png b/resources/builtin/repo/gears.png
new file mode 100644
index 0000000000..ecbef1a9cf
Binary files /dev/null and b/resources/builtin/repo/gears.png differ
diff --git a/resources/builtin/repo/globe.png b/resources/builtin/repo/globe.png
new file mode 100644
index 0000000000..b9ca72ffa2
Binary files /dev/null and b/resources/builtin/repo/globe.png differ
diff --git a/resources/builtin/repo/locked.png b/resources/builtin/repo/locked.png
new file mode 100644
index 0000000000..34074731a0
Binary files /dev/null and b/resources/builtin/repo/locked.png differ
diff --git a/resources/builtin/repo/microchip.png b/resources/builtin/repo/microchip.png
new file mode 100644
index 0000000000..25da5810de
Binary files /dev/null and b/resources/builtin/repo/microchip.png differ
diff --git a/resources/builtin/repo/mobile.png b/resources/builtin/repo/mobile.png
new file mode 100644
index 0000000000..5c59ad9b07
Binary files /dev/null and b/resources/builtin/repo/mobile.png differ
diff --git a/resources/builtin/repo/repo.png b/resources/builtin/repo/repo.png
new file mode 100644
index 0000000000..b3706f91bb
Binary files /dev/null and b/resources/builtin/repo/repo.png differ
diff --git a/resources/builtin/repo/servers.png b/resources/builtin/repo/servers.png
new file mode 100644
index 0000000000..3adb8a2fcd
Binary files /dev/null and b/resources/builtin/repo/servers.png differ
diff --git a/resources/sql/autopatches/20170612.repository.image.01.sql b/resources/sql/autopatches/20170612.repository.image.01.sql
new file mode 100644
index 0000000000..662c398855
--- /dev/null
+++ b/resources/sql/autopatches/20170612.repository.image.01.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_repository.repository
+ ADD profileImagePHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20170614.taskstatus.sql b/resources/sql/autopatches/20170614.taskstatus.sql
new file mode 100644
index 0000000000..2543632093
--- /dev/null
+++ b/resources/sql/autopatches/20170614.taskstatus.sql
@@ -0,0 +1,4 @@
+/* Extend from 12 characters to 64. */
+
+ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task
+ CHANGE status status VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 13e447c48d..9deeb338f6 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -453,13 +453,10 @@ phutil_register_library_map(array(
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php',
'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
- 'DifferentialGitHubLandingStrategy' => 'applications/differential/landing/DifferentialGitHubLandingStrategy.php',
'DifferentialGitSVNIDCommitMessageField' => 'applications/differential/field/DifferentialGitSVNIDCommitMessageField.php',
'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php',
'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
- 'DifferentialHostedGitLandingStrategy' => 'applications/differential/landing/DifferentialHostedGitLandingStrategy.php',
- 'DifferentialHostedMercurialLandingStrategy' => 'applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php',
'DifferentialHovercardEngineExtension' => 'applications/differential/engineextension/DifferentialHovercardEngineExtension.php',
'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php',
'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php',
@@ -472,8 +469,6 @@ phutil_register_library_map(array(
'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
'DifferentialJIRAIssuesCommitMessageField' => 'applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php',
'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
- 'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php',
- 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php',
'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.php',
'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
@@ -547,7 +542,7 @@ phutil_register_library_map(array(
'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php',
'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php',
'DifferentialRevisionIDCommitMessageField' => 'applications/differential/field/DifferentialRevisionIDCommitMessageField.php',
- 'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php',
+ 'DifferentialRevisionInlinesController' => 'applications/differential/controller/DifferentialRevisionInlinesController.php',
'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
@@ -612,6 +607,7 @@ phutil_register_library_map(array(
'DiffusionBlameConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBlameConduitAPIMethod.php',
'DiffusionBlameQuery' => 'applications/diffusion/query/blame/DiffusionBlameQuery.php',
'DiffusionBlockHeraldAction' => 'applications/diffusion/herald/DiffusionBlockHeraldAction.php',
+ 'DiffusionBranchListView' => 'applications/diffusion/view/DiffusionBranchListView.php',
'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php',
'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php',
'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php',
@@ -727,6 +723,7 @@ phutil_register_library_map(array(
'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php',
'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php',
'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php',
+ 'DiffusionGraphController' => 'applications/diffusion/controller/DiffusionGraphController.php',
'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php',
'DiffusionHistoryListView' => 'applications/diffusion/view/DiffusionHistoryListView.php',
'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php',
@@ -850,6 +847,7 @@ phutil_register_library_map(array(
'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php',
'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php',
+ 'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php',
'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php',
@@ -889,6 +887,7 @@ phutil_register_library_map(array(
'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php',
'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
+ 'DiffusionTagTableView' => 'applications/diffusion/view/DiffusionTagTableView.php',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php',
'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php',
@@ -1503,6 +1502,7 @@ phutil_register_library_map(array(
'ManiphestSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestSearchConduitAPIMethod.php',
'ManiphestStatusConfigOptionType' => 'applications/maniphest/config/ManiphestStatusConfigOptionType.php',
'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
+ 'ManiphestStatusSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php',
'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
'ManiphestSubtypesConfigOptionsType' => 'applications/maniphest/config/ManiphestSubtypesConfigOptionsType.php',
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
@@ -1737,6 +1737,7 @@ phutil_register_library_map(array(
'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php',
'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php',
'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php',
+ 'PHUIDiffInlineThreader' => 'infrastructure/diff/view/PHUIDiffInlineThreader.php',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php',
'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php',
'PHUIDiffTableOfContentsItemView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php',
@@ -2155,6 +2156,7 @@ phutil_register_library_map(array(
'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php',
'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php',
+ 'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
@@ -2164,6 +2166,7 @@ phutil_register_library_map(array(
'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php',
'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php',
'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php',
+ 'PhabricatorCachePurger' => 'applications/cache/purger/PhabricatorCachePurger.php',
'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php',
'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php',
'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php',
@@ -2313,6 +2316,7 @@ phutil_register_library_map(array(
'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
+ 'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
@@ -2923,6 +2927,7 @@ phutil_register_library_map(array(
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCollectWorkflow.php',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementSetPolicyWorkflow.php',
'PhabricatorGarbageCollectorManagementWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementWorkflow.php',
+ 'PhabricatorGeneralCachePurger' => 'applications/cache/purger/PhabricatorGeneralCachePurger.php',
'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php',
'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php',
'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php',
@@ -3741,6 +3746,7 @@ phutil_register_library_map(array(
'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
+ 'PhabricatorRemarkupCachePurger' => 'applications/cache/purger/PhabricatorRemarkupCachePurger.php',
'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php',
'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php',
@@ -4195,6 +4201,7 @@ phutil_register_library_map(array(
'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php',
+ 'PhabricatorUserCachePurger' => 'applications/cache/purger/PhabricatorUserCachePurger.php',
'PhabricatorUserCacheType' => 'applications/people/cache/PhabricatorUserCacheType.php',
'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php',
'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
@@ -5398,13 +5405,10 @@ phutil_register_library_map(array(
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetWorkingCopy' => 'Phobject',
- 'DifferentialGitHubLandingStrategy' => 'DifferentialLandingStrategy',
'DifferentialGitSVNIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialHarbormasterField' => 'DifferentialCustomField',
'DifferentialHiddenComment' => 'DifferentialDAO',
'DifferentialHostField' => 'DifferentialCustomField',
- 'DifferentialHostedGitLandingStrategy' => 'DifferentialLandingStrategy',
- 'DifferentialHostedMercurialLandingStrategy' => 'DifferentialLandingStrategy',
'DifferentialHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'DifferentialHunk' => array(
'DifferentialDAO',
@@ -5423,8 +5427,6 @@ phutil_register_library_map(array(
'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
'DifferentialJIRAIssuesCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
- 'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
- 'DifferentialLandingStrategy' => 'Phobject',
'DifferentialLegacyHunk' => 'DifferentialHunk',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialHarbormasterField',
@@ -5515,7 +5517,7 @@ phutil_register_library_map(array(
'DifferentialRevisionHeraldField' => 'HeraldField',
'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialRevisionIDCommitMessageField' => 'DifferentialCommitMessageField',
- 'DifferentialRevisionLandController' => 'DifferentialController',
+ 'DifferentialRevisionInlinesController' => 'DifferentialController',
'DifferentialRevisionListController' => 'DifferentialController',
'DifferentialRevisionListView' => 'AphrontView',
'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
@@ -5580,6 +5582,7 @@ phutil_register_library_map(array(
'DiffusionBlameConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBlameQuery' => 'DiffusionQuery',
'DiffusionBlockHeraldAction' => 'HeraldAction',
+ 'DiffusionBranchListView' => 'DiffusionView',
'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBranchTableController' => 'DiffusionController',
'DiffusionBranchTableView' => 'DiffusionView',
@@ -5698,6 +5701,7 @@ phutil_register_library_map(array(
'DiffusionRepositoryClusterEngineLogInterface',
),
'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
+ 'DiffusionGraphController' => 'DiffusionController',
'DiffusionHistoryController' => 'DiffusionController',
'DiffusionHistoryListView' => 'DiffusionHistoryView',
'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
@@ -5820,6 +5824,7 @@ phutil_register_library_map(array(
'DiffusionRepositoryManagementPanel' => 'Phobject',
'DiffusionRepositoryPath' => 'Phobject',
'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel',
+ 'DiffusionRepositoryProfilePictureController' => 'DiffusionController',
'DiffusionRepositoryRef' => 'Phobject',
'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
@@ -5859,6 +5864,7 @@ phutil_register_library_map(array(
'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery',
'DiffusionTagListController' => 'DiffusionController',
'DiffusionTagListView' => 'DiffusionView',
+ 'DiffusionTagTableView' => 'DiffusionView',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
@@ -6598,6 +6604,7 @@ phutil_register_library_map(array(
'ManiphestSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ManiphestStatusConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand',
+ 'ManiphestStatusSearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestSubpriorityController' => 'ManiphestController',
'ManiphestSubtypesConfigOptionsType' => 'PhabricatorConfigJSONOptionType',
'ManiphestTask' => array(
@@ -6870,6 +6877,7 @@ phutil_register_library_map(array(
'PHUIDiffInlineCommentTableScaffold' => 'AphrontView',
'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentView' => 'AphrontView',
+ 'PHUIDiffInlineThreader' => 'Phobject',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDiffRevealIconView' => 'AphrontView',
'PHUIDiffTableOfContentsItemView' => 'AphrontView',
@@ -7343,6 +7351,7 @@ phutil_register_library_map(array(
'PhabricatorBoolEditField' => 'PhabricatorEditField',
'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine',
+ 'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
@@ -7352,6 +7361,7 @@ phutil_register_library_map(array(
'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow',
'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector',
+ 'PhabricatorCachePurger' => 'Phobject',
'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorCacheSpec' => 'Phobject',
@@ -7537,6 +7547,7 @@ phutil_register_library_map(array(
'PhabricatorCelerityApplication' => 'PhabricatorApplication',
'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
+ 'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogApplication' => 'PhabricatorApplication',
'PhabricatorChatLogChannel' => array(
@@ -8239,6 +8250,7 @@ phutil_register_library_map(array(
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementWorkflow' => 'PhabricatorManagementWorkflow',
+ 'PhabricatorGeneralCachePurger' => 'PhabricatorCachePurger',
'PhabricatorGestureUIExample' => 'PhabricatorUIExample',
'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider',
@@ -9190,6 +9202,7 @@ phutil_register_library_map(array(
'PhabricatorRegistrationProfile' => 'Phobject',
'PhabricatorReleephApplication' => 'PhabricatorApplication',
'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
+ 'PhabricatorRemarkupCachePurger' => 'PhabricatorCachePurger',
'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule',
@@ -9742,6 +9755,7 @@ phutil_register_library_map(array(
'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserCache' => 'PhabricatorUserDAO',
+ 'PhabricatorUserCachePurger' => 'PhabricatorCachePurger',
'PhabricatorUserCacheType' => 'Phobject',
'PhabricatorUserCardView' => 'AphrontTagView',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index b89feaebb5..c0cd259992 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -270,7 +270,10 @@ abstract class AphrontApplicationConfiguration extends Phobject {
}
} catch (Exception $ex) {
$original_exception = $ex;
- $response = $this->handleException($ex);
+ $response = $this->handleThrowable($ex);
+ } catch (Throwable $ex) {
+ $original_exception = $ex;
+ $response = $this->handleThrowable($ex);
}
try {
@@ -663,24 +666,24 @@ abstract class AphrontApplicationConfiguration extends Phobject {
* This method delegates exception handling to available subclasses of
* @{class:AphrontRequestExceptionHandler}.
*
- * @param Exception Exception which needs to be handled.
+ * @param Throwable Exception which needs to be handled.
* @return wild Response or response producer, or null if no available
* handler can produce a response.
* @task exception
*/
- private function handleException(Exception $ex) {
+ private function handleThrowable($throwable) {
$handlers = AphrontRequestExceptionHandler::getAllHandlers();
$request = $this->getRequest();
foreach ($handlers as $handler) {
- if ($handler->canHandleRequestException($request, $ex)) {
- $response = $handler->handleRequestException($request, $ex);
+ if ($handler->canHandleRequestThrowable($request, $throwable)) {
+ $response = $handler->handleRequestThrowable($request, $throwable);
$this->validateErrorHandlerResponse($handler, $response);
return $response;
}
}
- throw $ex;
+ throw $throwable;
}
private static function newSelfCheckResponse() {
diff --git a/src/aphront/handler/AphrontRequestExceptionHandler.php b/src/aphront/handler/AphrontRequestExceptionHandler.php
index 694071e1ba..e626f53cc6 100644
--- a/src/aphront/handler/AphrontRequestExceptionHandler.php
+++ b/src/aphront/handler/AphrontRequestExceptionHandler.php
@@ -12,19 +12,13 @@ abstract class AphrontRequestExceptionHandler extends Phobject {
abstract public function getRequestExceptionHandlerPriority();
- public function shouldLogException(
+ abstract public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
- return null;
- }
+ $throwable);
- abstract public function canHandleRequestException(
+ abstract public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex);
-
- abstract public function handleRequestException(
- AphrontRequest $request,
- Exception $ex);
+ $throwable);
final public static function getAllHandlers() {
return id(new PhutilClassMapQuery())
diff --git a/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php b/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php
index 06023e02de..d519c83fb5 100644
--- a/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php
@@ -11,27 +11,28 @@ final class PhabricatorAjaxRequestExceptionHandler
return pht('Responds to requests made by AJAX clients.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
// For non-workflow requests, return a Ajax response.
return ($request->isAjax() && !$request->isWorkflow());
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
// Log these; they don't get shown on the client and can be difficult
// to debug.
- phlog($ex);
+ phlog($throwable);
$response = new AphrontAjaxResponse();
$response->setError(
array(
- 'code' => get_class($ex),
- 'info' => $ex->getMessage(),
+ 'code' => get_class($throwable),
+ 'info' => $throwable->getMessage(),
));
+
return $response;
}
diff --git a/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php b/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php
index 367f607f83..db8a5d5ecc 100644
--- a/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php
@@ -11,19 +11,19 @@ final class PhabricatorConduitRequestExceptionHandler
return pht('Responds to requests made by Conduit clients.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
return $request->isConduit();
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
$response = id(new ConduitAPIResponse())
- ->setErrorCode(get_class($ex))
- ->setErrorInfo($ex->getMessage());
+ ->setErrorCode(get_class($throwable))
+ ->setErrorInfo($throwable->getMessage());
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
diff --git a/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php b/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php
index d330cd5f2c..b13100b05d 100644
--- a/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php
@@ -11,9 +11,9 @@ final class PhabricatorDefaultRequestExceptionHandler
return pht('Handles all other exceptions.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
if (!$this->isPhabricatorSite($request)) {
return false;
@@ -22,9 +22,9 @@ final class PhabricatorDefaultRequestExceptionHandler
return true;
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
$viewer = $this->getViewer($request);
@@ -33,18 +33,18 @@ final class PhabricatorDefaultRequestExceptionHandler
// the internet. These include requests with bad CSRF tokens and
// questionable "Host" headers.
$should_log = true;
- if ($ex instanceof AphrontMalformedRequestException) {
- $should_log = !$ex->getIsUnlogged();
+ if ($throwable instanceof AphrontMalformedRequestException) {
+ $should_log = !$throwable->getIsUnlogged();
}
if ($should_log) {
- phlog($ex);
+ phlog($throwable);
}
- $class = get_class($ex);
- $message = $ex->getMessage();
+ $class = get_class($throwable);
+ $message = $throwable->getMessage();
- if ($ex instanceof AphrontSchemaQueryException) {
+ if ($throwable instanceof AphrontSchemaQueryException) {
$message .= "\n\n".pht(
"NOTE: This usually indicates that the MySQL schema has not been ".
"properly upgraded. Run '%s' to ensure your schema is up to date.",
@@ -54,7 +54,7 @@ final class PhabricatorDefaultRequestExceptionHandler
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$trace = id(new AphrontStackTraceView())
->setUser($viewer)
- ->setTrace($ex->getTrace());
+ ->setTrace($throwable->getTrace());
} else {
$trace = null;
}
diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
index ff4ace2e4b..f8d522711f 100644
--- a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
@@ -13,26 +13,26 @@ final class PhabricatorHighSecurityRequestExceptionHandler
'to present MFA credentials to take an action.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
if (!$this->isPhabricatorSite($request)) {
return false;
}
- return ($ex instanceof PhabricatorAuthHighSecurityRequiredException);
+ return ($throwable instanceof PhabricatorAuthHighSecurityRequiredException);
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
$viewer = $this->getViewer($request);
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
- $ex->getFactors(),
- $ex->getFactorValidationResults(),
+ $throwable->getFactors(),
+ $throwable->getFactorValidationResults(),
$viewer,
$request);
@@ -61,7 +61,7 @@ final class PhabricatorHighSecurityRequestExceptionHandler
'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.'))
->setSubmitURI($request->getPath())
- ->addCancelButton($ex->getCancelURI())
+ ->addCancelButton($throwable->getCancelURI())
->addSubmitButton(pht('Enter High Security'));
$request_parameters = $request->getPassthroughRequestParameters(
diff --git a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php
index 1c84e90fd1..cb1dea3533 100644
--- a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php
@@ -13,20 +13,20 @@ final class PhabricatorPolicyRequestExceptionHandler
'do something they do not have permission to do.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
if (!$this->isPhabricatorSite($request)) {
return false;
}
- return ($ex instanceof PhabricatorPolicyException);
+ return ($throwable instanceof PhabricatorPolicyException);
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
$viewer = $this->getViewer($request);
@@ -52,12 +52,12 @@ final class PhabricatorPolicyRequestExceptionHandler
array(
'class' => 'aphront-policy-rejection',
),
- $ex->getRejection()),
+ $throwable->getRejection()),
);
$list = null;
- if ($ex->getCapabilityName()) {
- $list = $ex->getMoreInfo();
+ if ($throwable->getCapabilityName()) {
+ $list = $throwable->getMoreInfo();
foreach ($list as $key => $item) {
$list[$key] = $item;
}
@@ -67,12 +67,14 @@ final class PhabricatorPolicyRequestExceptionHandler
array(
'class' => 'aphront-capability-details',
),
- pht('Users with the "%s" capability:', $ex->getCapabilityName()));
+ pht(
+ 'Users with the "%s" capability:',
+ $throwable->getCapabilityName()));
}
$dialog = id(new AphrontDialogView())
- ->setTitle($ex->getTitle())
+ ->setTitle($throwable->getTitle())
->setClass('aphront-access-dialog')
->setUser($viewer)
->appendChild($content);
diff --git a/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php b/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php
index 3fb549c29f..78dad564b4 100644
--- a/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php
@@ -13,20 +13,20 @@ final class PhabricatorRateLimitRequestExceptionHandler
'does something too frequently.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
if (!$this->isPhabricatorSite($request)) {
return false;
}
- return ($ex instanceof PhabricatorSystemActionRateLimitException);
+ return ($throwable instanceof PhabricatorSystemActionRateLimitException);
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
$viewer = $this->getViewer($request);
@@ -34,8 +34,8 @@ final class PhabricatorRateLimitRequestExceptionHandler
->setTitle(pht('Slow Down!'))
->setUser($viewer)
->setErrors(array(pht('You are being rate limited.')))
- ->appendParagraph($ex->getMessage())
- ->appendParagraph($ex->getRateExplanation())
+ ->appendParagraph($throwable->getMessage())
+ ->appendParagraph($throwable->getRateExplanation())
->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
}
diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
index b330368d1b..2534384f61 100644
--- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php
+++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
@@ -331,6 +331,14 @@ final class PhabricatorAuditInlineComment
return $this->isGhost;
}
+ public function getDateModified() {
+ return $this->proxy->getDateModified();
+ }
+
+ public function getDateCreated() {
+ return $this->proxy->getDateCreated();
+ }
+
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index c4d1b47c40..1ecfdaf557 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -483,8 +483,10 @@ abstract class PhabricatorController extends AphrontController {
// NOTE: Applications (objects of class PhabricatorApplication) can't
// currently be set here, although they don't need any of the extensions
// anyway. This should probably work differently than it does, though.
- if ($object instanceof PhabricatorLiskDAO) {
- $action_list->setObject($object);
+ if ($object) {
+ if ($object instanceof PhabricatorLiskDAO) {
+ $action_list->setObject($object);
+ }
}
$curtain = id(new PHUICurtainView())
diff --git a/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php b/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php
index 91ab0cf83d..cfe42c918b 100644
--- a/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php
+++ b/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php
@@ -6,119 +6,79 @@ final class PhabricatorCacheManagementPurgeWorkflow
protected function didConstruct() {
$this
->setName('purge')
- ->setSynopsis(pht('Drop data from caches. APC-based caches can be '.
- 'purged from the web interface.'))
+ ->setSynopsis(pht('Drop data from readthrough caches.'))
->setArguments(
array(
array(
- 'name' => 'purge-all',
- 'help' => pht('Purge all caches.'),
+ 'name' => 'all',
+ 'help' => pht('Purge all caches.'),
),
array(
- 'name' => 'purge-remarkup',
- 'help' => pht('Purge the remarkup cache.'),
- ),
- array(
- 'name' => 'purge-changeset',
- 'help' => pht('Purge the Differential changeset cache.'),
- ),
- array(
- 'name' => 'purge-general',
- 'help' => pht('Purge the general cache.'),
- ),
- array(
- 'name' => 'purge-user',
- 'help' => pht('Purge the user cache.'),
+ 'name' => 'caches',
+ 'param' => 'keys',
+ 'help' => pht('Purge a specific set of caches.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
- $console = PhutilConsole::getConsole();
+ $all_purgers = PhabricatorCachePurger::getAllPurgers();
- $purge_all = $args->getArg('purge-all');
-
- $purge = array(
- 'remarkup' => $purge_all || $args->getArg('purge-remarkup'),
- 'changeset' => $purge_all || $args->getArg('purge-changeset'),
- 'general' => $purge_all || $args->getArg('purge-general'),
- 'user' => $purge_all || $args->getArg('purge-user'),
- );
-
- if (!array_filter($purge)) {
- $list = array();
- foreach ($purge as $key => $ignored) {
- $list[] = "'--purge-".$key."'";
- }
+ $is_all = $args->getArg('all');
+ $key_list = $args->getArg('caches');
+ if ($is_all && strlen($key_list)) {
throw new PhutilArgumentUsageException(
pht(
- "Specify which cache or caches to purge, or use '%s'. Available ".
- "caches are: %s. Use '%s' for more information.",
- '--purge-all',
- implode(', ', $list),
- '--help'));
+ 'Specify either "--all" or "--caches", not both.'));
+ } else if (!$is_all && !strlen($key_list)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Select caches to purge with "--all" or "--caches". Available '.
+ 'caches are: %s.',
+ implode(', ', array_keys($all_purgers))));
}
- if ($purge['remarkup']) {
- $console->writeOut(pht('Purging remarkup cache...'));
- $this->purgeRemarkupCache();
- $console->writeOut("%s\n", pht('Done.'));
+ if ($is_all) {
+ $purgers = $all_purgers;
+ } else {
+ $key_list = preg_split('/[\s,]+/', $key_list);
+ $purgers = array();
+ foreach ($key_list as $key) {
+ if (isset($all_purgers[$key])) {
+ $purgers[$key] = $all_purgers[$key];
+ } else {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Cache purger "%s" is not recognized. Available caches '.
+ 'are: %s.',
+ $key,
+ implode(', ', array_keys($all_purgers))));
+ }
+ }
+ if (!$purgers) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'When using "--caches", you must select at least one valid '.
+ 'cache to purge.'));
+ }
}
- if ($purge['changeset']) {
- $console->writeOut(pht('Purging changeset cache...'));
- $this->purgeChangesetCache();
- $console->writeOut("%s\n", pht('Done.'));
+ $viewer = $this->getViewer();
+
+ foreach ($purgers as $key => $purger) {
+ $purger->setViewer($viewer);
+
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Purging "%s" cache...',
+ $key));
+
+ $purger->purgeCache();
}
- if ($purge['general']) {
- $console->writeOut(pht('Purging general cache...'));
- $this->purgeGeneralCache();
- $console->writeOut("%s\n", pht('Done.'));
- }
-
- if ($purge['user']) {
- $console->writeOut(pht('Purging user cache...'));
- $this->purgeUserCache();
- $console->writeOut("%s\n", pht('Done.'));
- }
- }
-
- private function purgeRemarkupCache() {
- $conn_w = id(new PhabricatorMarkupCache())->establishConnection('w');
-
- queryfx(
- $conn_w,
- 'TRUNCATE TABLE %T',
- id(new PhabricatorMarkupCache())->getTableName());
- }
-
- private function purgeChangesetCache() {
- $conn_w = id(new DifferentialChangeset())->establishConnection('w');
- queryfx(
- $conn_w,
- 'TRUNCATE TABLE %T',
- DifferentialChangeset::TABLE_CACHE);
- }
-
- private function purgeGeneralCache() {
- $conn_w = id(new PhabricatorMarkupCache())->establishConnection('w');
-
- queryfx(
- $conn_w,
- 'TRUNCATE TABLE %T',
- 'cache_general');
- }
-
- private function purgeUserCache() {
- $table = new PhabricatorUserCache();
- $conn_w = $table->establishConnection('w');
-
- queryfx(
- $conn_w,
- 'TRUNCATE TABLE %T',
- $table->getTableName());
+ return 0;
}
}
diff --git a/src/applications/cache/purger/PhabricatorBuiltinFileCachePurger.php b/src/applications/cache/purger/PhabricatorBuiltinFileCachePurger.php
new file mode 100644
index 0000000000..a4bc682c02
--- /dev/null
+++ b/src/applications/cache/purger/PhabricatorBuiltinFileCachePurger.php
@@ -0,0 +1,22 @@
+getViewer();
+
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withIsBuiltin(true)
+ ->execute();
+
+ $engine = new PhabricatorDestructionEngine();
+ foreach ($files as $file) {
+ $engine->destroyObject($file);
+ }
+ }
+
+}
diff --git a/src/applications/cache/purger/PhabricatorCachePurger.php b/src/applications/cache/purger/PhabricatorCachePurger.php
new file mode 100644
index 0000000000..0115b45d7e
--- /dev/null
+++ b/src/applications/cache/purger/PhabricatorCachePurger.php
@@ -0,0 +1,30 @@
+viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function getPurgerKey() {
+ return $this->getPhobjectClassConstant('PURGERKEY');
+ }
+
+ final public static function getAllPurgers() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getPurgerKey')
+ ->execute();
+ }
+
+}
diff --git a/src/applications/cache/purger/PhabricatorChangesetCachePurger.php b/src/applications/cache/purger/PhabricatorChangesetCachePurger.php
new file mode 100644
index 0000000000..fd6ff9940a
--- /dev/null
+++ b/src/applications/cache/purger/PhabricatorChangesetCachePurger.php
@@ -0,0 +1,18 @@
+establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'TRUNCATE TABLE %T',
+ DifferentialChangeset::TABLE_CACHE);
+ }
+
+}
diff --git a/src/applications/cache/purger/PhabricatorGeneralCachePurger.php b/src/applications/cache/purger/PhabricatorGeneralCachePurger.php
new file mode 100644
index 0000000000..f2dea1e1e5
--- /dev/null
+++ b/src/applications/cache/purger/PhabricatorGeneralCachePurger.php
@@ -0,0 +1,18 @@
+establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'TRUNCATE TABLE %T',
+ 'cache_general');
+ }
+
+}
diff --git a/src/applications/cache/purger/PhabricatorRemarkupCachePurger.php b/src/applications/cache/purger/PhabricatorRemarkupCachePurger.php
new file mode 100644
index 0000000000..2c416fff36
--- /dev/null
+++ b/src/applications/cache/purger/PhabricatorRemarkupCachePurger.php
@@ -0,0 +1,18 @@
+establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'TRUNCATE TABLE %T',
+ $table->getTableName());
+ }
+
+}
diff --git a/src/applications/cache/purger/PhabricatorUserCachePurger.php b/src/applications/cache/purger/PhabricatorUserCachePurger.php
new file mode 100644
index 0000000000..64f631c96b
--- /dev/null
+++ b/src/applications/cache/purger/PhabricatorUserCachePurger.php
@@ -0,0 +1,18 @@
+establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'TRUNCATE TABLE %T',
+ $table->getTableName());
+ }
+
+}
diff --git a/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php b/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php
index 49d5f38959..1fc3a56317 100644
--- a/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php
+++ b/src/applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php
@@ -49,6 +49,13 @@ final class PhabricatorCalendarEventHeraldAdapter extends HeraldAdapter {
}
}
+ public function getRepetitionOptions() {
+ return array(
+ HeraldRepetitionPolicyConfig::EVERY,
+ HeraldRepetitionPolicyConfig::FIRST,
+ );
+ }
+
public function getHeraldName() {
return $this->getObject()->getMonogram();
}
diff --git a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
index 958883d34e..d534d32adc 100644
--- a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
+++ b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
@@ -42,8 +42,7 @@ final class PhabricatorDashboardPanelDatasource
$properties = $panel->getProperties();
$result = id(new PhabricatorTypeaheadResult())
- ->setName($panel->getName())
- ->setDisplayName($monogram.' '.$panel->getName())
+ ->setName($monogram.' '.$panel->getName())
->setPHID($id)
->setIcon($impl->getIcon())
->addAttribute($type_text);
diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php
index 1b6443a9e0..e259c2f112 100644
--- a/src/applications/differential/application/PhabricatorDifferentialApplication.php
+++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php
@@ -41,12 +41,6 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication {
return "\xE2\x9A\x99";
}
- public function getEventListeners() {
- return array(
- new DifferentialLandingActionMenuEventListener(),
- );
- }
-
public function getOverview() {
return pht(
'Differential is a **code review application** which allows '.
@@ -69,14 +63,14 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication {
=> 'DifferentialRevisionEditController',
$this->getEditRoutePattern('attach/(?P[^/]+)/to/')
=> 'DifferentialRevisionEditController',
- 'land/(?:(?P[1-9]\d*))/(?P[^/]+)/'
- => 'DifferentialRevisionLandController',
'closedetails/(?P[^/]+)/'
=> 'DifferentialRevisionCloseDetailsController',
'update/(?P[1-9]\d*)/'
=> 'DifferentialDiffCreateController',
'operation/(?P[1-9]\d*)/'
=> 'DifferentialRevisionOperationController',
+ 'inlines/(?P[1-9]\d*)/'
+ => 'DifferentialRevisionInlinesController',
),
'comment/' => array(
'preview/(?P[1-9]\d*)/' => 'DifferentialCommentPreviewController',
diff --git a/src/applications/differential/controller/DifferentialRevisionInlinesController.php b/src/applications/differential/controller/DifferentialRevisionInlinesController.php
new file mode 100644
index 0000000000..c5eb063e99
--- /dev/null
+++ b/src/applications/differential/controller/DifferentialRevisionInlinesController.php
@@ -0,0 +1,190 @@
+getViewer();
+ $id = $request->getURIData('id');
+
+ $revision = id(new DifferentialRevisionQuery())
+ ->withIDs(array($id))
+ ->setViewer($viewer)
+ ->needDiffIDs(true)
+ ->executeOne();
+ if (!$revision) {
+ return new Aphront404Response();
+ }
+
+ $revision_monogram = $revision->getMonogram();
+ $revision_uri = $revision->getURI();
+ $revision_title = $revision->getTitle();
+
+ $query = id(new DifferentialInlineCommentQuery())
+ ->setViewer($viewer)
+ ->needHidden(true)
+ ->withRevisionPHIDs(array($revision->getPHID()));
+ $inlines = $query->execute();
+
+ $crumbs = $this->buildApplicationCrumbs();
+ $crumbs->addTextCrumb($revision_monogram, $revision_uri);
+ $crumbs->addTextCrumb(pht('Inline Comments'));
+ $crumbs->setBorder(true);
+
+ $content = $this->renderInlineTable($revision, $inlines);
+ $header = $this->buildHeader($revision);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter($content);
+
+ return $this->newPage()
+ ->setTitle(
+ array(
+ "{$revision_monogram} {$revision_title}",
+ pht('Inlines'),
+ ))
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+ }
+
+ private function buildHeader(DifferentialRevision $revision) {
+ $viewer = $this->getViewer();
+
+ $button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-chevron-left')
+ ->setHref($revision->getURI())
+ ->setText(pht('Back to Revision'));
+
+ return id(new PHUIHeaderView())
+ ->setHeader($revision->getTitle())
+ ->setUser($viewer)
+ ->setHeaderIcon('fa-cog')
+ ->addActionLink($button);
+ }
+
+ private function renderInlineTable(
+ DifferentialRevision $revision,
+ array $inlines) {
+
+ $viewer = $this->getViewer();
+ $inlines = id(new PHUIDiffInlineThreader())
+ ->reorderAndThreadCommments($inlines);
+
+ $handle_phids = array();
+ $changeset_ids = array();
+ foreach ($inlines as $inline) {
+ $handle_phids[] = $inline->getAuthorPHID();
+ $changeset_ids[] = $inline->getChangesetID();
+ }
+ $handles = $viewer->loadHandles($handle_phids);
+ $handles = iterator_to_array($handles);
+
+ if ($changeset_ids) {
+ $changesets = id(new DifferentialChangesetQuery())
+ ->setViewer($viewer)
+ ->withIDs($changeset_ids)
+ ->execute();
+ $changesets = mpull($changesets, null, 'getID');
+ } else {
+ $changesets = array();
+ }
+
+ $current_changeset = head($revision->getDiffIDs());
+
+ $rows = array();
+ foreach ($inlines as $inline) {
+ $status_icons = array();
+
+ $c_id = $inline->getChangesetID();
+ $d_id = $changesets[$c_id]->getDiffID();
+
+ if ($d_id == $current_changeset) {
+ $diff_id = phutil_tag('strong', array(), pht('Current'));
+ } else {
+ $diff_id = pht('Diff %d', $d_id);
+ }
+
+ $reviewer = $handles[$inline->getAuthorPHID()]->renderLink();
+ $now = PhabricatorTime::getNow();
+ $then = $inline->getDateModified();
+ $datetime = phutil_format_relative_time($now - $then);
+
+ $comment_href = $revision->getURI().'#inline-'.$inline->getID();
+ $comment = phutil_tag(
+ 'a',
+ array(
+ 'href' => $comment_href,
+ ),
+ $inline->getContent());
+
+ $state = $inline->getFixedState();
+ if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
+ $status_icons[] = id(new PHUIIconView())
+ ->setIcon('fa-check green')
+ ->addClass('mmr');
+ } else if ($inline->getReplyToCommentPHID() &&
+ $inline->getAuthorPHID() == $revision->getAuthorPHID()) {
+ $status_icons[] = id(new PHUIIconView())
+ ->setIcon('fa-commenting-o blue')
+ ->addClass('mmr');
+ } else {
+ $status_icons[] = id(new PHUIIconView())
+ ->setIcon('fa-circle-o grey')
+ ->addClass('mmr');
+ }
+
+
+ if ($inline->getReplyToCommentPHID()) {
+ $reply_icon = id(new PHUIIconView())
+ ->setIcon('fa-reply mmr darkgrey');
+ $comment = array($reply_icon, $comment);
+ }
+
+ $rows[] = array(
+ $diff_id,
+ $status_icons,
+ $reviewer,
+ AphrontTableView::renderSingleDisplayLine($comment),
+ $datetime,
+ );
+ }
+
+ $table = new AphrontTableView($rows);
+ $table->setHeaders(
+ array(
+ pht('Diff'),
+ pht('Status'),
+ pht('Reviewer'),
+ pht('Comment'),
+ pht('Created'),
+ ));
+ $table->setColumnClasses(
+ array(
+ '',
+ '',
+ '',
+ 'wide',
+ 'right',
+ ));
+ $table->setColumnVisibility(
+ array(
+ true,
+ true,
+ true,
+ true,
+ true,
+ ));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Inline Comments'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($table);
+ }
+
+}
diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php
deleted file mode 100644
index 0e222370df..0000000000
--- a/src/applications/differential/controller/DifferentialRevisionLandController.php
+++ /dev/null
@@ -1,157 +0,0 @@
-getViewer();
- $revision_id = $request->getURIData('id');
- $strategy_class = $request->getURIData('strategy');
-
- $revision = id(new DifferentialRevisionQuery())
- ->withIDs(array($revision_id))
- ->setViewer($viewer)
- ->executeOne();
- if (!$revision) {
- return new Aphront404Response();
- }
-
- if (is_subclass_of($strategy_class, 'DifferentialLandingStrategy')) {
- $this->pushStrategy = newv($strategy_class, array());
- } else {
- throw new Exception(
- pht(
- "Strategy type must be a valid class name and must subclass ".
- "%s. '%s' is not a subclass of %s",
- 'DifferentialLandingStrategy',
- $strategy_class,
- 'DifferentialLandingStrategy'));
- }
-
- if ($request->isDialogFormPost()) {
- $response = null;
- $text = '';
- try {
- $response = $this->attemptLand($revision, $request);
- $title = pht('Success!');
- $text = pht('Revision was successfully landed.');
- } catch (Exception $ex) {
- $title = pht('Failed to land revision');
- if ($ex instanceof PhutilProxyException) {
- $text = hsprintf(
- '%s:%s ',
- $ex->getMessage(),
- $ex->getPreviousException()->getMessage());
- } else {
- $text = phutil_tag('pre', array(), $ex->getMessage());
- }
- $text = id(new PHUIInfoView())
- ->appendChild($text);
- }
-
- if ($response instanceof AphrontDialogView) {
- $dialog = $response;
- } else {
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
- ->setTitle($title)
- ->appendChild(phutil_tag('p', array(), $text))
- ->addCancelButton('/D'.$revision_id, pht('Done'));
- }
- return id(new AphrontDialogResponse())->setDialog($dialog);
- }
-
- $is_disabled = $this->pushStrategy->isActionDisabled(
- $viewer,
- $revision,
- $revision->getRepository());
- if ($is_disabled) {
- if (is_string($is_disabled)) {
- $explain = $is_disabled;
- } else {
- $explain = pht('This action is not currently enabled.');
- }
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
- ->setTitle(pht("Can't land revision"))
- ->appendChild($explain)
- ->addCancelButton('/D'.$revision_id);
-
- return id(new AphrontDialogResponse())->setDialog($dialog);
- }
-
-
- $prompt = hsprintf('%s %s',
- pht(
- 'This will squash and rebase revision %s, and push it to '.
- 'the default / master branch.',
- $revision_id),
- pht('It is an experimental feature and may not work.'));
-
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
- ->setTitle(pht('Land Revision %s?', $revision_id))
- ->appendChild($prompt)
- ->setSubmitURI($request->getRequestURI())
- ->addSubmitButton(pht('Land it!'))
- ->addCancelButton('/D'.$revision_id);
-
- return id(new AphrontDialogResponse())->setDialog($dialog);
- }
-
- private function attemptLand($revision, $request) {
- $status = $revision->getStatus();
- if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
- throw new Exception(pht('Only Accepted revisions can be landed.'));
- }
-
- $repository = $revision->getRepository();
-
- if ($repository === null) {
- throw new Exception(pht('Revision is not attached to a repository.'));
- }
-
- $can_push = PhabricatorPolicyFilter::hasCapability(
- $request->getUser(),
- $repository,
- DiffusionPushCapability::CAPABILITY);
-
- if (!$can_push) {
- throw new Exception(
- pht('You do not have permission to push to this repository.'));
- }
-
- $lock = $this->lockRepository($repository);
-
- try {
- $response = $this->pushStrategy->processLandRequest(
- $request,
- $revision,
- $repository);
- } catch (Exception $e) {
- $lock->unlock();
- throw $e;
- }
-
- $lock->unlock();
-
- $looksoon = new ConduitCall(
- 'diffusion.looksoon',
- array(
- 'repositories' => array($repository->getPHID()),
- ));
- $looksoon->setUser($request->getUser());
- $looksoon->execute();
-
- return $response;
- }
-
- private function lockRepository($repository) {
- $lock_name = __CLASS__.':'.($repository->getPHID());
- $lock = PhabricatorGlobalLock::newLock($lock_name);
- $lock->lock();
- return $lock;
- }
-
-}
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index 69e45b377d..f19ff37360 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -281,6 +281,12 @@ final class DifferentialRevisionViewController extends DifferentialController {
->setTitle(pht('Diff %s', $target->getID()))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
+
+ $revision_id = $revision->getID();
+ $inline_list_uri = "/revision/inlines/{$revision_id}/";
+ $inline_list_uri = $this->getApplicationURI($inline_list_uri);
+ $changeset_view->setInlineListURI($inline_list_uri);
+
if ($repository) {
$changeset_view->setRepository($repository);
}
diff --git a/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php b/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php
deleted file mode 100644
index b4ff727cbe..0000000000
--- a/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php
+++ /dev/null
@@ -1,186 +0,0 @@
-getUser();
- $this->init($viewer, $repository);
-
- $workspace = $this->getGitWorkspace($repository);
-
- try {
- id(new DifferentialHostedGitLandingStrategy())
- ->commitRevisionToWorkspace($revision, $workspace, $viewer);
- } catch (Exception $e) {
- throw new PhutilProxyException(pht('Failed to commit patch.'), $e);
- }
-
- try {
- $this->pushWorkspaceRepository($repository, $workspace);
- } catch (Exception $e) {
- // If it's a permission problem, we know more than git.
- $dialog = $this->verifyRemotePermissions($viewer, $revision, $repository);
- if ($dialog) {
- return $dialog;
- }
-
- // Else, throw what git said.
- throw new PhutilProxyException(
- pht('Failed to push changes upstream.'),
- $e);
- }
- }
-
- /**
- * Returns PhabricatorActionView or an array of PhabricatorActionView or null.
- */
- public function createMenuItem(
- PhabricatorUser $viewer,
- DifferentialRevision $revision,
- PhabricatorRepository $repository) {
-
- // TODO: This temporarily disables this action, because it doesn't work
- // and is confusing to users. If you want to use it, comment out this line
- // for now and we'll provide real support eventually.
- return;
-
- $vcs = $repository->getVersionControlSystem();
- if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
- return;
- }
-
- if ($repository->isHosted()) {
- return;
- }
-
- try {
- // These throw when failing.
- $this->init($viewer, $repository);
- $this->findGitHubRepo($repository);
- } catch (Exception $e) {
- return;
- }
-
- return $this->createActionView($revision, pht('Land to GitHub'))
- ->setIcon('fa-cloud-upload');
- }
-
- public function pushWorkspaceRepository(
- PhabricatorRepository $repository,
- ArcanistRepositoryAPI $workspace) {
-
- $token = $this->getAccessToken();
-
- $github_repo = $this->findGitHubRepo($repository);
-
- $remote = urisprintf(
- 'https://%s:x-oauth-basic@%s/%s.git',
- $token,
- $this->provider->getProviderDomain(),
- $github_repo);
-
- $workspace->execxLocal(
- 'push %P HEAD:master',
- new PhutilOpaqueEnvelope($remote));
- }
-
- private function init($viewer, $repository) {
- $repo_uri = $repository->getRemoteURIObject();
- $repo_domain = $repo_uri->getDomain();
-
- $this->account = id(new PhabricatorExternalAccountQuery())
- ->setViewer($viewer)
- ->withUserPHIDs(array($viewer->getPHID()))
- ->withAccountTypes(array('github'))
- ->withAccountDomains(array($repo_domain))
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->executeOne();
-
- if (!$this->account) {
- throw new Exception(
- pht('No matching GitHub account found for %s.', $repo_domain));
- }
-
- $this->provider = PhabricatorAuthProvider::getEnabledProviderByKey(
- $this->account->getProviderKey());
- if (!$this->provider) {
- throw new Exception(
- pht('GitHub provider for %s is not enabled.', $repo_domain));
- }
- }
-
- private function findGitHubRepo(PhabricatorRepository $repository) {
- $repo_uri = $repository->getRemoteURIObject();
-
- $repo_path = $repo_uri->getPath();
-
- if (substr($repo_path, -4) == '.git') {
- $repo_path = substr($repo_path, 0, -4);
- }
- $repo_path = ltrim($repo_path, '/');
-
- return $repo_path;
- }
-
- private function getAccessToken() {
- return $this->provider->getOAuthAccessToken($this->account);
- }
-
- private function verifyRemotePermissions($viewer, $revision, $repository) {
- $github_user = $this->account->getUsername();
- $github_repo = $this->findGitHubRepo($repository);
-
- $uri = urisprintf(
- 'https://api.github.com/repos/%s/collaborators/%s',
- $github_repo,
- $github_user);
-
- $uri = new PhutilURI($uri);
- $uri->setQueryParam('access_token', $this->getAccessToken());
- list($status, $body, $headers) = id(new HTTPSFuture($uri))->resolve();
-
- // Likely status codes:
- // 204 No Content: Has permissions. Token might be too weak.
- // 404 Not Found: Not a collaborator.
- // 401 Unauthorized: Token is bad/revoked.
-
- $no_permission = ($status->getStatusCode() == 404);
-
- if ($no_permission) {
- throw new Exception(
- pht(
- "You don't have permission to push to this repository. ".
- "Push permissions for this repository are managed on GitHub."));
- }
-
- $scopes = BaseHTTPFuture::getHeader($headers, 'X-OAuth-Scopes');
- if (strpos($scopes, 'public_repo') === false) {
- $provider_key = $this->provider->getProviderKey();
- $refresh_token_uri = new PhutilURI("/auth/refresh/{$provider_key}/");
- $refresh_token_uri->setQueryParam('scope', 'public_repo');
-
- return id(new AphrontDialogView())
- ->setUser($viewer)
- ->setTitle(pht('Stronger token needed'))
- ->appendChild(pht(
- 'In order to complete this action, you need a '.
- 'stronger GitHub token.'))
- ->setSubmitURI($refresh_token_uri)
- ->addCancelButton('/D'.$revision->getId())
- ->setDisableWorkflowOnSubmit(true)
- ->addSubmitButton(pht('Refresh Account Link'));
- }
- }
-}
diff --git a/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php b/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php
deleted file mode 100644
index cc0cf97998..0000000000
--- a/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php
+++ /dev/null
@@ -1,127 +0,0 @@
-getUser();
- $workspace = $this->getGitWorkspace($repository);
-
- try {
- $this->commitRevisionToWorkspace($revision, $workspace, $viewer);
- } catch (Exception $e) {
- throw new PhutilProxyException(
- pht('Failed to commit patch.'),
- $e);
- }
-
- try {
- $this->pushWorkspaceRepository($repository, $workspace, $viewer);
- } catch (Exception $e) {
- throw new PhutilProxyException(
- pht('Failed to push changes upstream.'),
- $e);
- }
- }
-
- public function commitRevisionToWorkspace(
- DifferentialRevision $revision,
- ArcanistRepositoryAPI $workspace,
- PhabricatorUser $user) {
-
- $diff_id = $revision->loadActiveDiff()->getID();
-
- $call = new ConduitCall(
- 'differential.getrawdiff',
- array(
- 'diffID' => $diff_id,
- ));
-
- $call->setUser($user);
- $raw_diff = $call->execute();
-
- $missing_binary =
- "\nindex "
- ."0000000000000000000000000000000000000000.."
- ."0000000000000000000000000000000000000000\n";
- if (strpos($raw_diff, $missing_binary) !== false) {
- throw new Exception(pht('Patch is missing content for a binary file'));
- }
-
- $future = $workspace->execFutureLocal('apply --index -');
- $future->write($raw_diff);
- $future->resolvex();
-
- $workspace->reloadWorkingCopy();
-
- $call = new ConduitCall(
- 'differential.getcommitmessage',
- array(
- 'revision_id' => $revision->getID(),
- ));
-
- $call->setUser($user);
- $message = $call->execute();
-
- $author = id(new PhabricatorUser())->loadOneWhere(
- 'phid = %s',
- $revision->getAuthorPHID());
-
- $author_string = sprintf(
- '%s <%s>',
- $author->getRealName(),
- $author->loadPrimaryEmailAddress());
- $author_date = $revision->getDateCreated();
-
- $workspace->execxLocal(
- '-c user.name=%s -c user.email=%s '.
- 'commit --date=%s --author=%s '.
- '--message=%s',
- // -c will set the 'committer'
- $user->getRealName(),
- $user->loadPrimaryEmailAddress(),
- $author_date,
- $author_string,
- $message);
- }
-
- public function pushWorkspaceRepository(
- PhabricatorRepository $repository,
- ArcanistRepositoryAPI $workspace,
- PhabricatorUser $user) {
-
- $workspace->execxLocal('push origin HEAD:master');
- }
-
- public function createMenuItem(
- PhabricatorUser $viewer,
- DifferentialRevision $revision,
- PhabricatorRepository $repository) {
-
- $vcs = $repository->getVersionControlSystem();
- if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
- return;
- }
-
- if (!$repository->isHosted()) {
- return;
- }
-
- if (!$repository->isWorkingCopyBare()) {
- return;
- }
-
- // TODO: This temporarily disables this action, because it doesn't work
- // and is confusing to users. If you want to use it, comment out this line
- // for now and we'll provide real support eventually.
- return;
-
- return $this->createActionView(
- $revision,
- pht('Land to Hosted Repository'));
- }
-}
diff --git a/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php b/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php
deleted file mode 100644
index 38f7160958..0000000000
--- a/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php
+++ /dev/null
@@ -1,106 +0,0 @@
-getUser();
-
- $workspace = $this->getMercurialWorkspace($repository);
-
- try {
- $this->commitRevisionToWorkspace($revision, $workspace, $viewer);
- } catch (Exception $e) {
- throw new PhutilProxyException(pht('Failed to commit patch.'), $e);
- }
-
- try {
- $this->pushWorkspaceRepository($repository, $workspace, $viewer);
- } catch (Exception $e) {
- throw new PhutilProxyException(
- pht('Failed to push changes upstream.'),
- $e);
- }
- }
-
- public function commitRevisionToWorkspace(
- DifferentialRevision $revision,
- ArcanistRepositoryAPI $workspace,
- PhabricatorUser $user) {
-
- $diff_id = $revision->loadActiveDiff()->getID();
-
- $call = new ConduitCall(
- 'differential.getrawdiff',
- array(
- 'diffID' => $diff_id,
- ));
-
- $call->setUser($user);
- $raw_diff = $call->execute();
-
- $future = $workspace->execFutureLocal('patch --no-commit -');
- $future->write($raw_diff);
- $future->resolvex();
-
- $workspace->reloadWorkingCopy();
-
- $call = new ConduitCall(
- 'differential.getcommitmessage',
- array(
- 'revision_id' => $revision->getID(),
- ));
-
- $call->setUser($user);
- $message = $call->execute();
-
- $author = id(new PhabricatorUser())->loadOneWhere(
- 'phid = %s',
- $revision->getAuthorPHID());
-
- $author_string = sprintf(
- '%s <%s>',
- $author->getRealName(),
- $author->loadPrimaryEmailAddress());
- $author_date = $revision->getDateCreated();
-
- $workspace->execxLocal(
- 'commit --date=%s --user=%s '.
- '--message=%s',
- $author_date.' 0',
- $author_string,
- $message);
- }
-
-
- public function pushWorkspaceRepository(
- PhabricatorRepository $repository,
- ArcanistRepositoryAPI $workspace,
- PhabricatorUser $user) {
-
- $workspace->execxLocal('push -b default');
- }
-
- public function createMenuItem(
- PhabricatorUser $viewer,
- DifferentialRevision $revision,
- PhabricatorRepository $repository) {
-
- $vcs = $repository->getVersionControlSystem();
- if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL) {
- return;
- }
-
- if (!$repository->isHosted()) {
- return;
- }
-
- return $this->createActionView(
- $revision,
- pht('Land to Hosted Repository'));
- }
-}
diff --git a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
deleted file mode 100644
index 2cf3eaa4e5..0000000000
--- a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
+++ /dev/null
@@ -1,81 +0,0 @@
-listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
- }
-
- public function handleEvent(PhutilEvent $event) {
- switch ($event->getType()) {
- case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
- $this->handleActionsEvent($event);
- break;
- }
- }
-
- private function handleActionsEvent(PhutilEvent $event) {
- $object = $event->getValue('object');
- if ($object instanceof DifferentialRevision) {
- $this->renderRevisionAction($event);
- }
- }
-
- private function renderRevisionAction(PhutilEvent $event) {
- $viewer = $event->getUser();
-
- if (!$this->canUseApplication($viewer)) {
- return null;
- }
-
- $revision = $event->getValue('object');
-
- $repository = $revision->getRepository();
- if ($repository === null) {
- return null;
- }
-
- if ($repository->canPerformAutomation()) {
- $revision_id = $revision->getID();
-
- $op = new DrydockLandRepositoryOperation();
- $barrier = $op->getBarrierToLanding($viewer, $revision);
-
- if ($barrier) {
- $can_land = false;
- } else {
- $can_land = true;
- }
-
- $action = id(new PhabricatorActionView())
- ->setName(pht('Land Revision'))
- ->setIcon('fa-fighter-jet')
- ->setHref("/differential/revision/operation/{$revision_id}/")
- ->setWorkflow(true)
- ->setDisabled(!$can_land);
-
-
- $this->addActionMenuItems($event, $action);
- }
-
- $strategies = id(new PhutilClassMapQuery())
- ->setAncestorClass('DifferentialLandingStrategy')
- ->execute();
-
- foreach ($strategies as $strategy) {
- $action = $strategy->createMenuItem($viewer, $revision, $repository);
- if ($action == null) {
- continue;
- }
- if ($strategy->isActionDisabled($viewer, $revision, $repository)) {
- $action->setDisabled(true);
- }
- $this->addActionMenuItems($event, $action);
- }
- }
-
-}
diff --git a/src/applications/differential/landing/DifferentialLandingStrategy.php b/src/applications/differential/landing/DifferentialLandingStrategy.php
deleted file mode 100644
index 2cec11fefe..0000000000
--- a/src/applications/differential/landing/DifferentialLandingStrategy.php
+++ /dev/null
@@ -1,87 +0,0 @@
-getId();
- return id(new PhabricatorActionView())
- ->setRenderAsForm(true)
- ->setWorkflow(true)
- ->setName($name)
- ->setHref("/differential/revision/land/{$revision_id}/{$strategy}/");
- }
-
- /**
- * Check if this action should be disabled, and explain why.
- *
- * By default, this method checks for push permissions, and for the
- * revision being Accepted.
- *
- * @return False for "not disabled"; human-readable text explaining why, if
- * it is disabled.
- */
- public function isActionDisabled(
- PhabricatorUser $viewer,
- DifferentialRevision $revision,
- PhabricatorRepository $repository) {
-
- $status = $revision->getStatus();
- if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
- return pht('Only Accepted revisions can be landed.');
- }
-
- if (!PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $repository,
- DiffusionPushCapability::CAPABILITY)) {
- return pht('You do not have permissions to push to this repository.');
- }
-
- return false;
- }
-
- /**
- * Might break if repository is not Git.
- */
- protected function getGitWorkspace(PhabricatorRepository $repository) {
- try {
- return DifferentialGetWorkingCopy::getCleanGitWorkspace($repository);
- } catch (Exception $e) {
- throw new PhutilProxyException(
- pht('Failed to allocate a workspace.'),
- $e);
- }
- }
-
- /**
- * Might break if repository is not Mercurial.
- */
- protected function getMercurialWorkspace(PhabricatorRepository $repository) {
- try {
- return DifferentialGetWorkingCopy::getCleanMercurialWorkspace(
- $repository);
- } catch (Exception $e) {
- throw new PhutilProxyException(
- pht('Failed to allocate a workspace.'),
- $e);
- }
- }
-
-}
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index c745d8e42a..88303cf420 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1045,7 +1045,8 @@ final class DifferentialChangesetParser extends Phobject {
}
}
- $this->comments = $this->reorderAndThreadComments($this->comments);
+ $this->comments = id(new PHUIDiffInlineThreader())
+ ->reorderAndThreadCommments($this->comments);
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
@@ -1617,68 +1618,6 @@ final class DifferentialChangesetParser extends Phobject {
return array($old_back, $new_back);
}
- private function reorderAndThreadComments(array $comments) {
- $comments = msort($comments, 'getID');
-
- // Build an empty map of all the comments we actually have. If a comment
- // is a reply but the parent has gone missing, we don't want it to vanish
- // completely.
- $comment_phids = mpull($comments, 'getPHID');
- $replies = array_fill_keys($comment_phids, array());
-
- // Now, remove all comments which are replies, leaving only the top-level
- // comments.
- foreach ($comments as $key => $comment) {
- $reply_phid = $comment->getReplyToCommentPHID();
- if (isset($replies[$reply_phid])) {
- $replies[$reply_phid][] = $comment;
- unset($comments[$key]);
- }
- }
-
- // For each top level comment, add the comment, then add any replies
- // to it. Do this recursively so threads are shown in threaded order.
- $results = array();
- foreach ($comments as $comment) {
- $results[] = $comment;
- $phid = $comment->getPHID();
- $descendants = $this->getInlineReplies($replies, $phid, 1);
- foreach ($descendants as $descendant) {
- $results[] = $descendant;
- }
- }
-
- // If we have anything left, they were cyclic references. Just dump
- // them in a the end. This should be impossible, but users are very
- // creative.
- foreach ($replies as $phid => $comments) {
- foreach ($comments as $comment) {
- $results[] = $comment;
- }
- }
-
- return $results;
- }
-
- private function getInlineReplies(array &$replies, $phid, $depth) {
- $comments = idx($replies, $phid, array());
- unset($replies[$phid]);
-
- $results = array();
- foreach ($comments as $comment) {
- $results[] = $comment;
- $descendants = $this->getInlineReplies(
- $replies,
- $comment->getPHID(),
- $depth + 1);
- foreach ($descendants as $descendant) {
- $results[] = $descendant;
- }
- }
-
- return $results;
- }
-
private function getOffset(array $map, $line) {
if (!$map) {
return null;
diff --git a/src/applications/differential/storage/DifferentialInlineComment.php b/src/applications/differential/storage/DifferentialInlineComment.php
index bdc231671f..cbe05663db 100644
--- a/src/applications/differential/storage/DifferentialInlineComment.php
+++ b/src/applications/differential/storage/DifferentialInlineComment.php
@@ -255,6 +255,13 @@ final class DifferentialInlineComment
return $this;
}
+ public function getDateModified() {
+ return $this->proxy->getDateModified();
+ }
+
+ public function getDateCreated() {
+ return $this->proxy->getDateCreated();
+ }
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php
index 25f762ba66..2787356bd2 100644
--- a/src/applications/differential/view/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/DifferentialChangesetListView.php
@@ -14,6 +14,7 @@ final class DifferentialChangesetListView extends AphrontView {
private $standaloneURI;
private $leftRawFileURI;
private $rightRawFileURI;
+ private $inlineListURI;
private $symbolIndexes = array();
private $repository;
@@ -64,6 +65,15 @@ final class DifferentialChangesetListView extends AphrontView {
return $this;
}
+ public function setInlineListURI($uri) {
+ $this->inlineListURI = $uri;
+ return $this;
+ }
+
+ public function getInlineListURI() {
+ return $this->inlineListURI;
+ }
+
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
@@ -208,6 +218,7 @@ final class DifferentialChangesetListView extends AphrontView {
array(
'changesetViewIDs' => $ids,
'inlineURI' => $this->inlineURI,
+ 'inlineListURI' => $this->inlineListURI,
'pht' => array(
'Open in Editor' => pht('Open in Editor'),
'Show All Context' => pht('Show All Context'),
@@ -257,15 +268,15 @@ final class DifferentialChangesetListView extends AphrontView {
'You must select a comment to mark done.' =>
pht('You must select a comment to mark done.'),
- 'Hide or show inline comment.' =>
- pht('Hide or show inline comment.'),
+ 'Collapse or expand inline comment.' =>
+ pht('Collapse or expand inline comment.'),
'You must select a comment to hide.' =>
pht('You must select a comment to hide.'),
- 'Jump to next inline comment, including hidden comments.' =>
- pht('Jump to next inline comment, including hidden comments.'),
- 'Jump to previous inline comment, including hidden comments.' =>
- pht('Jump to previous inline comment, including hidden comments.'),
+ 'Jump to next inline comment, including collapsed comments.' =>
+ pht('Jump to next inline comment, including collapsed comments.'),
+ 'Jump to previous inline comment, including collapsed comments.' =>
+ pht('Jump to previous inline comment, including collapsed comments.'),
'This file content has been collapsed.' =>
pht('This file content has been collapsed.'),
@@ -279,6 +290,14 @@ final class DifferentialChangesetListView extends AphrontView {
'Unsaved' => pht('Unsaved'),
'Unsubmitted' => pht('Unsubmitted'),
'Comments' => pht('Comments'),
+
+ 'Hide "Done" Inlines' => pht('Hide "Done" Inlines'),
+ 'Hide Collapsed Inlines' => pht('Hide Collapsed Inlines'),
+ 'Hide Older Inlines' => pht('Hide Older Inlines'),
+ 'Hide All Inlines' => pht('Hide All Inlines'),
+ 'Show All Inlines' => pht('Show All Inlines'),
+
+ 'List Inline Comments' => pht('List Inline Comments'),
),
));
diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
index 8fef05bd88..dc1b120138 100644
--- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
+++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
@@ -56,6 +56,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
'repository/(?P.*)' => 'DiffusionRepositoryController',
'change/(?P.*)' => 'DiffusionChangeController',
'history/(?P.*)' => 'DiffusionHistoryController',
+ 'graph/(?P.*)' => 'DiffusionGraphController',
'browse/(?P.*)' => 'DiffusionBrowseController',
'lastmodified/(?P.*)' => 'DiffusionLastModifiedController',
'diff/' => 'DiffusionDiffController',
@@ -140,6 +141,8 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
$this->getEditRoutePattern('edit/') =>
'DiffusionCommitEditController',
),
+ 'picture/(?P[0-9]\d*)/'
+ => 'DiffusionRepositoryProfilePictureController',
),
);
}
diff --git a/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php
index e5e033f416..7f8ae11f57 100644
--- a/src/applications/diffusion/controller/DiffusionBranchTableController.php
+++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php
@@ -48,7 +48,7 @@ final class DiffusionBranchTableController extends DiffusionController {
->withRepository($repository)
->execute();
- $table = id(new DiffusionBranchTableView())
+ $list = id(new DiffusionBranchListView())
->setUser($viewer)
->setBranches($branches)
->setCommits($commits)
@@ -57,7 +57,7 @@ final class DiffusionBranchTableController extends DiffusionController {
$content = id(new PHUIObjectBoxView())
->setHeaderText($repository->getName())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setTable($table)
+ ->setTable($list)
->setPager($pager);
}
@@ -84,10 +84,7 @@ final class DiffusionBranchTableController extends DiffusionController {
$repository->getDisplayName(),
))
->setCrumbs($crumbs)
- ->appendChild(
- array(
- $view,
- ));
+ ->appendChild($view);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php
index 52465da410..abebbace90 100644
--- a/src/applications/diffusion/controller/DiffusionController.php
+++ b/src/applications/diffusion/controller/DiffusionController.php
@@ -204,6 +204,9 @@ abstract class DiffusionController extends PhabricatorController {
case 'history':
$view_name = pht('History');
break;
+ case 'graph':
+ $view_name = pht('Graph');
+ break;
case 'browse':
$view_name = pht('Browse');
break;
diff --git a/src/applications/diffusion/controller/DiffusionGraphController.php b/src/applications/diffusion/controller/DiffusionGraphController.php
new file mode 100644
index 0000000000..8428a8152d
--- /dev/null
+++ b/src/applications/diffusion/controller/DiffusionGraphController.php
@@ -0,0 +1,108 @@
+loadDiffusionContext();
+ if ($response) {
+ return $response;
+ }
+
+ $viewer = $this->getViewer();
+ $drequest = $this->getDiffusionRequest();
+ $repository = $drequest->getRepository();
+
+ $pager = id(new PHUIPagerView())
+ ->readFromRequest($request);
+
+ $params = array(
+ 'commit' => $drequest->getCommit(),
+ 'path' => $drequest->getPath(),
+ 'offset' => $pager->getOffset(),
+ 'limit' => $pager->getPageSize() + 1,
+ );
+
+ $history_results = $this->callConduitWithDiffusionRequest(
+ 'diffusion.historyquery',
+ $params);
+ $history = DiffusionPathChange::newFromConduit(
+ $history_results['pathChanges']);
+
+ $history = $pager->sliceResults($history);
+
+ $graph = id(new DiffusionHistoryTableView())
+ ->setViewer($viewer)
+ ->setDiffusionRequest($drequest)
+ ->setHistory($history);
+
+ $graph->loadRevisions();
+ $show_graph = !strlen($drequest->getPath());
+ if ($show_graph) {
+ $graph->setParents($history_results['parents']);
+ $graph->setIsHead(!$pager->getOffset());
+ $graph->setIsTail(!$pager->getHasMorePages());
+ }
+
+ $header = $this->buildHeader($drequest);
+
+ $crumbs = $this->buildCrumbs(
+ array(
+ 'branch' => true,
+ 'path' => true,
+ 'view' => 'graph',
+ ));
+ $crumbs->setBorder(true);
+
+ $title = array(
+ pht('Graph'),
+ $repository->getDisplayName(),
+ );
+
+ $graph_view = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('History Graph'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($graph)
+ ->setPager($pager);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter($graph_view);
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+ }
+
+ private function buildHeader(DiffusionRequest $drequest) {
+ $viewer = $this->getViewer();
+
+ $tag = $this->renderCommitHashTag($drequest);
+ $history_uri = $drequest->generateURI(
+ array(
+ 'action' => 'history',
+ ));
+
+ $history_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setText(pht('History'))
+ ->setHref($history_uri)
+ ->setIcon('fa-history');
+
+ $header = id(new PHUIHeaderView())
+ ->setUser($viewer)
+ ->setPolicyObject($drequest->getRepository())
+ ->addTag($tag)
+ ->setHeader($this->renderPathLinks($drequest, $mode = 'history'))
+ ->setHeaderIcon('fa-code-fork')
+ ->addActionLink($history_button);
+
+ return $header;
+
+ }
+
+}
diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php
index b8a1877ddf..c2f718f11e 100644
--- a/src/applications/diffusion/controller/DiffusionHistoryController.php
+++ b/src/applications/diffusion/controller/DiffusionHistoryController.php
@@ -77,24 +77,28 @@ final class DiffusionHistoryController extends DiffusionController {
$viewer = $this->getViewer();
$tag = $this->renderCommitHashTag($drequest);
- $browse_uri = $drequest->generateURI(
- array(
- 'action' => 'browse',
- ));
-
- $browse_button = id(new PHUIButtonView())
- ->setTag('a')
- ->setText(pht('Browse'))
- ->setHref($browse_uri)
- ->setIcon('fa-code');
+ $show_graph = !strlen($drequest->getPath());
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setPolicyObject($drequest->getRepository())
->addTag($tag)
->setHeader($this->renderPathLinks($drequest, $mode = 'history'))
- ->setHeaderIcon('fa-clock-o')
- ->addActionLink($browse_button);
+ ->setHeaderIcon('fa-clock-o');
+
+ if ($show_graph) {
+ $graph_uri = $drequest->generateURI(
+ array(
+ 'action' => 'graph',
+ ));
+
+ $graph_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setText(pht('Graph'))
+ ->setHref($graph_uri)
+ ->setIcon('fa-code-fork');
+ $header->addActionLink($graph_button);
+ }
return $header;
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php
index 91b33bcaaa..362f78bd3f 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php
@@ -274,7 +274,9 @@ final class DiffusionRepositoryController extends DiffusionController {
->setHeader($repository->getName())
->setUser($viewer)
->setPolicyObject($repository)
- ->setHeaderIcon('fa-code');
+ ->setProfileHeader(true)
+ ->setImage($repository->getProfileImageURI())
+ ->setImageEditURL('/diffusion/picture/'.$repository->getID().'/');
if (!$repository->isTracked()) {
$header->setStatus('fa-ban', 'dark', pht('Inactive'));
@@ -449,11 +451,11 @@ final class DiffusionRepositoryController extends DiffusionController {
$header->setSubheader(pht('Showing %d branches.', $limit));
}
- $button = new PHUIButtonView();
- $button->setText(pht('Show All'));
- $button->setTag('a');
- $button->setIcon('fa-code-fork');
- $button->setHref($drequest->generateURI(
+ $button = id(new PHUIButtonView())
+ ->setText(pht('Show All'))
+ ->setTag('a')
+ ->setIcon('fa-code-fork')
+ ->setHref($drequest->generateURI(
array(
'action' => 'branches',
)));
@@ -490,7 +492,7 @@ final class DiffusionRepositoryController extends DiffusionController {
->needCommitData(true)
->execute();
- $view = id(new DiffusionTagListView())
+ $view = id(new DiffusionTagTableView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setTags($tags)
@@ -509,11 +511,11 @@ final class DiffusionRepositoryController extends DiffusionController {
pht('Showing the %d most recent tags.', $tag_limit));
}
- $button = new PHUIButtonView();
- $button->setText(pht('Show All Tags'));
- $button->setTag('a');
- $button->setIcon('fa-tag');
- $button->setHref($drequest->generateURI(
+ $button = id(new PHUIButtonView())
+ ->setText(pht('Show All Tags'))
+ ->setTag('a')
+ ->setIcon('fa-tag')
+ ->setHref($drequest->generateURI(
array(
'action' => 'tags',
)));
@@ -565,23 +567,30 @@ final class DiffusionRepositoryController extends DiffusionController {
$history_table->setIsHead(true);
- $icon = id(new PHUIIconView())
- ->setIcon('fa-list-alt');
-
- $button = id(new PHUIButtonView())
- ->setText(pht('View History'))
+ $history = id(new PHUIButtonView())
+ ->setText(pht('History'))
->setHref($drequest->generateURI(
array(
'action' => 'history',
)))
->setTag('a')
- ->setIcon($icon);
+ ->setIcon('fa-history');
+
+ $graph = id(new PHUIButtonView())
+ ->setText(pht('Graph'))
+ ->setHref($drequest->generateURI(
+ array(
+ 'action' => 'graph',
+ )))
+ ->setTag('a')
+ ->setIcon('fa-code-fork');
$panel = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$header = id(new PHUIHeaderView())
->setHeader(pht('Recent Commits'))
- ->addActionLink($button);
+ ->addActionLink($graph)
+ ->addActionLink($history);
$panel->setHeader($header);
$panel->setTable($history_table);
@@ -670,14 +679,11 @@ final class DiffusionRepositoryController extends DiffusionController {
$header = id(new PHUIHeaderView())
->setHeader($repository->getName());
- $icon = id(new PHUIIconView())
- ->setIcon('fa-folder-open');
-
- $button = new PHUIButtonView();
- $button->setText(pht('Browse Repository'));
- $button->setTag('a');
- $button->setIcon($icon);
- $button->setHref($browse_uri);
+ $button = id(new PHUIButtonView())
+ ->setText(pht('Browse'))
+ ->setTag('a')
+ ->setIcon('fa-code')
+ ->setHref($browse_uri);
$header->addActionLink($button);
$browse_panel->setHeader($header);
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php b/src/applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php
new file mode 100644
index 0000000000..e6256989b5
--- /dev/null
+++ b/src/applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php
@@ -0,0 +1,246 @@
+getViewer();
+ $id = $request->getURIData('id');
+
+ $repository = id(new PhabricatorRepositoryQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->needProfileImage(true)
+ ->needURIs(true)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+
+ if (!$repository) {
+ return new Aphront404Response();
+ }
+
+ $supported_formats = PhabricatorFile::getTransformableImageFormats();
+ $e_file = true;
+ $errors = array();
+ $done_uri = $repository->getURI();
+
+ if ($request->isFormPost()) {
+ $phid = $request->getStr('phid');
+ $is_default = false;
+ if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
+ $phid = null;
+ $is_default = true;
+ } else if ($phid) {
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($phid))
+ ->executeOne();
+ } else {
+ if ($request->getFileExists('picture')) {
+ $file = PhabricatorFile::newFromPHPUpload(
+ $_FILES['picture'],
+ array(
+ 'authorPHID' => $viewer->getPHID(),
+ 'canCDN' => true,
+ ));
+ } else {
+ $e_file = pht('Required');
+ $errors[] = pht(
+ 'You must choose a file when uploading a new profile picture.');
+ }
+ }
+
+ if (!$errors && !$is_default) {
+ if (!$file->isTransformableImage()) {
+ $e_file = pht('Not Supported');
+ $errors[] = pht(
+ 'This server only supports these image formats: %s.',
+ implode(', ', $supported_formats));
+ } else {
+ $xform = PhabricatorFileTransform::getTransformByKey(
+ PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
+ $xformed = $xform->executeTransform($file);
+ }
+ }
+
+ if (!$errors) {
+ if ($is_default) {
+ $repository->setProfileImagePHID(null);
+ } else {
+ $repository->setProfileImagePHID($xformed->getPHID());
+ $xformed->attachToObject($repository->getPHID());
+ }
+ $repository->save();
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
+ }
+ }
+
+ $title = pht('Edit Picture');
+
+ $form = id(new PHUIFormLayoutView())
+ ->setUser($viewer);
+
+ $default_image = PhabricatorFile::loadBuiltin(
+ $viewer, 'repo/code.png');
+
+ $images = array();
+
+ $current = $repository->getProfileImagePHID();
+ $has_current = false;
+ if ($current) {
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($current))
+ ->execute();
+ if ($files) {
+ $file = head($files);
+ if ($file->isTransformableImage()) {
+ $has_current = true;
+ $images[$current] = array(
+ 'uri' => $file->getBestURI(),
+ 'tip' => pht('Current Picture'),
+ );
+ }
+ }
+ }
+
+ $builtins = array(
+ 'repo/building.png',
+ 'repo/cloud.png',
+ 'repo/commit.png',
+ 'repo/database.png',
+ 'repo/desktop.png',
+ 'repo/gears.png',
+ 'repo/globe.png',
+ 'repo/locked.png',
+ 'repo/microchip.png',
+ 'repo/mobile.png',
+ 'repo/repo.png',
+ 'repo/servers.png',
+ );
+ foreach ($builtins as $builtin) {
+ $file = PhabricatorFile::loadBuiltin($viewer, $builtin);
+ $images[$file->getPHID()] = array(
+ 'uri' => $file->getBestURI(),
+ 'tip' => pht('Builtin Image'),
+ );
+ }
+
+ $images[PhabricatorPHIDConstants::PHID_VOID] = array(
+ 'uri' => $default_image->getBestURI(),
+ 'tip' => pht('Default Picture'),
+ );
+
+ require_celerity_resource('people-profile-css');
+ Javelin::initBehavior('phabricator-tooltips', array());
+
+ $buttons = array();
+ foreach ($images as $phid => $spec) {
+ $style = null;
+ if (isset($spec['style'])) {
+ $style = $spec['style'];
+ }
+ $button = javelin_tag(
+ 'button',
+ array(
+ 'class' => 'button-grey profile-image-button',
+ 'sigil' => 'has-tooltip',
+ 'meta' => array(
+ 'tip' => $spec['tip'],
+ 'size' => 300,
+ ),
+ ),
+ phutil_tag(
+ 'img',
+ array(
+ 'height' => 50,
+ 'width' => 50,
+ 'src' => $spec['uri'],
+ )));
+
+ $button = array(
+ phutil_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => 'phid',
+ 'value' => $phid,
+ )),
+ $button,
+ );
+
+ $button = phabricator_form(
+ $viewer,
+ array(
+ 'class' => 'profile-image-form',
+ 'method' => 'POST',
+ ),
+ $button);
+
+ $buttons[] = $button;
+ }
+
+ if ($has_current) {
+ $form->appendChild(
+ id(new AphrontFormMarkupControl())
+ ->setLabel(pht('Current Picture'))
+ ->setValue(array_shift($buttons)));
+ }
+
+ $form->appendChild(
+ id(new AphrontFormMarkupControl())
+ ->setLabel(pht('Use Picture'))
+ ->setValue($buttons));
+
+ $form_box = id(new PHUIObjectBoxView())
+ ->setHeaderText($title)
+ ->setFormErrors($errors)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setForm($form);
+
+ $upload_form = id(new AphrontFormView())
+ ->setUser($viewer)
+ ->setEncType('multipart/form-data')
+ ->appendChild(
+ id(new AphrontFormFileControl())
+ ->setName('picture')
+ ->setLabel(pht('Upload Picture'))
+ ->setError($e_file)
+ ->setCaption(
+ pht('Supported formats: %s', implode(', ', $supported_formats))))
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->addCancelButton($done_uri)
+ ->setValue(pht('Upload Picture')));
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Edit Repository Picture'))
+ ->setHeaderIcon('fa-camera-retro');
+
+ $crumbs = $this->buildApplicationCrumbs();
+ $crumbs->addTextCrumb($repository->getName(), $repository->getURI());
+ $crumbs->addTextCrumb(pht('Edit Picture'));
+ $crumbs->setBorder(true);
+
+ $upload_box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Upload New Picture'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setForm($upload_form);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter(array(
+ $form_box,
+ $upload_box,
+ ));
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+ }
+}
diff --git a/src/applications/diffusion/controller/DiffusionTagListController.php b/src/applications/diffusion/controller/DiffusionTagListController.php
index df3b356f5d..5e765ddcb6 100644
--- a/src/applications/diffusion/controller/DiffusionTagListController.php
+++ b/src/applications/diffusion/controller/DiffusionTagListController.php
@@ -64,17 +64,21 @@ final class DiffusionTagListController extends DiffusionController {
->needCommitData(true)
->execute();
- $view = id(new DiffusionTagListView())
+ $tag_list = id(new DiffusionTagListView())
->setTags($tags)
->setUser($viewer)
->setCommits($commits)
->setDiffusionRequest($drequest);
- $phids = $view->getRequiredHandlePHIDs();
+ $phids = $tag_list->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
- $view->setHandles($handles);
+ $tag_list->setHandles($handles);
- $content = $view;
+ $content = id(new PHUIObjectBoxView())
+ ->setHeaderText($repository->getDisplayName())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($tag_list)
+ ->setPager($pager);
}
$crumbs = $this->buildCrumbs(
@@ -84,17 +88,9 @@ final class DiffusionTagListController extends DiffusionController {
));
$crumbs->setBorder(true);
- $box = id(new PHUIObjectBoxView())
- ->setHeaderText($repository->getDisplayName())
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setTable($view)
- ->setPager($pager);
-
$view = id(new PHUITwoColumnView())
->setHeader($header)
- ->setFooter(array(
- $box,
- ));
+ ->setFooter($content);
return $this->newPage()
->setTitle(
@@ -103,7 +99,8 @@ final class DiffusionTagListController extends DiffusionController {
$repository->getDisplayName(),
))
->setCrumbs($crumbs)
- ->appendChild($view);
+ ->appendChild($view)
+ ->addClass('diffusion-history-view');
}
}
diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php
index ab08025349..1a46d43adf 100644
--- a/src/applications/diffusion/request/DiffusionRequest.php
+++ b/src/applications/diffusion/request/DiffusionRequest.php
@@ -143,6 +143,7 @@ abstract class DiffusionRequest extends Phobject {
$query = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
+ ->needProfileImage(true)
->needURIs(true);
if ($need_edit) {
diff --git a/src/applications/diffusion/view/DiffusionBranchListView.php b/src/applications/diffusion/view/DiffusionBranchListView.php
new file mode 100644
index 0000000000..8d1df82dc9
--- /dev/null
+++ b/src/applications/diffusion/view/DiffusionBranchListView.php
@@ -0,0 +1,138 @@
+branches = $branches;
+ return $this;
+ }
+
+ public function setCommits(array $commits) {
+ assert_instances_of($commits, 'PhabricatorRepositoryCommit');
+ $this->commits = mpull($commits, null, 'getCommitIdentifier');
+ return $this;
+ }
+
+ public function render() {
+ $drequest = $this->getDiffusionRequest();
+ $current_branch = $drequest->getBranch();
+ $repository = $drequest->getRepository();
+ $commits = $this->commits;
+ $viewer = $this->getUser();
+ require_celerity_resource('diffusion-history-css');
+
+ $buildables = $this->loadBuildables($commits);
+ $have_builds = false;
+
+ $can_close_branches = ($repository->isHg());
+
+ Javelin::initBehavior('phabricator-tooltips');
+
+ $doc_href = PhabricatorEnv::getDoclink('Diffusion User Guide: Autoclose');
+ $list = id(new PHUIObjectItemListView())
+ ->setFlush(true)
+ ->addClass('diffusion-history-list')
+ ->addClass('diffusion-branch-list');
+
+ foreach ($this->branches as $branch) {
+ $build_view = null;
+ $button_bar = new PHUIButtonBarView();
+ $commit = idx($commits, $branch->getCommitIdentifier());
+ if ($commit) {
+ $details = $commit->getSummary();
+ $datetime = phabricator_datetime($commit->getEpoch(), $viewer);
+
+ $buildable = idx($buildables, $commit->getPHID());
+ if ($buildable) {
+ $build_view = $this->renderBuildable($buildable, 'button');
+ }
+ } else {
+ $datetime = null;
+ $details = null;
+ }
+
+ if ($repository->supportsBranchComparison()) {
+ $compare_uri = $drequest->generateURI(
+ array(
+ 'action' => 'compare',
+ 'head' => $branch->getShortName(),
+ ));
+ $can_compare = ($branch->getShortName() != $current_branch);
+ if ($can_compare) {
+ $button_bar->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-balance-scale')
+ ->setToolTip(pht('Compare'))
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
+ ->setWorkflow(true)
+ ->setHref($compare_uri));
+ }
+ }
+
+ $fields = $branch->getRawFields();
+ $closed = idx($fields, 'closed');
+ if ($closed) {
+ $status = pht('Closed');
+ } else {
+ $status = pht('Open');
+ }
+
+ $browse_href = $drequest->generateURI(
+ array(
+ 'action' => 'browse',
+ 'branch' => $branch->getShortName(),
+ ));
+
+ $button_bar->addButton(
+ id(new PHUIButtonView())
+ ->setIcon('fa-code')
+ ->setHref($browse_href)
+ ->setTag('a')
+ ->setTooltip(pht('Browse'))
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE));
+
+ $commit_link = $repository->getCommitURI(
+ $branch->getCommitIdentifier());
+
+ $commit_name = $repository->formatCommitName(
+ $branch->getCommitIdentifier(), $local = true);
+
+ $commit_tag = id(new PHUITagView())
+ ->setName($commit_name)
+ ->setHref($commit_link)
+ ->setType(PHUITagView::TYPE_SHADE)
+ ->setColor(PHUITagView::COLOR_INDIGO)
+ ->setBorder(PHUITagView::BORDER_NONE)
+ ->setSlimShady(true);
+ $subhead = array($commit_tag, ' ', $details);
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($branch->getShortName())
+ ->setHref($drequest->generateURI(
+ array(
+ 'action' => 'history',
+ 'branch' => $branch->getShortName(),
+ )))
+ ->setSubhead($subhead)
+ ->setSideColumn(array(
+ $build_view,
+ $button_bar,
+ ));
+
+ if ($branch->getShortName() == $repository->getDefaultBranch()) {
+ $item->setStatusIcon('fa-code-fork', pht('Default Branch'));
+ }
+ $item->addAttribute(array($datetime));
+
+ $list->addItem($item);
+
+ }
+ return $list;
+
+ }
+}
diff --git a/src/applications/diffusion/view/DiffusionHistoryListView.php b/src/applications/diffusion/view/DiffusionHistoryListView.php
index 9d6727a357..9dfa46cbef 100644
--- a/src/applications/diffusion/view/DiffusionHistoryListView.php
+++ b/src/applications/diffusion/view/DiffusionHistoryListView.php
@@ -119,19 +119,7 @@ final class DiffusionHistoryListView extends DiffusionHistoryView {
if ($show_builds) {
$buildable = idx($buildables, $commit->getPHID());
if ($buildable !== null) {
- $status = $buildable->getBuildableStatus();
- $icon = HarbormasterBuildable::getBuildableStatusIcon($status);
- $color = HarbormasterBuildable::getBuildableStatusColor($status);
- $name = HarbormasterBuildable::getBuildableStatusName($status);
- $build_view = id(new PHUIButtonView())
- ->setTag('a')
- ->setText($name)
- ->setIcon($icon)
- ->setColor($color)
- ->setHref('/'.$buildable->getMonogram())
- ->addClass('mmr')
- ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
- ->addClass('diffusion-list-build-status');
+ $build_view = $this->renderBuildable($buildable, 'button');
}
}
diff --git a/src/applications/diffusion/view/DiffusionTagListView.php b/src/applications/diffusion/view/DiffusionTagListView.php
index df59925522..f9030581e3 100644
--- a/src/applications/diffusion/view/DiffusionTagListView.php
+++ b/src/applications/diffusion/view/DiffusionTagListView.php
@@ -29,36 +29,28 @@ final class DiffusionTagListView extends DiffusionView {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$viewer = $this->getViewer();
+ require_celerity_resource('diffusion-history-css');
$buildables = $this->loadBuildables($this->commits);
- $has_builds = false;
- $rows = array();
+ $list = id(new PHUIObjectItemListView())
+ ->setFlush(true)
+ ->addClass('diffusion-history-list');
foreach ($this->tags as $tag) {
$commit = idx($this->commits, $tag->getCommitIdentifier());
+ $button_bar = new PHUIButtonBarView();
- $tag_link = phutil_tag(
- 'a',
+ $tag_href = $drequest->generateURI(
array(
- 'href' => $drequest->generateURI(
- array(
- 'action' => 'browse',
- 'commit' => $tag->getName(),
- )),
- ),
- $tag->getName());
+ 'action' => 'history',
+ 'commit' => $tag->getName(),
+ ));
- $commit_link = phutil_tag(
- 'a',
+ $commit_href = $drequest->generateURI(
array(
- 'href' => $drequest->generateURI(
- array(
- 'action' => 'commit',
- 'commit' => $tag->getCommitIdentifier(),
- )),
- ),
- $repository->formatCommitName(
- $tag->getCommitIdentifier()));
+ 'action' => 'commit',
+ 'commit' => $tag->getCommitIdentifier(),
+ ));
$author = null;
if ($commit && $commit->getAuthorPHID()) {
@@ -69,6 +61,15 @@ final class DiffusionTagListView extends DiffusionView {
$author = self::renderName($tag->getAuthor());
}
+ $committed = phabricator_datetime($commit->getEpoch(), $viewer);
+ $author_name = phutil_tag(
+ 'strong',
+ array(
+ 'class' => 'diffusion-history-author-name',
+ ),
+ $author);
+ $authored = pht('%s on %s.', $author_name, $committed);
+
$description = null;
if ($tag->getType() == 'git/tag') {
// In Git, a tag may be a "real" tag, or just a reference to a commit.
@@ -83,58 +84,71 @@ final class DiffusionTagListView extends DiffusionView {
}
}
- $build = null;
+ $build_view = null;
if ($commit) {
$buildable = idx($buildables, $commit->getPHID());
if ($buildable) {
- $build = $this->renderBuildable($buildable);
- $has_builds = true;
+ $build_view = $this->renderBuildable($buildable, 'button');
}
}
- $history = $this->linkTagHistory($tag->getName());
+ if ($repository->supportsBranchComparison()) {
+ $compare_uri = $drequest->generateURI(
+ array(
+ 'action' => 'compare',
+ 'head' => $tag->getName(),
+ ));
- $rows[] = array(
- $history,
- $tag_link,
- $commit_link,
- $build,
- $author,
- $description,
- $viewer->formatShortDateTime($tag->getEpoch()),
- );
- }
+ $button_bar->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-balance-scale')
+ ->setToolTip(pht('Compare'))
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
+ ->setWorkflow(true)
+ ->setHref($compare_uri));
+ }
- $table = id(new AphrontTableView($rows))
- ->setHeaders(
+ $commit_name = $repository->formatCommitName(
+ $tag->getCommitIdentifier(), $local = true);
+
+ $browse_href = $drequest->generateURI(
array(
- null,
- pht('Tag'),
- pht('Commit'),
- null,
- pht('Author'),
- pht('Description'),
- pht('Created'),
- ))
- ->setColumnClasses(
- array(
- 'nudgeright',
- 'pri',
- '',
- '',
- '',
- 'wide',
- 'right',
- ))
- ->setColumnVisibility(
- array(
- true,
- true,
- true,
- $has_builds,
+ 'action' => 'browse',
+ 'commit' => $tag->getName(),
));
- return $table->render();
+ $button_bar->addButton(
+ id(new PHUIButtonView())
+ ->setTooltip(pht('Browse'))
+ ->setIcon('fa-code')
+ ->setHref($browse_href)
+ ->setTag('a')
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE));
+
+ $commit_tag = id(new PHUITagView())
+ ->setName($commit_name)
+ ->setHref($commit_href)
+ ->setType(PHUITagView::TYPE_SHADE)
+ ->setColor(PHUITagView::COLOR_INDIGO)
+ ->setBorder(PHUITagView::BORDER_NONE)
+ ->setSlimShady(true);
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($tag->getName())
+ ->setHref($tag_href)
+ ->addAttribute(array($commit_tag))
+ ->addAttribute($description)
+ ->addAttribute($authored)
+ ->setSideColumn(array(
+ $build_view,
+ $button_bar,
+ ));
+
+ $list->addItem($item);
+ }
+
+ return $list;
}
}
diff --git a/src/applications/diffusion/view/DiffusionTagTableView.php b/src/applications/diffusion/view/DiffusionTagTableView.php
new file mode 100644
index 0000000000..59a06353ab
--- /dev/null
+++ b/src/applications/diffusion/view/DiffusionTagTableView.php
@@ -0,0 +1,140 @@
+tags = $tags;
+ return $this;
+ }
+
+ public function setCommits(array $commits) {
+ $this->commits = mpull($commits, null, 'getCommitIdentifier');
+ return $this;
+ }
+
+ public function setHandles(array $handles) {
+ $this->handles = $handles;
+ return $this;
+ }
+
+ public function getRequiredHandlePHIDs() {
+ return array_filter(mpull($this->commits, 'getAuthorPHID'));
+ }
+
+ public function render() {
+ $drequest = $this->getDiffusionRequest();
+ $repository = $drequest->getRepository();
+ $viewer = $this->getViewer();
+
+ $buildables = $this->loadBuildables($this->commits);
+ $has_builds = false;
+
+ $rows = array();
+ foreach ($this->tags as $tag) {
+ $commit = idx($this->commits, $tag->getCommitIdentifier());
+
+ $tag_link = phutil_tag(
+ 'a',
+ array(
+ 'href' => $drequest->generateURI(
+ array(
+ 'action' => 'browse',
+ 'commit' => $tag->getName(),
+ )),
+ ),
+ $tag->getName());
+
+ $commit_link = phutil_tag(
+ 'a',
+ array(
+ 'href' => $drequest->generateURI(
+ array(
+ 'action' => 'commit',
+ 'commit' => $tag->getCommitIdentifier(),
+ )),
+ ),
+ $repository->formatCommitName(
+ $tag->getCommitIdentifier()));
+
+ $author = null;
+ if ($commit && $commit->getAuthorPHID()) {
+ $author = $this->handles[$commit->getAuthorPHID()]->renderLink();
+ } else if ($commit && $commit->getCommitData()) {
+ $author = self::renderName($commit->getCommitData()->getAuthorName());
+ } else {
+ $author = self::renderName($tag->getAuthor());
+ }
+
+ $description = null;
+ if ($tag->getType() == 'git/tag') {
+ // In Git, a tag may be a "real" tag, or just a reference to a commit.
+ // If it's a real tag, use the message on the tag, since this may be
+ // unique data which isn't otherwise available.
+ $description = $tag->getDescription();
+ } else {
+ if ($commit) {
+ $description = $commit->getSummary();
+ } else {
+ $description = $tag->getDescription();
+ }
+ }
+
+ $build = null;
+ if ($commit) {
+ $buildable = idx($buildables, $commit->getPHID());
+ if ($buildable) {
+ $build = $this->renderBuildable($buildable);
+ $has_builds = true;
+ }
+ }
+
+ $history = $this->linkTagHistory($tag->getName());
+
+ $rows[] = array(
+ $history,
+ $tag_link,
+ $commit_link,
+ $build,
+ $author,
+ $description,
+ $viewer->formatShortDateTime($tag->getEpoch()),
+ );
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ null,
+ pht('Tag'),
+ pht('Commit'),
+ null,
+ pht('Author'),
+ pht('Description'),
+ pht('Created'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'nudgeright',
+ 'pri',
+ '',
+ '',
+ '',
+ 'wide',
+ 'right',
+ ))
+ ->setColumnVisibility(
+ array(
+ true,
+ true,
+ true,
+ $has_builds,
+ ));
+
+ return $table->render();
+ }
+
+}
diff --git a/src/applications/diffusion/view/DiffusionView.php b/src/applications/diffusion/view/DiffusionView.php
index 1d89a9dbd7..d058dc5cec 100644
--- a/src/applications/diffusion/view/DiffusionView.php
+++ b/src/applications/diffusion/view/DiffusionView.php
@@ -116,10 +116,10 @@ abstract class DiffusionView extends AphrontView {
if ($button) {
return id(new PHUIButtonView())
- ->setText(pht('Browse'))
+ ->setTag('a')
->setIcon('fa-code')
->setHref($href)
- ->setTag('a')
+ ->setToolTip(pht('Browse'))
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE);
}
@@ -190,7 +190,8 @@ abstract class DiffusionView extends AphrontView {
}
final protected function renderBuildable(
- HarbormasterBuildable $buildable) {
+ HarbormasterBuildable $buildable,
+ $type = null) {
$status = $buildable->getBuildableStatus();
Javelin::initBehavior('phabricator-tooltips');
@@ -198,6 +199,18 @@ abstract class DiffusionView extends AphrontView {
$color = HarbormasterBuildable::getBuildableStatusColor($status);
$name = HarbormasterBuildable::getBuildableStatusName($status);
+ if ($type == 'button') {
+ return id(new PHUIButtonView())
+ ->setTag('a')
+ ->setText($name)
+ ->setIcon($icon)
+ ->setColor($color)
+ ->setHref('/'.$buildable->getMonogram())
+ ->addClass('mmr')
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
+ ->addClass('diffusion-list-build-status');
+ }
+
return id(new PHUIIconView())
->setIcon($icon.' '.$color)
->addSigil('has-tooltip')
diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php
index a2de5ca6ab..071ee03991 100644
--- a/src/applications/files/query/PhabricatorFileQuery.php
+++ b/src/applications/files/query/PhabricatorFileQuery.php
@@ -18,6 +18,7 @@ final class PhabricatorFileQuery
private $isDeleted;
private $needTransforms;
private $builtinKeys;
+ private $isBuiltin;
public function withIDs(array $ids) {
$this->ids = $ids;
@@ -54,6 +55,11 @@ final class PhabricatorFileQuery
return $this;
}
+ public function withIsBuiltin($is_builtin) {
+ $this->isBuiltin = $is_builtin;
+ return $this;
+ }
+
/**
* Select files which are transformations of some other file. For example,
* you can use this query to find previously generated thumbnails of an image
@@ -416,6 +422,18 @@ final class PhabricatorFileQuery
$this->builtinKeys);
}
+ if ($this->isBuiltin !== null) {
+ if ($this->isBuiltin) {
+ $where[] = qsprintf(
+ $conn,
+ 'builtinKey IS NOT NULL');
+ } else {
+ $where[] = qsprintf(
+ $conn,
+ 'builtinKey IS NULL');
+ }
+ }
+
return $where;
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index 9fdae4445a..19f0bed90a 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -393,6 +393,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
+ unset($tmp);
}
try {
diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php
index cda07d590b..e3242af329 100644
--- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php
+++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php
@@ -186,12 +186,18 @@ abstract class PhabricatorFileUploadSource
$actual_length = strlen($data);
$rope->removeBytesFromHead($actual_length);
- $chunk_data = PhabricatorFile::newFromFileData(
- $data,
- array(
- 'name' => $file->getMonogram().'.chunk-'.$offset,
- 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
- ));
+ $params = array(
+ 'name' => $file->getMonogram().'.chunk-'.$offset,
+ 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
+ );
+
+ // If this isn't the initial chunk, provide a dummy MIME type so we do not
+ // try to detect it. See T12857.
+ if ($offset > 0) {
+ $params['mime-type'] = 'application/octet-stream';
+ }
+
+ $chunk_data = PhabricatorFile::newFromFileData($data, $params);
$chunk = PhabricatorFileChunk::initializeNewChunk(
$file->getStorageHandle(),
@@ -208,11 +214,17 @@ abstract class PhabricatorFileUploadSource
}
private function getNewFileParameters() {
- return array(
+ $parameters = array(
'name' => $this->getName(),
- 'ttl.relative' => $this->getRelativeTTL(),
'viewPolicy' => $this->getViewPolicy(),
);
+
+ $ttl = $this->getRelativeTTL();
+ if ($ttl !== null) {
+ $parameters['ttl.relative'] = $ttl;
+ }
+
+ return $parameters;
}
private function getChunkEngine() {
diff --git a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
index 1e571190d7..58190f6a89 100644
--- a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
+++ b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
@@ -194,11 +194,14 @@ final class ManiphestTaskTestCase extends PhabricatorTestCase {
$dst,
$is_after);
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head($keyword_map[$pri]);
+
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($pri);
+ ->setNewValue($keyword);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE)
@@ -217,11 +220,14 @@ final class ManiphestTaskTestCase extends PhabricatorTestCase {
$target_priority,
$is_end);
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head($keyword_map[$pri]);
+
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($pri);
+ ->setNewValue($keyword);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE)
diff --git a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php b/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php
index 0e11530b51..1b083d88f9 100644
--- a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php
+++ b/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php
@@ -285,6 +285,11 @@ final class ManiphestTaskEditBulkJobType
'=' => array_fuse($value),
));
break;
+ case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE:
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $value));
+ $xaction->setNewValue($keyword);
+ break;
default:
$xaction->setNewValue($value);
break;
diff --git a/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php b/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php
index 5d7bbb1eea..ef966a50da 100644
--- a/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php
+++ b/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php
@@ -49,18 +49,8 @@ final class ManiphestPriorityEmailCommand
array $argv) {
$xactions = array();
- $target = phutil_utf8_strtolower(head($argv));
- $priority = null;
-
- $keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
- foreach ($keywords as $key => $words) {
- foreach ($words as $word) {
- if ($word == $target) {
- $priority = $key;
- break;
- }
- }
- }
+ $keyword = phutil_utf8_strtolower(head($argv));
+ $priority = ManiphestTaskPriority::getTaskPriorityFromKeyword($keyword);
if ($priority === null) {
return array();
@@ -72,7 +62,7 @@ final class ManiphestPriorityEmailCommand
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($priority);
+ ->setNewValue($keyword);
return $xactions;
}
diff --git a/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php
index 5a6a8cea33..640e30fee5 100644
--- a/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php
@@ -99,7 +99,9 @@ abstract class ManiphestConduitAPIMethod extends ConduitAPIMethod {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(pht('Priority set to invalid value.'));
}
- $changes[ManiphestTaskPriorityTransaction::TRANSACTIONTYPE] = $priority;
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $priority));
+ $changes[ManiphestTaskPriorityTransaction::TRANSACTIONTYPE] = $keyword;
}
$owner_phid = $request->getValue('ownerPHID');
diff --git a/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php
index 971be8820b..28afaa4fe1 100644
--- a/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php
@@ -33,4 +33,14 @@ final class ManiphestQueryStatusesConduitAPIMethod
return $results;
}
+ public function getMethodStatus() {
+ return self::METHOD_STATUS_FROZEN;
+ }
+
+ public function getMethodStatusDescription() {
+ return pht(
+ 'This method is frozen and will eventually be deprecated. New code '.
+ 'should use "maniphest.status.search" instead.');
+ }
+
}
diff --git a/src/applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php
new file mode 100644
index 0000000000..7185cde96f
--- /dev/null
+++ b/src/applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php
@@ -0,0 +1,52 @@
+';
+ }
+
+ public function getRequiredScope() {
+ return self::SCOPE_ALWAYS;
+ }
+
+ protected function execute(ConduitAPIRequest $request) {
+ $config = PhabricatorEnv::getEnvConfig('maniphest.statuses');
+ $results = array();
+ foreach ($config as $code => $status) {
+ $stripped_status = array(
+ 'name' => $status['name'],
+ 'value' => $code,
+ 'closed' => !empty($status['closed']),
+ );
+
+ if (isset($status['special'])) {
+ $stripped_status['special'] = $status['special'];
+ }
+
+ $results[] = $stripped_status;
+ }
+
+ return array('data' => $results);
+ }
+
+}
diff --git a/src/applications/maniphest/constants/ManiphestTaskPriority.php b/src/applications/maniphest/constants/ManiphestTaskPriority.php
index dd2ab69c69..86c7adbcb7 100644
--- a/src/applications/maniphest/constants/ManiphestTaskPriority.php
+++ b/src/applications/maniphest/constants/ManiphestTaskPriority.php
@@ -2,6 +2,8 @@
final class ManiphestTaskPriority extends ManiphestConstants {
+ const UNKNOWN_PRIORITY_KEYWORD = '!!unknown!!';
+
/**
* Get the priorities and their full descriptions.
*
@@ -41,6 +43,58 @@ final class ManiphestTaskPriority extends ManiphestConstants {
return $map;
}
+ /**
+ * Get the canonical keyword for a given priority constant.
+ *
+ * @return string|null Keyword, or `null` if no keyword is configured.
+ */
+ public static function getKeywordForTaskPriority($priority) {
+ $map = self::getConfig();
+
+ $spec = idx($map, $priority);
+ if (!$spec) {
+ return null;
+ }
+
+ $keywords = idx($spec, 'keywords');
+ if (!$keywords) {
+ return null;
+ }
+
+ return head($keywords);
+ }
+
+
+ /**
+ * Get a map of supported alternate names for each priority.
+ *
+ * Keys are aliases, like "wish" and "wishlist". Values are canonical
+ * priority keywords, like "wishlist".
+ *
+ * @return map Map of aliases to canonical priority keywords.
+ */
+ public static function getTaskPriorityAliasMap() {
+ $keyword_map = self::getTaskPriorityKeywordsMap();
+
+ $result = array();
+ foreach ($keyword_map as $key => $keywords) {
+ $target = self::getKeywordForTaskPriority($key);
+ if ($target === null) {
+ continue;
+ }
+
+ // NOTE: Include the raw priority value, like "25", in the list of
+ // aliases. This supports legacy sources like saved EditEngine forms.
+ $result[$key] = $target;
+
+ foreach ($keywords as $keyword) {
+ $result[$keyword] = $target;
+ }
+ }
+
+ return $result;
+ }
+
/**
* Get the priorities and their related short (one-word) descriptions.
@@ -105,6 +159,18 @@ final class ManiphestTaskPriority extends ManiphestConstants {
return 'fa-arrow-right';
}
+ public static function getTaskPriorityFromKeyword($keyword) {
+ $map = self::getTaskPriorityKeywordsMap();
+
+ foreach ($map as $priority => $keywords) {
+ if (in_array($keyword, $keywords)) {
+ return $priority;
+ }
+ }
+
+ return null;
+ }
+
public static function isDisabledPriority($priority) {
$config = idx(self::getConfig(), $priority, array());
return idx($config, 'disabled', false);
@@ -116,6 +182,18 @@ final class ManiphestTaskPriority extends ManiphestConstants {
return $config;
}
+ private static function isValidPriorityKeyword($keyword) {
+ if (!strlen($keyword) || strlen($keyword) > 64) {
+ return false;
+ }
+
+ // Alphanumeric, but not exclusively numeric
+ if (!preg_match('/^(?![0-9]*$)[a-zA-Z0-9]+$/', $keyword)) {
+ return false;
+ }
+ return true;
+ }
+
public static function validateConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
@@ -147,9 +225,24 @@ final class ManiphestTaskPriority extends ManiphestConstants {
'name' => 'string',
'short' => 'optional string',
'color' => 'optional string',
- 'keywords' => 'optional list',
+ 'keywords' => 'list',
'disabled' => 'optional bool',
));
+
+ $keywords = $value['keywords'];
+ foreach ($keywords as $keyword) {
+ if (!self::isValidPriorityKeyword($keyword)) {
+ throw new Exception(
+ pht(
+ 'Key "%s" is not a valid priority keyword. Priority keywords '.
+ 'must be 1-64 alphanumeric characters and cannot be '.
+ 'exclusively digits. For example, "%s" or "%s" are '.
+ 'reasonable choices.',
+ $keyword,
+ 'low',
+ 'critical'));
+ }
+ }
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php
index 6781fb7724..53d2e1afe3 100644
--- a/src/applications/maniphest/constants/ManiphestTaskStatus.php
+++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php
@@ -232,16 +232,17 @@ final class ManiphestTaskStatus extends ManiphestConstants {
* @task validate
*/
public static function isValidStatusConstant($constant) {
- if (strlen($constant) > 12) {
+ if (!strlen($constant) || strlen($constant) > 64) {
return false;
}
- if (!preg_match('/^[a-z0-9]+\z/', $constant)) {
+
+ // Alphanumeric, but not exclusively numeric
+ if (!preg_match('/^(?![0-9]*$)[a-zA-Z0-9]+$/', $constant)) {
return false;
}
return true;
}
-
/**
* @task validate
*/
@@ -250,10 +251,9 @@ final class ManiphestTaskStatus extends ManiphestConstants {
if (!self::isValidStatusConstant($key)) {
throw new Exception(
pht(
- 'Key "%s" is not a valid status constant. Status constants must '.
- 'be 1-12 characters long and contain only lowercase letters (a-z) '.
- 'and digits (0-9). For example, "%s" or "%s" are reasonable '.
- 'choices.',
+ 'Key "%s" is not a valid status constant. Status constants '.
+ 'must be 1-64 alphanumeric characters and cannot be exclusively '.
+ 'digits. For example, "%s" or "%s" are reasonable choices.',
$key,
'open',
'closed'));
diff --git a/src/applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php b/src/applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php
index 20f4c438a7..140c86d6dd 100644
--- a/src/applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php
+++ b/src/applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php
@@ -10,10 +10,15 @@ final class ManiphestTaskStatusTestCase extends PhabricatorTestCase {
'duplicate2' => true,
'' => false,
- 'longlonglonglong' => false,
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' =>
+ false,
'.' => false,
- 'ABCD' => false,
+ ' ' => false,
+ 'ABCD' => true,
'a b c ' => false,
+ '1' => false,
+ '111' => false,
+ '11a' => true,
);
foreach ($map as $input => $expect) {
diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php
index e91cc65d4a..8869b6a327 100644
--- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php
+++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php
@@ -40,11 +40,14 @@ final class ManiphestSubpriorityController extends ManiphestController {
$is_end = false);
}
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $pri));
+
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($pri);
+ ->setNewValue($keyword);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE)
diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
index 52491ce6e6..9f5e647ca9 100644
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -77,6 +77,8 @@ final class ManiphestEditEngine
$status_map = $this->getTaskStatusMap($object);
$priority_map = $this->getTaskPriorityMap($object);
+ $alias_map = ManiphestTaskPriority::getTaskPriorityAliasMap();
+
if ($object->isClosed()) {
$default_status = ManiphestTaskStatus::getDefaultStatus();
} else {
@@ -215,8 +217,9 @@ EODOCS
->setConduitTypeDescription(pht('New task priority constant.'))
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
- ->setValue($object->getPriority())
+ ->setValue($object->getPriorityKeyword())
->setOptions($priority_map)
+ ->setOptionAliases($alias_map)
->setCommentActionLabel(pht('Change Priority')),
);
@@ -289,29 +292,29 @@ EODOCS
private function getTaskPriorityMap(ManiphestTask $task) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
+ $priority_keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
$current_priority = $task->getPriority();
+ $results = array();
+
+ foreach ($priority_map as $priority => $priority_name) {
+ $disabled = ManiphestTaskPriority::isDisabledPriority($priority);
+ if ($disabled && !($priority == $current_priority)) {
+ continue;
+ }
+
+ $keyword = head(idx($priority_keywords, $priority));
+ $results[$keyword] = $priority_name;
+ }
// If the current value isn't a legitimate one, put it in the dropdown
- // anyway so saving the form doesn't cause a side effects.
+ // anyway so saving the form doesn't cause any side effects.
if (idx($priority_map, $current_priority) === null) {
- $priority_map[$current_priority] = pht(
+ $results[ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD] = pht(
'',
$current_priority);
}
- foreach ($priority_map as $priority => $priority_name) {
- // Always keep the current priority.
- if ($priority == $current_priority) {
- continue;
- }
-
- if (ManiphestTaskPriority::isDisabledPriority($priority)) {
- unset($priority_map[$priority]);
- continue;
- }
- }
-
- return $priority_map;
+ return $results;
}
protected function newEditResponse(
diff --git a/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php
index ff8420544d..2e2db4b62d 100644
--- a/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php
+++ b/src/applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php
@@ -39,12 +39,15 @@ final class ManiphestTaskPriorityHeraldAction
return;
}
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $priority));
+
$xaction = $adapter->newTransaction()
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($priority);
+ ->setNewValue($keyword);
$adapter->queueTransaction($xaction);
- $this->logEffect(self::DO_PRIORITY, $priority);
+ $this->logEffect(self::DO_PRIORITY, $keyword);
}
public function getHeraldActionStandardType() {
diff --git a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
index 2d6a6807ce..3fc1957b4f 100644
--- a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
+++ b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
@@ -100,7 +100,10 @@ final class PhabricatorManiphestTaskTestDataGenerator
}
public function generateTaskPriority() {
- return array_rand(ManiphestTaskPriority::getTaskPriorityMap());
+ $pri = array_rand(ManiphestTaskPriority::getTaskPriorityMap());
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $pri));
+ return $keyword;
}
public function generateTaskSubPriority() {
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index 7cf9d3353f..d8a174bd0a 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -79,7 +79,7 @@ final class ManiphestTask extends ManiphestDAO
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
- 'status' => 'text12',
+ 'status' => 'text64',
'priority' => 'uint32',
'title' => 'sort',
'originalTitle' => 'text',
@@ -245,6 +245,17 @@ final class ManiphestTask extends ManiphestDAO
);
}
+ public function getPriorityKeyword() {
+ $priority = $this->getPriority();
+
+ $keyword = ManiphestTaskPriority::getKeywordForTaskPriority($priority);
+ if ($keyword !== null) {
+ return $keyword;
+ }
+
+ return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD;
+ }
+
private function comparePriorityTo(ManiphestTask $other) {
$upri = $this->getPriority();
$vpri = $other->getPriority();
diff --git a/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php
index f7d58911ba..2ed7c4e15b 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php
@@ -12,6 +12,19 @@ final class ManiphestTaskPriorityTransaction
return $object->getPriority();
}
+ public function generateNewValue($object, $value) {
+ // `$value` is supposed to be a keyword, but if the priority
+ // assigned to a task has been removed from the config,
+ // no such keyword will be available. Other edits to the task
+ // should still be allowed, even if the priority is no longer
+ // valid, so treat this as a no-op.
+ if ($value === ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD) {
+ return $object->getPriority();
+ }
+
+ return (string)ManiphestTaskPriority::getTaskPriorityFromKeyword($value);
+ }
+
public function applyInternalEffects($object, $value) {
$object->setPriority($value);
}
@@ -116,4 +129,50 @@ final class ManiphestTaskPriorityTransaction
}
}
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $content_source = $this->getEditor()->getContentSource();
+ $is_web = ($content_source instanceof PhabricatorWebContentSource);
+
+ foreach ($xactions as $xaction) {
+ $value = $xaction->getNewValue();
+
+ // If this is a legitimate keyword like "low" or "high", this transaction
+ // is fine and apply normally.
+ $keyword = ManiphestTaskPriority::getTaskPriorityFromKeyword($value);
+ if ($keyword !== null) {
+ continue;
+ }
+
+ // If this is the magic "don't change things" value for editing tasks
+ // with an obsolete priority constant in the database, let it through if
+ // this is a web edit.
+ if ($value === ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD) {
+ if ($is_web) {
+ continue;
+ }
+ }
+
+ $keyword_list = array();
+ foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $name) {
+ $keyword = ManiphestTaskPriority::getKeywordForTaskPriority($pri);
+ if ($keyword === null) {
+ continue;
+ }
+ $keyword_list[] = $keyword;
+ }
+
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Task priority "%s" is not a valid task priority. Use a priority '.
+ 'keyword to choose a task priority: %s.',
+ $value,
+ implode(', ', $keyword_list)),
+ $xaction);
+ }
+
+ return $errors;
+ }
+
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index f2ed8c7721..20b1482036 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -372,16 +372,16 @@ final class PhabricatorMetaMTAMail
}
$editor->save();
- // Queue a task to send this mail.
- $mailer_task = PhabricatorWorker::scheduleTask(
- 'PhabricatorMetaMTAWorker',
- $this->getID(),
- array(
- 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
- ));
-
$this->saveTransaction();
+ // Queue a task to send this mail.
+ $mailer_task = PhabricatorWorker::scheduleTask(
+ 'PhabricatorMetaMTAWorker',
+ $this->getID(),
+ array(
+ 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
+ ));
+
return $result;
}
diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php
index 24e7fbbef5..2736d825b5 100644
--- a/src/applications/phriction/query/PhrictionDocumentQuery.php
+++ b/src/applications/phriction/query/PhrictionDocumentQuery.php
@@ -163,7 +163,7 @@ final class PhrictionDocumentQuery
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
- $joins = array();
+ $joins = parent::buildJoinClauseParts($conn);
if ($this->getOrderVector()->containsKey('updated')) {
$content_dao = new PhrictionContent();
diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php
index 801acbb90e..29b70cfafc 100644
--- a/src/applications/project/controller/PhabricatorProjectMoveController.php
+++ b/src/applications/project/controller/PhabricatorProjectMoveController.php
@@ -153,11 +153,14 @@ final class PhabricatorProjectMoveController
break;
}
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $pri));
+
$xactions = array();
if ($pri !== null) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($pri);
+ ->setNewValue($keyword);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE)
diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php
index c7cfb4df5c..85b2ea0f0d 100644
--- a/src/applications/repository/query/PhabricatorRepositoryQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php
@@ -36,6 +36,7 @@ final class PhabricatorRepositoryQuery
private $needCommitCounts;
private $needProjectPHIDs;
private $needURIs;
+ private $needProfileImage;
public function withIDs(array $ids) {
$this->ids = $ids;
@@ -160,6 +161,11 @@ final class PhabricatorRepositoryQuery
return $this;
}
+ public function needProfileImage($need) {
+ $this->needProfileImage = $need;
+ return $this;
+ }
+
public function getBuiltinOrders() {
return array(
'committed' => array(
@@ -374,6 +380,36 @@ final class PhabricatorRepositoryQuery
}
}
+ if ($this->needProfileImage) {
+ $default = null;
+
+ $file_phids = mpull($repositories, 'getProfileImagePHID');
+ $file_phids = array_filter($file_phids);
+ if ($file_phids) {
+ $files = id(new PhabricatorFileQuery())
+ ->setParentQuery($this)
+ ->setViewer($this->getViewer())
+ ->withPHIDs($file_phids)
+ ->execute();
+ $files = mpull($files, null, 'getPHID');
+ } else {
+ $files = array();
+ }
+
+ foreach ($repositories as $repository) {
+ $file = idx($files, $repository->getProfileImagePHID());
+ if (!$file) {
+ if (!$default) {
+ $default = PhabricatorFile::loadBuiltin(
+ $this->getViewer(),
+ 'repo/code.png');
+ }
+ $file = $default;
+ }
+ $repository->attachProfileImageFile($file);
+ }
+ }
+
return $repositories;
}
diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
index 78f29fad1a..f321a112d9 100644
--- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
@@ -15,7 +15,8 @@ final class PhabricatorRepositorySearchEngine
return id(new PhabricatorRepositoryQuery())
->needProjectPHIDs(true)
->needCommitCounts(true)
- ->needMostRecentCommits(true);
+ ->needMostRecentCommits(true)
+ ->needProfileImage(true);
}
protected function buildCustomSearchFields() {
@@ -165,7 +166,8 @@ final class PhabricatorRepositorySearchEngine
->setObject($repository)
->setHeader($repository->getName())
->setObjectName($repository->getMonogram())
- ->setHref($repository->getURI());
+ ->setHref($repository->getURI())
+ ->setImageURI($repository->getProfileImageURI());
$commit = $repository->getMostRecentCommit();
if ($commit) {
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index 889bb734f3..f8a434e42b 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -57,6 +57,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
protected $viewPolicy;
protected $editPolicy;
protected $pushPolicy;
+ protected $profileImagePHID;
protected $versionControlSystem;
protected $details = array();
@@ -69,6 +70,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE;
private $uris = self::ATTACHABLE;
+ private $profileImageFile = self::ATTACHABLE;
public static function initializeNewRepository(PhabricatorUser $actor) {
@@ -110,6 +112,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
'credentialPHID' => 'phid?',
'almanacServicePHID' => 'phid?',
'localPath' => 'text128?',
+ 'profileImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'callsign' => array(
@@ -478,6 +481,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
}
+ public function getProfileImageURI() {
+ return $this->getProfileImageFile()->getBestURI();
+ }
+
+ public function attachProfileImageFile(PhabricatorFile $file) {
+ $this->profileImageFile = $file;
+ return $this;
+ }
+
+ public function getProfileImageFile() {
+ return $this->assertAttached($this->profileImageFile);
+ }
+
+
/* -( Remote Command Execution )------------------------------------------- */
@@ -682,6 +699,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$action = idx($params, 'action');
switch ($action) {
case 'history':
+ case 'graph':
case 'browse':
case 'change':
case 'lastmodified':
@@ -759,6 +777,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
switch ($action) {
case 'change':
case 'history':
+ case 'graph':
case 'browse':
case 'lastmodified':
case 'tags':
diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php
index 4b24e67c6b..7b1e213d20 100644
--- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php
+++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php
@@ -238,7 +238,14 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
case 'view':
$navigation->selectFilter($selected_item->getDefaultMenuItemKey());
- $content = $this->buildItemViewContent($selected_item);
+ try {
+ $content = $this->buildItemViewContent($selected_item);
+ } catch (Exception $ex) {
+ $content = id(new PHUIInfoView())
+ ->setTitle(pht('Unable to Render Dashboard'))
+ ->setErrors(array($ex->getMessage()));
+ }
+
$crumbs->addTextCrumb($selected_item->getDisplayName());
if (!$content) {
return new Aphront404Response();
diff --git a/src/applications/search/field/PhabricatorSearchCheckboxesField.php b/src/applications/search/field/PhabricatorSearchCheckboxesField.php
index 5a552362bf..e1a72f4576 100644
--- a/src/applications/search/field/PhabricatorSearchCheckboxesField.php
+++ b/src/applications/search/field/PhabricatorSearchCheckboxesField.php
@@ -18,6 +18,14 @@ final class PhabricatorSearchCheckboxesField
return array();
}
+ protected function didReadValueFromSavedQuery($value) {
+ if (!is_array($value)) {
+ return array();
+ }
+
+ return $value;
+ }
+
protected function getValueFromRequest(AphrontRequest $request, $key) {
return $this->getListFromRequest($request, $key);
}
diff --git a/src/applications/transactions/editfield/PhabricatorSelectEditField.php b/src/applications/transactions/editfield/PhabricatorSelectEditField.php
index d8e99d610c..fa565dbe61 100644
--- a/src/applications/transactions/editfield/PhabricatorSelectEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorSelectEditField.php
@@ -4,6 +4,7 @@ final class PhabricatorSelectEditField
extends PhabricatorEditField {
private $options;
+ private $optionAliases = array();
public function setOptions(array $options) {
$this->options = $options;
@@ -17,6 +18,24 @@ final class PhabricatorSelectEditField
return $this->options;
}
+ public function setOptionAliases(array $option_aliases) {
+ $this->optionAliases = $option_aliases;
+ return $this;
+ }
+
+ public function getOptionAliases() {
+ return $this->optionAliases;
+ }
+
+ protected function getDefaultValueFromConfiguration($value) {
+ return $this->getCanonicalValue($value);
+ }
+
+ protected function getValueForControl() {
+ $value = parent::getValueForControl();
+ return $this->getCanonicalValue($value);
+ }
+
protected function newControl() {
return id(new AphrontFormSelectControl())
->setOptions($this->getOptions());
@@ -35,4 +54,16 @@ final class PhabricatorSelectEditField
return new ConduitStringParameterType();
}
+ private function getCanonicalValue($value) {
+ $options = $this->getOptions();
+ if (!isset($options[$value])) {
+ $aliases = $this->getOptionAliases();
+ if (isset($aliases[$value])) {
+ $value = $aliases[$value];
+ }
+ }
+
+ return $value;
+ }
+
}
diff --git a/src/applications/uiexample/examples/PHUIIconExample.php b/src/applications/uiexample/examples/PHUIIconExample.php
index 35395fbff0..81778b0fd4 100644
--- a/src/applications/uiexample/examples/PHUIIconExample.php
+++ b/src/applications/uiexample/examples/PHUIIconExample.php
@@ -159,7 +159,8 @@ final class PHUIIconExample extends PhabricatorUIExample {
->setIcon($icon)
->setBackground('bg-blue')
->setHref('#')
- ->addClass('mmr');
+ ->addClass('mmr')
+ ->setTooltip($icon);
}
$layout_cicons = id(new PHUIBoxView())
diff --git a/src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php b/src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php
index 2abda20b8f..ed0e0cc605 100644
--- a/src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php
+++ b/src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php
@@ -11,24 +11,24 @@ final class PhabricatorClusterExceptionHandler
return pht('Handles runtime problems with cluster configuration.');
}
- public function canHandleRequestException(
+ public function canHandleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
- return ($ex instanceof PhabricatorClusterException);
+ $throwable) {
+ return ($throwable instanceof PhabricatorClusterException);
}
- public function handleRequestException(
+ public function handleRequestThrowable(
AphrontRequest $request,
- Exception $ex) {
+ $throwable) {
$viewer = $this->getViewer($request);
- $title = $ex->getExceptionTitle();
+ $title = $throwable->getExceptionTitle();
$dialog = id(new AphrontDialogView())
->setTitle($title)
->setUser($viewer)
- ->appendParagraph($ex->getMessage())
+ ->appendParagraph($throwable->getMessage())
->addCancelButton('/', pht('Proceed With Caution'));
return id(new AphrontDialogResponse())
diff --git a/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php b/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
index 13bf3ad83b..5c2bafdc86 100644
--- a/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
+++ b/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
@@ -60,4 +60,7 @@ interface PhabricatorInlineCommentInterface extends PhabricatorMarkupInterface {
public function supportsHiding();
public function isHidden();
+ public function getDateModified();
+ public function getDateCreated();
+
}
diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
index 46853e185c..27a489a705 100644
--- a/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
+++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
@@ -107,6 +107,14 @@ final class PHUIDiffInlineCommentDetailView
break;
}
+ $is_draft_done = false;
+ switch ($inline->getFixedState()) {
+ case PhabricatorInlineCommentInterface::STATE_DRAFT:
+ case PhabricatorInlineCommentInterface::STATE_UNDRAFT:
+ $is_draft_done = true;
+ break;
+ }
+
$is_synthetic = false;
if ($inline->getSyntheticAuthor()) {
$is_synthetic = true;
@@ -126,6 +134,7 @@ final class PHUIDiffInlineCommentDetailView
'isFixed' => $is_fixed,
'isGhost' => $inline->getIsGhost(),
'isSynthetic' => $is_synthetic,
+ 'isDraftDone' => $is_draft_done,
);
$sigil = 'differential-inline-comment';
@@ -265,7 +274,7 @@ final class PHUIDiffInlineCommentDetailView
if (!$this->preview && $this->canHide()) {
$action_buttons[] = id(new PHUIButtonView())
->setTag('a')
- ->setTooltip(pht('Hide Comment'))
+ ->setTooltip(pht('Collapse'))
->setIcon('fa-times')
->addSigil('hide-inline')
->setMustCapture(true);
diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
index a7c10b5b48..7ad3b1f1a6 100644
--- a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
+++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
@@ -121,14 +121,13 @@ final class PHUIDiffInlineCommentEditView
private function renderBody() {
$buttons = array();
- $buttons[] = phutil_tag('button', array(), pht('Save Draft'));
- $buttons[] = javelin_tag(
- 'button',
- array(
- 'sigil' => 'inline-edit-cancel',
- 'class' => 'grey',
- ),
- pht('Cancel'));
+ $buttons[] = id(new PHUIButtonView())
+ ->setText(pht('Save Draft'));
+
+ $buttons[] = id(new PHUIButtonView())
+ ->setText(pht('Cancel'))
+ ->setColor(PHUIButtonView::GREY)
+ ->addSigil('inline-edit-cancel');
$title = phutil_tag(
'div',
diff --git a/src/infrastructure/diff/view/PHUIDiffInlineThreader.php b/src/infrastructure/diff/view/PHUIDiffInlineThreader.php
new file mode 100644
index 0000000000..5209f1f359
--- /dev/null
+++ b/src/infrastructure/diff/view/PHUIDiffInlineThreader.php
@@ -0,0 +1,66 @@
+ $comment) {
+ $reply_phid = $comment->getReplyToCommentPHID();
+ if (isset($replies[$reply_phid])) {
+ $replies[$reply_phid][] = $comment;
+ unset($comments[$key]);
+ }
+ }
+
+ // For each top level comment, add the comment, then add any replies
+ // to it. Do this recursively so threads are shown in threaded order.
+ $results = array();
+ foreach ($comments as $comment) {
+ $results[] = $comment;
+ $phid = $comment->getPHID();
+ $descendants = $this->getInlineReplies($replies, $phid, 1);
+ foreach ($descendants as $descendant) {
+ $results[] = $descendant;
+ }
+ }
+
+ // If we have anything left, they were cyclic references. Just dump
+ // them in a the end. This should be impossible, but users are very
+ // creative.
+ foreach ($replies as $phid => $comments) {
+ foreach ($comments as $comment) {
+ $results[] = $comment;
+ }
+ }
+
+ return $results;
+ }
+
+ private function getInlineReplies(array &$replies, $phid, $depth) {
+ $comments = idx($replies, $phid, array());
+ unset($replies[$phid]);
+
+ $results = array();
+ foreach ($comments as $comment) {
+ $results[] = $comment;
+ $descendants = $this->getInlineReplies(
+ $replies,
+ $comment->getPHID(),
+ $depth + 1);
+ foreach ($descendants as $descendant) {
+ $results[] = $descendant;
+ }
+ }
+
+ return $results;
+ }
+}
diff --git a/src/infrastructure/diff/view/PHUIDiffRevealIconView.php b/src/infrastructure/diff/view/PHUIDiffRevealIconView.php
index 8ca3eae2c1..dca19725d8 100644
--- a/src/infrastructure/diff/view/PHUIDiffRevealIconView.php
+++ b/src/infrastructure/diff/view/PHUIDiffRevealIconView.php
@@ -8,7 +8,7 @@ final class PHUIDiffRevealIconView extends AphrontView {
->addSigil('has-tooltip')
->setMetadata(
array(
- 'tip' => pht('Show Hidden Comment'),
+ 'tip' => pht('Expand'),
'align' => 'E',
'size' => 275,
));
diff --git a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
index bad8e0eacf..25422b970d 100644
--- a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
@@ -2,34 +2,37 @@
final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule {
- private $uri;
-
public function getPriority() {
return 350.0;
}
public function apply($text) {
- $this->uri = new PhutilURI($text);
-
- if ($this->uri->getDomain() &&
- preg_match('/(^|\.)youtube\.com$/', $this->uri->getDomain()) &&
- idx($this->uri->getQueryParams(), 'v')) {
- return $this->markupYoutubeLink();
+ try {
+ $uri = new PhutilURI($text);
+ } catch (Exception $ex) {
+ return $text;
}
- return $text;
- }
+ $domain = $uri->getDomain();
+ if (!preg_match('/(^|\.)youtube\.com\z/', $domain)) {
+ return $text;
+ }
+
+ $params = $uri->getQueryParams();
+ $v_param = idx($params, 'v');
+ if (!strlen($v_param)) {
+ return $text;
+ }
- public function markupYoutubeLink() {
- $v = idx($this->uri->getQueryParams(), 'v');
$text_mode = $this->getEngine()->isTextMode();
$mail_mode = $this->getEngine()->isHTMLMailMode();
if ($text_mode || $mail_mode) {
- return $this->getEngine()->storeText('http://youtu.be/'.$v);
+ return $text;
}
- $youtube_src = 'https://www.youtube.com/embed/'.$v;
+ $youtube_src = 'https://www.youtube.com/embed/'.$v_param;
+
$iframe = $this->newTag(
'div',
array(
@@ -45,6 +48,7 @@ final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule {
'frameborder' => 0,
),
''));
+
return $this->getEngine()->storeText($iframe);
}
diff --git a/src/view/phui/PHUIIconView.php b/src/view/phui/PHUIIconView.php
index d6b1ad0ccd..8cc61ba2eb 100644
--- a/src/view/phui/PHUIIconView.php
+++ b/src/view/phui/PHUIIconView.php
@@ -18,6 +18,7 @@ final class PHUIIconView extends AphrontTagView {
private $iconFont;
private $iconColor;
private $iconBackground;
+ private $tooltip;
public function setHref($href) {
$this->href = $href;
@@ -60,6 +61,11 @@ final class PHUIIconView extends AphrontTagView {
return $this;
}
+ public function setTooltip($text) {
+ $this->tooltip = $text;
+ return $this;
+ }
+
protected function getTagName() {
$tag = 'span';
if ($this->href) {
@@ -100,11 +106,24 @@ final class PHUIIconView extends AphrontTagView {
$this->appendChild($this->text);
}
+ $sigil = null;
+ $meta = array();
+ if ($this->tooltip) {
+ Javelin::initBehavior('phabricator-tooltips');
+ require_celerity_resource('aphront-tooltip-css');
+ $sigil = 'has-tooltip';
+ $meta = array(
+ 'tip' => $this->tooltip,
+ );
+ }
+
return array(
'href' => $this->href,
'style' => $style,
'aural' => false,
'class' => $classes,
+ 'sigil' => $sigil,
+ 'meta' => $meta,
);
}
diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php
index 1c56886f7a..d0443280b6 100644
--- a/src/view/phui/PHUITwoColumnView.php
+++ b/src/view/phui/PHUITwoColumnView.php
@@ -10,6 +10,7 @@ final class PHUITwoColumnView extends AphrontTagView {
private $header;
private $subheader;
private $footer;
+ private $tabs;
private $propertySection = array();
private $curtain;
@@ -42,6 +43,12 @@ final class PHUITwoColumnView extends AphrontTagView {
return $this;
}
+ public function setTabs(PHUIListView $tabs) {
+ $tabs->setType(PHUIListView::TABBAR_LIST);
+ $this->tabs = $tabs;
+ return $this;
+ }
+
public function setFooter($footer) {
$this->footer = $footer;
return $this;
@@ -91,6 +98,10 @@ final class PHUITwoColumnView extends AphrontTagView {
$classes[] = 'phui-two-column-fluid';
}
+ if ($this->tabs) {
+ $classes[] = 'with-tabs';
+ }
+
if ($this->subheader) {
$classes[] = 'with-subheader';
}
@@ -124,6 +135,12 @@ final class PHUITwoColumnView extends AphrontTagView {
'phui-two-column-header', $this->header);
}
+ $tabs = null;
+ if ($this->tabs) {
+ $tabs = phutil_tag_div(
+ 'phui-two-column-tabs', $this->tabs);
+ }
+
$subheader = null;
if ($this->subheader) {
$subheader = phutil_tag_div(
@@ -137,6 +154,7 @@ final class PHUITwoColumnView extends AphrontTagView {
),
array(
$header,
+ $tabs,
$subheader,
$table,
$footer,
diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css
index bfde532c44..bf532d0eb3 100644
--- a/webroot/rsrc/css/application/differential/changeset-view.css
+++ b/webroot/rsrc/css/application/differential/changeset-view.css
@@ -415,7 +415,8 @@ tr.differential-inline-loading {
}
.diff-banner-has-unsaved,
-.diff-banner-has-unsubmitted {
+.diff-banner-has-unsubmitted,
+.diff-banner-has-draft-done {
background: {$sh-yellowbackground};
}
diff --git a/webroot/rsrc/css/application/diffusion/diffusion-history.css b/webroot/rsrc/css/application/diffusion/diffusion-history.css
index 2d0aea3b10..f4a51f7b56 100644
--- a/webroot/rsrc/css/application/diffusion/diffusion-history.css
+++ b/webroot/rsrc/css/application/diffusion/diffusion-history.css
@@ -30,6 +30,23 @@
margin-left: 4px;
}
+/* - Branch Styles ----------------------------------------------------------*/
+
+.diffusion-branch-list .phui-oi-attribute a {
+ color: {$darkbluetext};
+}
+
+.diffusion-branch-list .phui-oi-attribute-spacer {
+ visibility: hidden;
+}
+
+.diffusion-branch-list .phui-oi-subhead {
+ color: {$bluetext};
+}
+
+.diffusion-branch-list .phui-oi-subhead .phui-tag-view {
+ margin-right: 4px;
+}
/* - Phone Style ------------------------------------------------------------*/
diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css
index 98529b9434..1de7ccf85a 100644
--- a/webroot/rsrc/css/core/remarkup.css
+++ b/webroot/rsrc/css/core/remarkup.css
@@ -128,7 +128,8 @@
}
.phabricator-remarkup .remarkup-list-with-checkmarks input {
- display: none;
+ visibility: hidden;
+ width: 0;
}
.phabricator-remarkup .remarkup-list-with-checkmarks
diff --git a/webroot/rsrc/css/phui/phui-list.css b/webroot/rsrc/css/phui/phui-list.css
index e571e228d8..5df6f4a210 100644
--- a/webroot/rsrc/css/phui/phui-list.css
+++ b/webroot/rsrc/css/phui/phui-list.css
@@ -144,6 +144,77 @@
border: none;
}
+/* - Two Column View, Responsive Navigations -----------------------------------
+
+ Sets a two column page with a responsive, top navbar
+
+*/
+
+.phui-list-view.phui-list-tabbar {
+ list-style: none;
+ overflow: hidden;
+}
+
+.phui-list-view.phui-list-tabbar > li {
+ list-style: none;
+ float: left;
+ display: block;
+}
+
+.phui-list-view.phui-list-tabbar > li > * {
+ display: block;
+}
+
+.phui-list-tabbar .phui-list-item-href {
+ color: {$bluetext};
+ padding: 8px 24px;
+ line-height: 24px;
+ font-weight: bold;
+ font-size: {$biggerfontsize};
+ border-top: 4px solid #fff;
+}
+
+.phui-list-tabbar .phui-list-item-selected .phui-list-item-href {
+ color: {$sky};
+ border-bottom: 4px solid {$sky};
+}
+
+.phui-list-tabbar .phui-list-item-selected .phui-list-item-href
+ .phui-icon-view {
+ color: {$sky};
+}
+
+.device-desktop .phui-list-tabbar .phui-list-item-href:hover {
+ color: {$sky};
+ border-bottom: 4px solid $fff;
+ text-decoration: none;
+}
+
+.phui-list-tabbar .phui-list-item-icon {
+ height: 20px;
+ width: 20px;
+ display: none;
+ font-size: 20px;
+ text-align: center;
+}
+
+.device-phone .phui-list-tabbar .phui-list-item-icon {
+ display: inline-block;
+}
+
+.device-phone .phui-list-tabbar .phui-list-item-name {
+ display: none;
+}
+
+.device-phone .phui-list-tabbar .phui-list-item-href {
+ padding: 8px 16px;
+}
+
+.device-phone .phui-list-view.phui-list-navbar > li {
+ float: none;
+ border: none;
+}
+
/* - Status Colors -------------------------------------------------------------
Colors for navbars
diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css
index d910cf71a0..7728980268 100644
--- a/webroot/rsrc/css/phui/phui-two-column-view.css
+++ b/webroot/rsrc/css/phui/phui-two-column-view.css
@@ -12,6 +12,7 @@
margin-bottom: 12px;
}
+.phui-two-column-view.with-tabs .phui-two-column-header,
.phui-two-column-view.with-subheader .phui-two-column-header {
margin-bottom: 0;
}
@@ -168,6 +169,17 @@
margin: 0 8px;
}
+.phui-two-column-tabs {
+ padding: 0 32px;
+ margin-bottom: 32px;
+ background: #fff;
+ box-shadow: 0 1px 3px 0 rgba(0,0,0,0.2);
+}
+
+.device-phone .phui-two-column-tabs {
+ padding: 0 12px;
+}
+
/* Info View */
.phui-two-column-view .phui-info-view {
diff --git a/webroot/rsrc/js/application/diff/DiffChangeset.js b/webroot/rsrc/js/application/diff/DiffChangeset.js
index c697ba5a6c..72eeae294a 100644
--- a/webroot/rsrc/js/application/diff/DiffChangeset.js
+++ b/webroot/rsrc/js/application/diff/DiffChangeset.js
@@ -447,6 +447,7 @@ JX.install('DiffChangeset', {
changeset: this,
target: inline,
hidden: inline.isHidden(),
+ collapsed: inline.isCollapsed(),
deleted: !inline.getID() && !inline.isEditing(),
nodes: {
begin: block.items[jj],
@@ -454,7 +455,7 @@ JX.install('DiffChangeset', {
},
attributes: {
unsaved: inline.isEditing(),
- unsubmitted: inline.isDraft(),
+ anyDraft: inline.isDraft() || inline.isDraftDone(),
undone: (is_saved && !inline.isDone()),
done: (is_saved && inline.isDone())
}
diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js
index b476b8c168..808527876c 100644
--- a/webroot/rsrc/js/application/diff/DiffChangesetList.js
+++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js
@@ -19,11 +19,11 @@ JX.install('DiffChangesetList', {
var onmenu = JX.bind(this, this._ifawake, this._onmenu);
JX.Stratcom.listen('click', 'differential-view-options', onmenu);
- var onhide = JX.bind(this, this._ifawake, this._onhide);
- JX.Stratcom.listen('click', 'hide-inline', onhide);
+ var oncollapse = JX.bind(this, this._ifawake, this._oncollapse, true);
+ JX.Stratcom.listen('click', 'hide-inline', oncollapse);
- var onreveal = JX.bind(this, this._ifawake, this._onreveal);
- JX.Stratcom.listen('click', 'reveal-inline', onreveal);
+ var onexpand = JX.bind(this, this._ifawake, this._oncollapse, false);
+ JX.Stratcom.listen('click', 'reveal-inline', onexpand);
var onedit = JX.bind(this, this._ifawake, this._onaction, 'edit');
JX.Stratcom.listen(
@@ -88,7 +88,8 @@ JX.install('DiffChangesetList', {
properties: {
translations: null,
- inlineURI: null
+ inlineURI: null,
+ inlineListURI: null
},
members: {
@@ -117,6 +118,10 @@ JX.install('DiffChangesetList', {
_doneButton: null,
_doneMode: null,
+ _dropdownMenu: null,
+ _menuButton: null,
+ _menuItems: null,
+
sleep: function() {
this._asleep = true;
@@ -158,11 +163,11 @@ JX.install('DiffChangesetList', {
label = pht('Jump to previous inline comment.');
this._installJumpKey('p', label, -1, 'comment');
- label = pht('Jump to next inline comment, including hidden comments.');
+ label = pht('Jump to next inline comment, including collapsed comments.');
this._installJumpKey('N', label, 1, 'comment', true);
label = pht(
- 'Jump to previous inline comment, including hidden comments.');
+ 'Jump to previous inline comment, including collapsed comments.');
this._installJumpKey('P', label, -1, 'comment', true);
label = pht('Hide or show the current file.');
@@ -183,8 +188,8 @@ JX.install('DiffChangesetList', {
label = pht('Mark or unmark selected inline comment as done.');
this._installKey('w', label, this._onkeydone);
- label = pht('Hide or show inline comment.');
- this._installKey('q', label, this._onkeyhide);
+ label = pht('Collapse or expand inline comment.');
+ this._installKey('q', label, this._onkeycollapse);
},
isAsleep: function() {
@@ -261,12 +266,12 @@ JX.install('DiffChangesetList', {
.register();
},
- _installJumpKey: function(key, label, delta, filter, show_hidden) {
+ _installJumpKey: function(key, label, delta, filter, show_collapsed) {
filter = filter || null;
var options = {
filter: filter,
- hidden: show_hidden
+ collapsed: show_collapsed
};
var handler = JX.bind(this, this._onjumpkey, delta, options);
@@ -424,16 +429,16 @@ JX.install('DiffChangesetList', {
this._warnUser(pht('You must select a file to hide or show.'));
},
- _onkeyhide: function() {
+ _onkeycollapse: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
- if (inline.canHide()) {
+ if (inline.canCollapse()) {
this.setFocus(null);
- inline.setHidden(!inline.isHidden());
+ inline.setCollapsed(!inline.isCollapsed());
return;
}
}
@@ -455,9 +460,10 @@ JX.install('DiffChangesetList', {
var state = this._getSelectionState();
var filter = options.filter || null;
- var hidden = options.hidden || false;
+ var collapsed = options.collapsed || false;
var wrap = options.wrap || false;
var attribute = options.attribute || null;
+ var show = options.show || false;
var cursor = state.cursor;
var items = state.items;
@@ -507,10 +513,10 @@ JX.install('DiffChangesetList', {
}
}
- // If the item is hidden, don't select it when iterating with jump
+ // If the item is collapsed, don't select it when iterating with jump
// keys. It can still potentially be selected in other ways.
- if (!hidden) {
- if (items[cursor].hidden) {
+ if (!collapsed) {
+ if (items[cursor].collapsed) {
continue;
}
}
@@ -529,6 +535,15 @@ JX.install('DiffChangesetList', {
}
}
+ // If this item is a hidden inline but we're clicking a button which
+ // selects inlines of a particular type, make it visible again.
+ if (items[cursor].hidden) {
+ if (!show) {
+ continue;
+ }
+ items[cursor].target.setHidden(false);
+ }
+
// Otherwise, we've found a valid item to select.
break;
}
@@ -851,20 +866,12 @@ JX.install('DiffChangesetList', {
menu.open();
},
- _onhide: function(e) {
- this._onhidereveal(e, true);
- },
-
- _onreveal: function(e) {
- this._onhidereveal(e, false);
- },
-
- _onhidereveal: function(e, is_hide) {
+ _oncollapse: function(is_collapse, e) {
e.kill();
var inline = this._getInlineForEvent(e);
- inline.setHidden(is_hide);
+ inline.setCollapsed(is_collapse);
},
_onresize: function() {
@@ -1322,6 +1329,13 @@ JX.install('DiffChangesetList', {
},
_redrawBanner: function() {
+ // If the inline comment menu is open and we've done a redraw, close it.
+ // In particular, this makes it close when you scroll the document:
+ // otherwise, it stays open but the banner moves underneath it.
+ if (this._dropdownMenu) {
+ this._dropdownMenu.close();
+ }
+
var node = this._getBannerNode();
var changeset = this._getVisibleChangeset();
@@ -1337,40 +1351,13 @@ JX.install('DiffChangesetList', {
return;
}
- var changesets = this._changesets;
- var unsaved = [];
- var unsubmitted = [];
- var undone = [];
- var done = [];
+ var inlines = this._getInlinesByType();
- for (var ii = 0; ii < changesets.length; ii++) {
- var inlines = changesets[ii].getInlines();
- for (var jj = 0; jj < inlines.length; jj++) {
- var inline = inlines[jj];
-
- if (inline.isDeleted()) {
- continue;
- }
-
- if (inline.isSynthetic()) {
- continue;
- }
-
- if (inline.isEditing()) {
- unsaved.push(inline);
- } else if (!inline.getID()) {
- // These are new comments which have been cancelled, and do not
- // count as anything.
- continue;
- } else if (inline.isDraft()) {
- unsubmitted.push(inline);
- } else if (!inline.isDone()) {
- undone.push(inline);
- } else {
- done.push(inline);
- }
- }
- }
+ var unsaved = inlines.unsaved;
+ var unsubmitted = inlines.unsubmitted;
+ var undone = inlines.undone;
+ var done = inlines.done;
+ var draft_done = inlines.draftDone;
JX.DOM.alterClass(
node,
@@ -1382,10 +1369,16 @@ JX.install('DiffChangesetList', {
'diff-banner-has-unsubmitted',
!!unsubmitted.length);
+ JX.DOM.alterClass(
+ node,
+ 'diff-banner-has-draft-done',
+ !!draft_done.length);
+
var pht = this.getTranslations();
var unsaved_button = this._getUnsavedButton();
var unsubmitted_button = this._getUnsubmittedButton();
var done_button = this._getDoneButton();
+ var menu_button = this._getMenuButton();
if (unsaved.length) {
unsaved_button.setText(unsaved.length + ' ' + pht('Unsaved'));
@@ -1394,9 +1387,10 @@ JX.install('DiffChangesetList', {
JX.DOM.hide(unsaved_button.getNode());
}
- if (unsubmitted.length) {
- unsubmitted_button.setText(
- unsubmitted.length + ' ' + pht('Unsubmitted'));
+ if (unsubmitted.length || draft_done.length) {
+ var any_draft_count = unsubmitted.length + draft_done.length;
+
+ unsubmitted_button.setText(any_draft_count + ' ' + pht('Unsubmitted'));
JX.DOM.show(unsubmitted_button.getNode());
} else {
JX.DOM.hide(unsubmitted_button.getNode());
@@ -1450,7 +1444,8 @@ JX.install('DiffChangesetList', {
var buttons_list = [
unsaved_button.getNode(),
unsubmitted_button.getNode(),
- done_button.getNode()
+ done_button.getNode(),
+ menu_button.getNode()
];
var buttons_view = JX.$N('div', buttons_attrs, buttons_list);
@@ -1463,6 +1458,104 @@ JX.install('DiffChangesetList', {
document.body.appendChild(node);
},
+ _getInlinesByType: function() {
+ var changesets = this._changesets;
+ var unsaved = [];
+ var unsubmitted = [];
+ var undone = [];
+ var done = [];
+ var draft_done = [];
+
+ var visible_done = [];
+ var visible_collapsed = [];
+ var visible_ghosts = [];
+ var visible = [];
+ var hidden = [];
+
+ for (var ii = 0; ii < changesets.length; ii++) {
+ var inlines = changesets[ii].getInlines();
+ var inline;
+ var jj;
+ for (jj = 0; jj < inlines.length; jj++) {
+ inline = inlines[jj];
+
+ if (inline.isDeleted()) {
+ continue;
+ }
+
+ if (inline.isSynthetic()) {
+ continue;
+ }
+
+ if (inline.isEditing()) {
+ unsaved.push(inline);
+ } else if (!inline.getID()) {
+ // These are new comments which have been cancelled, and do not
+ // count as anything.
+ continue;
+ } else if (inline.isDraft()) {
+ unsubmitted.push(inline);
+ } else {
+ // NOTE: Unlike other states, an inline may be marked with a
+ // draft checkmark and still be a "done" or "undone" comment.
+ if (inline.isDraftDone()) {
+ draft_done.push(inline);
+ }
+
+ if (!inline.isDone()) {
+ undone.push(inline);
+ } else {
+ done.push(inline);
+ }
+ }
+ }
+
+ for (jj = 0; jj < inlines.length; jj++) {
+ inline = inlines[jj];
+ if (inline.isDeleted()) {
+ continue;
+ }
+
+ if (inline.isEditing()) {
+ continue;
+ }
+
+ if (inline.isHidden()) {
+ hidden.push(inline);
+ continue;
+ }
+
+ visible.push(inline);
+
+ if (inline.isDone()) {
+ visible_done.push(inline);
+ }
+
+ if (inline.isCollapsed()) {
+ visible_collapsed.push(inline);
+ }
+
+ if (inline.isGhost()) {
+ visible_ghosts.push(inline);
+ }
+ }
+ }
+
+ return {
+ unsaved: unsaved,
+ unsubmitted: unsubmitted,
+ undone: undone,
+ done: done,
+ draftDone: draft_done,
+ visibleDone: visible_done,
+ visibleGhosts: visible_ghosts,
+ visibleCollapsed: visible_collapsed,
+ visible: visible,
+ hidden: hidden
+ };
+
+ },
+
_getUnsavedButton: function() {
if (!this._unsavedButton) {
var button = new JX.PHUIXButtonView()
@@ -1513,12 +1606,140 @@ JX.install('DiffChangesetList', {
return this._doneButton;
},
+
+ _getMenuButton: function() {
+ if (!this._menuButton) {
+ var button = new JX.PHUIXButtonView()
+ .setIcon('fa-gear')
+ .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
+
+ var dropdown = new JX.PHUIXDropdownMenu(button.getNode());
+ this._menuItems = {};
+
+ var list = new JX.PHUIXActionListView();
+ dropdown.setContent(list.getNode());
+
+ var map = {
+ hideDone: {
+ type: 'done'
+ },
+ hideCollapsed: {
+ type: 'collapsed'
+ },
+ hideGhosts: {
+ type: 'ghosts'
+ },
+ hideAll: {
+ type: 'all'
+ },
+ showAll: {
+ type: 'show'
+ }
+ };
+
+ for (var k in map) {
+ var spec = map[k];
+
+ var handler = JX.bind(this, this._onhideinlines, spec.type);
+ var item = new JX.PHUIXActionView()
+ .setHandler(handler);
+
+ list.addItem(item);
+ this._menuItems[k] = item;
+ }
+
+ dropdown.listen('open', JX.bind(this, this._ondropdown));
+
+ var pht = this.getTranslations();
+
+ if (this.getInlineListURI()) {
+ list.addItem(
+ new JX.PHUIXActionView()
+ .setDivider(true));
+
+ list.addItem(
+ new JX.PHUIXActionView()
+ .setIcon('fa-link')
+ .setName(pht('List Inline Comments'))
+ .setHref(this.getInlineListURI()));
+ }
+
+ this._menuButton = button;
+ this._dropdownMenu = dropdown;
+ }
+
+ return this._menuButton;
+ },
+
+ _ondropdown: function() {
+ var inlines = this._getInlinesByType();
+ var items = this._menuItems;
+ var pht = this.getTranslations();
+
+ items.hideDone
+ .setName(pht('Hide "Done" Inlines'))
+ .setDisabled(!inlines.visibleDone.length);
+
+ items.hideCollapsed
+ .setName(pht('Hide Collapsed Inlines'))
+ .setDisabled(!inlines.visibleCollapsed.length);
+
+ items.hideGhosts
+ .setName(pht('Hide Older Inlines'))
+ .setDisabled(!inlines.visibleGhosts.length);
+
+ items.hideAll
+ .setName(pht('Hide All Inlines'))
+ .setDisabled(!inlines.visible.length);
+
+ items.showAll
+ .setName(pht('Show All Inlines'))
+ .setDisabled(!inlines.hidden.length);
+ },
+
+ _onhideinlines: function(type, e) {
+ this._dropdownMenu.close();
+ e.prevent();
+
+ var inlines = this._getInlinesByType();
+
+ // Clear the selection state since we end up in a weird place if the
+ // user hides the selected inline.
+ this._setSelectionState(null);
+
+ var targets;
+ var mode = true;
+ switch (type) {
+ case 'done':
+ targets = inlines.visibleDone;
+ break;
+ case 'collapsed':
+ targets = inlines.visibleCollapsed;
+ break;
+ case 'ghosts':
+ targets = inlines.visibleGhosts;
+ break;
+ case 'all':
+ targets = inlines.visible;
+ break;
+ case 'show':
+ targets = inlines.hidden;
+ mode = false;
+ break;
+ }
+
+ for (var ii = 0; ii < targets.length; ii++) {
+ targets[ii].setHidden(mode);
+ }
+ },
+
_onunsavedclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
+ show: true,
attribute: 'unsaved'
};
@@ -1531,7 +1752,8 @@ JX.install('DiffChangesetList', {
var options = {
filter: 'comment',
wrap: true,
- attribute: 'unsubmitted'
+ show: true,
+ attribute: 'anyDraft'
};
this._onjumpkey(1, options);
@@ -1543,6 +1765,7 @@ JX.install('DiffChangesetList', {
var options = {
filter: 'comment',
wrap: true,
+ show: true,
attribute: this._doneMode
};
diff --git a/webroot/rsrc/js/application/diff/DiffInline.js b/webroot/rsrc/js/application/diff/DiffInline.js
index baee53a7a4..1c93959b32 100644
--- a/webroot/rsrc/js/application/diff/DiffInline.js
+++ b/webroot/rsrc/js/application/diff/DiffInline.js
@@ -14,7 +14,6 @@ JX.install('DiffInline', {
_phid: null,
_changesetID: null,
_row: null,
- _hidden: false,
_number: null,
_length: null,
_displaySide: null,
@@ -30,18 +29,21 @@ JX.install('DiffInline', {
_changeset: null,
+ _isCollapsed: false,
_isDraft: null,
+ _isDraftDone: null,
_isFixed: null,
_isEditing: false,
_isNew: false,
_isSynthetic: false,
+ _isHidden: false,
bindToRow: function(row) {
this._row = row;
var row_data = JX.Stratcom.getData(row);
row_data.inline = this;
- this._hidden = row_data.hidden || false;
+ this._isCollapsed = row_data.hidden || false;
// TODO: Get smarter about this once we do more editing, this is pretty
// hacky.
@@ -73,6 +75,7 @@ JX.install('DiffInline', {
this._isFixed = data.isFixed;
this._isGhost = data.isGhost;
this._isSynthetic = data.isSynthetic;
+ this._isDraftDone = data.isDraftDone;
this._changesetID = data.changesetID;
this._isNew = false;
@@ -103,6 +106,18 @@ JX.install('DiffInline', {
return this._isSynthetic;
},
+ isDraftDone: function() {
+ return this._isDraftDone;
+ },
+
+ isHidden: function() {
+ return this._isHidden;
+ },
+
+ isGhost: function() {
+ return this._isGhost;
+ },
+
bindToRange: function(data) {
this._displaySide = data.displaySide;
this._number = parseInt(data.number, 10);
@@ -201,6 +216,12 @@ JX.install('DiffInline', {
return this;
},
+ setHidden: function(hidden) {
+ this._isHidden = hidden;
+ this._redraw();
+ return this;
+ },
+
canReply: function() {
if (!this._hasAction('reply')) {
return false;
@@ -225,7 +246,7 @@ JX.install('DiffInline', {
return true;
},
- canHide: function() {
+ canCollapse: function() {
if (!JX.DOM.scry(this._row, 'a', 'hide-inline').length) {
return false;
}
@@ -254,20 +275,18 @@ JX.install('DiffInline', {
this._id = null;
this._phid = null;
- this._hidden = false;
+ this._isCollapsed = false;
this._originalText = null;
return row;
},
- setHidden: function(hidden) {
- this._hidden = hidden;
-
- JX.DOM.alterClass(this._row, 'inline-hidden', this._hidden);
+ setCollapsed: function(collapsed) {
+ this._isCollapsed = collapsed;
var op;
- if (hidden) {
+ if (collapsed) {
op = 'hide';
} else {
op = 'show';
@@ -280,11 +299,12 @@ JX.install('DiffInline', {
.setHandler(JX.bag)
.start();
+ this._redraw();
this._didUpdate(true);
},
- isHidden: function() {
- return this._hidden;
+ isCollapsed: function() {
+ return this._isCollapsed;
},
toggleDone: function() {
@@ -322,6 +342,7 @@ JX.install('DiffInline', {
JX.DOM.alterClass(comment, 'inline-state-is-draft', response.draftState);
this._isFixed = response.isChecked;
+ this._isDraftDone = !!response.draftState;
this._didUpdate();
},
@@ -702,12 +723,15 @@ JX.install('DiffInline', {
},
_redraw: function() {
- var is_invisible = (this._isInvisible || this._isDeleted);
- var is_loading = (this._isLoading);
+ var is_invisible =
+ (this._isInvisible || this._isDeleted || this._isHidden);
+ var is_loading = this._isLoading;
+ var is_collapsed = (this._isCollapsed && !this._isHidden);
var row = this._row;
JX.DOM.alterClass(row, 'differential-inline-hidden', is_invisible);
JX.DOM.alterClass(row, 'differential-inline-loading', is_loading);
+ JX.DOM.alterClass(row, 'inline-hidden', is_collapsed);
},
_removeRow: function(row) {
diff --git a/webroot/rsrc/js/application/differential/behavior-populate.js b/webroot/rsrc/js/application/differential/behavior-populate.js
index e5b0d5039d..0a4a7912e4 100644
--- a/webroot/rsrc/js/application/differential/behavior-populate.js
+++ b/webroot/rsrc/js/application/differential/behavior-populate.js
@@ -60,7 +60,8 @@ JX.behavior('differential-populate', function(config, statics) {
var changeset_list = new JX.DiffChangesetList()
.setTranslations(JX.phtize(config.pht))
- .setInlineURI(config.inlineURI);
+ .setInlineURI(config.inlineURI)
+ .setInlineListURI(config.inlineListURI);
// Install and activate the current page.
var page_id = JX.Quicksand.getCurrentPageID();
diff --git a/webroot/rsrc/js/phuix/PHUIXActionView.js b/webroot/rsrc/js/phuix/PHUIXActionView.js
index d5d849e733..2db58dbdc4 100644
--- a/webroot/rsrc/js/phuix/PHUIXActionView.js
+++ b/webroot/rsrc/js/phuix/PHUIXActionView.js
@@ -16,6 +16,7 @@ JX.install('PHUIXActionView', {
_label: false,
_handler: null,
_selected: false,
+ _divider: false,
_iconNode: null,
_nameNode: null,
@@ -41,6 +42,15 @@ JX.install('PHUIXActionView', {
return this;
},
+ setDivider: function(divider) {
+ this._divider = divider;
+ JX.DOM.alterClass(
+ this.getNode(),
+ 'phabricator-action-view-type-divider',
+ divider);
+ return this;
+ },
+
setSelected: function(selected) {
this._selected = selected;
JX.DOM.alterClass(