diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php index 737db1aef8..70f79309fd 100644 --- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php +++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php @@ -1,105 +1,105 @@ loadPhabrictorUser(); + $author = $this->loadPhabricatorUser(); $revision = DifferentialRevision::initializeNewRevision($author); $revision->attachReviewerStatus(array()); $revision->attachActiveDiff(null); // This could be a bit richer and more formal than it is. $revision->setTitle($this->generateTitle()); $revision->setSummary($this->generateDescription()); $revision->setTestPlan($this->generateDescription()); $diff = $this->generateDiff($author); $xactions = array(); $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_UPDATE) ->setNewValue($diff->getPHID()); id(new DifferentialTransactionEditor()) ->setActor($author) ->setContentSource($this->getLipsumContentSource()) ->applyTransactions($revision, $xactions); return $revision; } public function getCCPHIDs() { $ccs = array(); for ($i = 0; $i < rand(1, 4);$i++) { - $ccs[] = $this->loadPhabrictorUserPHID(); + $ccs[] = $this->loadPhabricatorUserPHID(); } return $ccs; } public function generateDiff($author) { $paste_generator = new PhabricatorPasteTestDataGenerator(); $languages = $paste_generator->supportedLanguages; $lang = array_rand($languages); $code = $paste_generator->generateContent($lang); $altcode = $paste_generator->generateContent($lang); $newcode = $this->randomlyModify($code, $altcode); $diff = id(new PhabricatorDifferenceEngine()) ->generateRawDiffFromFileContent($code, $newcode); $call = new ConduitCall( 'differential.createrawdiff', array( 'diff' => $diff, )); $call->setUser($author); $result = $call->execute(); $thediff = id(new DifferentialDiff())->load( $result['id']); $thediff->setDescription($this->generateTitle())->save(); return $thediff; } public function generateDescription() { return id(new PhutilLipsumContextFreeGrammar()) ->generate(10, 20); } public function generateTitle() { return id(new PhutilLipsumContextFreeGrammar()) ->generate(); } public function randomlyModify($code, $altcode) { $codearr = explode("\n", $code); $altcodearr = explode("\n", $altcode); $no_lines_to_delete = rand(1, min(count($codearr) - 2, 5)); $randomlines = array_rand($codearr, count($codearr) - $no_lines_to_delete); $newcode = array(); foreach ($randomlines as $lineno) { $newcode[] = $codearr[$lineno]; } $newlines_count = rand(2, min(count($codearr) - 2, count($altcodearr) - 2, 5)); $randomlines_orig = array_rand($codearr, $newlines_count); $randomlines_new = array_rand($altcodearr, $newlines_count); $newcode2 = array(); $c = 0; for ($i = 0; $i < count($newcode);$i++) { $newcode2[] = $newcode[$i]; if (in_array($i, $randomlines_orig)) { $newcode2[] = $altcodearr[$randomlines_new[$c++]]; } } return implode($newcode2, "\n"); } } diff --git a/src/applications/files/lipsum/PhabricatorFileTestDataGenerator.php b/src/applications/files/lipsum/PhabricatorFileTestDataGenerator.php index cf084d32a7..c1daa0dea4 100644 --- a/src/applications/files/lipsum/PhabricatorFileTestDataGenerator.php +++ b/src/applications/files/lipsum/PhabricatorFileTestDataGenerator.php @@ -1,24 +1,24 @@ loadPhabrictorUserPHID(); + $author_phid = $this->loadPhabricatorUserPHID(); $dimension = 1 << rand(5, 12); $image = id(new PhabricatorLipsumMondrianArtist()) ->generate($dimension, $dimension); $file = PhabricatorFile::newFromFileData( $image, array( 'name' => 'rand-'.rand(1000, 9999), )); $file->setAuthorPHID($author_phid); $file->setMimeType('image/jpeg'); return $file->save(); } } diff --git a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php index 2617c755df..fe784dcb10 100644 --- a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php +++ b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php @@ -1,118 +1,115 @@ viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } protected function loadRandomPHID($table) { $conn_r = $table->establishConnection('r'); $row = queryfx_one( $conn_r, 'SELECT phid FROM %T ORDER BY RAND() LIMIT 1', $table->getTableName()); if (!$row) { return null; } return $row['phid']; } protected function loadRandomUser() { $viewer = $this->getViewer(); $user_phid = $this->loadRandomPHID(new PhabricatorUser()); $user = null; if ($user_phid) { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($user_phid)) ->executeOne(); } if (!$user) { throw new Exception( pht( 'Failed to load a random user. You may need to generate more '. 'test users first.')); } return $user; } protected function getLipsumContentSource() { return PhabricatorContentSource::newForSource( PhabricatorLipsumContentSource::SOURCECONST); } /** * Roll `n` dice with `d` sides each, then add `bonus` and return the sum. */ protected function roll($n, $d, $bonus = 0) { $sum = 0; for ($ii = 0; $ii < $n; $ii++) { $sum += mt_rand(1, $d); } $sum += $bonus; return $sum; } protected function newEmptyTransaction() { throw new PhutilMethodNotImplementedException(); } protected function newTransaction($type, $value, $metadata = array()) { $xaction = $this->newEmptyTransaction() ->setTransactionType($type) ->setNewValue($value); foreach ($metadata as $key => $value) { $xaction->setMetadataValue($key, $value); } return $xaction; } - - - public function loadOneRandom($classname) { try { return newv($classname, array()) ->loadOneWhere('1 = 1 ORDER BY RAND() LIMIT 1'); } catch (PhutilMissingSymbolException $ex) { throw new PhutilMissingSymbolException( $classname, pht('class'), pht( 'Unable to load symbol %s: this class does not exist.', $classname)); } } - public function loadPhabrictorUserPHID() { + public function loadPhabricatorUserPHID() { return $this->loadOneRandom('PhabricatorUser')->getPHID(); } - public function loadPhabrictorUser() { + public function loadPhabricatorUser() { return $this->loadOneRandom('PhabricatorUser'); } } diff --git a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php index 5b52aaa480..50a1df77c0 100644 --- a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php +++ b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php @@ -1,120 +1,120 @@ loadPhabrictorUserPHID(); + $author_phid = $this->loadPhabricatorUserPHID(); $author = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $author_phid); $task = ManiphestTask::initializeNewTask($author) ->setSubPriority($this->generateTaskSubPriority()) ->setTitle($this->generateTitle()); $content_source = $this->getLipsumContentSource(); $template = new ManiphestTransaction(); // Accumulate Transactions $changes = array(); $changes[ManiphestTransaction::TYPE_TITLE] = $this->generateTitle(); $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $this->generateDescription(); $changes[ManiphestTransaction::TYPE_OWNER] = $this->loadOwnerPHID(); $changes[ManiphestTransaction::TYPE_STATUS] = $this->generateTaskStatus(); $changes[ManiphestTransaction::TYPE_PRIORITY] = $this->generateTaskPriority(); $changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] = array('=' => $this->getCCPHIDs()); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); $transaction->setNewValue($value); $transactions[] = $transaction; } $transactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) ->setNewValue( array( '=' => array_fuse($this->getProjectPHIDs()), )); // Apply Transactions $editor = id(new ManiphestTransactionEditor()) ->setActor($author) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($task, $transactions); return $task; } public function getCCPHIDs() { $ccs = array(); for ($i = 0; $i < rand(1, 4);$i++) { - $ccs[] = $this->loadPhabrictorUserPHID(); + $ccs[] = $this->loadPhabricatorUserPHID(); } return $ccs; } public function getProjectPHIDs() { $projects = array(); for ($i = 0; $i < rand(1, 4);$i++) { $project = $this->loadOneRandom('PhabricatorProject'); if ($project) { $projects[] = $project->getPHID(); } } return $projects; } public function loadOwnerPHID() { if (rand(0, 3) == 0) { return null; } else { - return $this->loadPhabrictorUserPHID(); + return $this->loadPhabricatorUserPHID(); } } public function generateTitle() { return id(new PhutilLipsumContextFreeGrammar()) ->generate(); } public function generateDescription() { return id(new PhutilLipsumContextFreeGrammar()) ->generateSeveral(rand(30, 40)); } public function generateTaskPriority() { return array_rand(ManiphestTaskPriority::getTaskPriorityMap()); } public function generateTaskSubPriority() { return rand(2 << 16, 2 << 32); } public function generateTaskStatus() { $statuses = array_keys(ManiphestTaskStatus::getTaskStatusMap()); // Make sure 4/5th of all generated Tasks are open $random = rand(0, 4); if ($random != 0) { return ManiphestTaskStatus::getDefaultStatus(); } else { return array_rand($statuses); } } } diff --git a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php index ac61327b39..9a274443cf 100644 --- a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php +++ b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php @@ -1,117 +1,117 @@ loadPhabrictorUserPHID(); + $author_phid = $this->loadPhabricatorUserPHID(); $author = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $author_phid); $mock = PholioMock::initializeNewMock($author); $content_source = $this->getLipsumContentSource(); $template = id(new PholioTransaction()) ->setContentSource($content_source); // Accumulate Transactions $changes = array(); $changes[PholioTransaction::TYPE_NAME] = $this->generateTitle(); $changes[PholioTransaction::TYPE_DESCRIPTION] = $this->generateDescription(); $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = PhabricatorPolicies::POLICY_PUBLIC; $changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] = array('=' => $this->getCCPHIDs()); // Get Files and make Images $file_phids = $this->generateImages(); $files = id(new PhabricatorFileQuery()) ->setViewer($author) ->withPHIDs($file_phids) ->execute(); $mock->setCoverPHID(head($files)->getPHID()); $sequence = 0; $images = array(); foreach ($files as $file) { $image = new PholioImage(); $image->setFilePHID($file->getPHID()); $image->setSequence($sequence++); $image->attachMock($mock); $images[] = $image; } // Apply Transactions $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); $transaction->setNewValue($value); $transactions[] = $transaction; } $mock->openTransaction(); $editor = id(new PholioMockEditor()) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setActor($author) ->applyTransactions($mock, $transactions); foreach ($images as $image) { $image->setMockID($mock->getID()); $image->save(); } $mock->saveTransaction(); return $mock->save(); } public function generateTitle() { return id(new PhutilLipsumContextFreeGrammar()) ->generate(); } public function generateDescription() { return id(new PhutilLipsumContextFreeGrammar()) ->generateSeveral(rand(30, 40)); } public function getCCPHIDs() { $ccs = array(); for ($i = 0; $i < rand(1, 4);$i++) { - $ccs[] = $this->loadPhabrictorUserPHID(); + $ccs[] = $this->loadPhabricatorUserPHID(); } return $ccs; } public function generateImages() { $images = newv('PhabricatorFile', array()) ->loadAllWhere('mimeType = %s', 'image/jpeg'); $rand_images = array(); $quantity = rand(2, 10); $quantity = min($quantity, count($images)); if ($quantity) { $random_images = $quantity === 1 ? array(array_rand($images, $quantity)) : array_rand($images, $quantity); foreach ($random_images as $random) { $rand_images[] = $images[$random]->getPHID(); } } // This means you don't have any JPEGs yet. We'll just use a built-in image. if (empty($rand_images)) { $default = PhabricatorFile::loadBuiltin( PhabricatorUser::getOmnipotentUser(), 'profile.png'); $rand_images[] = $default->getPHID(); } return $rand_images; } } diff --git a/src/docs/contributor/phabricator_code_layout.diviner b/src/docs/contributor/phabricator_code_layout.diviner index 19ec048f73..422f228a27 100644 --- a/src/docs/contributor/phabricator_code_layout.diviner +++ b/src/docs/contributor/phabricator_code_layout.diviner @@ -1,111 +1,111 @@ @title Phabricator Code Layout @group developer Guide to Phabricator code layout, including how URI mapping works through application class and subdirectory organization best practices. = URI Mapping = When a user visits a Phabricator URI, the Phabricator infrastructure parses that URI with a regular expression to determine what controller class to load. The Phabricator infrastructure knows where a given controller class lives on disk from a cache file the Arcanist phutil mapper generates. This mapping should be updated whenever new classes or files are added: arc liberate /path/to/phabricator/src Finally, a given controller class will map to an application which will have most of its code in standardized subdirectories and classes. = Best Practice Class and Subdirectory Organization = Suppose you were working on the application `Derp`. phabricator/src/applications/derp/ If `Derp` were as simple as possible, it would have one subdirectory: phabricator/src/applications/derp/controller/ containing the file `DerpController.php` with the class - `DerpController`: minimally implements a `processRequest()` method which returns some @{class:AphrontResponse} object. The class would probably extend @{class:PhabricatorController}. If `Derp` were (relatively) complex, one could reasonably expect to see the following directory layout: phabricator/src/applications/derp/conduit/ phabricator/src/applications/derp/constants/ phabricator/src/applications/derp/controller/ phabricator/src/applications/derp/editor/ phabricator/src/applications/derp/exception/ phabricator/src/applications/derp/query/ phabricator/src/applications/derp/replyhandler/ phabricator/src/applications/derp/storage/ phabricator/src/applications/derp/view/ (The following two folders are also likely to be included for JavaScript and CSS respectively. However, static resources are largely outside the scope of this document. See @{article:Adding New CSS and JS}.) phabricator/webroot/rsrc/js/application/derp/ phabricator/webroot/rsrc/css/application/derp/ These directories under `phabricator/src/applications/derp/` represent -the basic set of class types from which most Phabrictor applications are +the basic set of class types from which most Phabricator applications are assembled. Each would contain a class file. For `Derp`, these classes could be something like: - **DerpConstants**: constants used in the `Derp` application. - **DerpController**: business logic providing functionality for a given URI. Typically, controllers load data via Storage or Query classes, then present the data to the user via one or more View classes. - **DerpEditor**: business logic for workflows that change one or more Storage objects. Editor classes are only necessary for particularly complicated edits and should be used pragmatically versus Storage objects. - **DerpException**: exceptions used in the `Derp` application. - **DerpQuery**: query one or more storage objects for pertinent `Derp` application data. @{class:PhabricatorOffsetPagedQuery} is particularly handy for pagination and works well with @{class:AphrontPagerView}. - **DerpReplyHandler**: business logic from any configured email interactions users can have with the `Derp` application. - **DerpStorage**: storage objects for the `Derp` application. Typically there is a base class which extends @{class:PhabricatorLiskDAO} to configure application-wide storage settings like the application (thus database) name. Reading more about the @{class:LiskDAO} is highly recommended. - **DerpView**: view objects for the `Derp` application. Typically these extend @{class:AphrontView}. - **DerpConduitAPIMethod**: provides any and all `Derp` application functionality that is accessible over Conduit. However, it is likely that `Derp` is even more complex, and rather than containing one class, each directory has several classes. A typical example happens around the CRUD of an object: - **DerpBaseController**: typically extends @{class:PhabricatorController} and contains any controller-specific functionality used throughout the `Derp` application. - **DerpDeleteController**: typically extends `DerpBaseController` and presents a confirmation dialogue to the user about deleting a `Derp`. - **DerpEditController**: typically extends `DerpBaseController` and presents a form to create and edit `Derps`. Most likely uses @{class:AphrontFormView} and various `AphrontFormXControl` classes such as @{class:AphrontFormTextControl} to create the form. - **DerpListController**: typically extends `DerpBaseController` and displays a set of one or more `Derps`. Might use @{class:AphrontTableView} to create a table of `Derps`. - **DerpViewController**: typically extends `DerpBaseController` and displays a single `Derp`. Some especially awesome directories might have a `__tests__` subdirectory containing all pertinent unit test code for the class. = Next Steps = - Learn about @{article:Adding New CSS and JS}; or - learn about the @{class:LiskDAO}; or - learn about @{article:Writing Unit Tests}; or - learn how to contribute (see @{article:Contributor Introduction}). diff --git a/src/docs/user/configuration/advanced_configuration.diviner b/src/docs/user/configuration/advanced_configuration.diviner index 17212ad4ee..5721ffd597 100644 --- a/src/docs/user/configuration/advanced_configuration.diviner +++ b/src/docs/user/configuration/advanced_configuration.diviner @@ -1,117 +1,117 @@ @title Configuration User Guide: Advanced Configuration @group config Configuring Phabricator for multiple environments. = Overview = Phabricator reads configuration from multiple sources. This document explains the configuration stack and how to set up advanced configuration sources, which may be useful for deployments with multiple environments (e.g., development and production). This is a complicated topic for advanced users. You do not need to understand this topic to install Phabricator. = Configuration Sources = Phabricator supports the following configuration sources, from highest priority to lowest priority: - **Database**: Values are stored in the database and edited from the web UI by administrators. They have the highest priority and override other settings. - **Local**: Values are stored in `conf/local/config.json` and edited by running `bin/config`. - **Config Files**: Values are stored in a config file in `conf/`. The file to use is selected by writing to `conf/local/ENVIRONMENT`, or setting the `PHABRICATOR_ENV` configuration variable. See below for more information. - **Defaults**: Defaults hard-coded in the Phabricator source, which can not be edited. They have the lowest priority, and all other settings override them. Normally, you install and configure Phabricator by writing enough configuration into the local config to get access to the database configuration (e.g., the MySQL username, password, and hostname), then use the web interface to further configure Phabricator. = Configuration Files = Configuration files provide an alternative to database configuration, and may be appropriate if you want to deploy in multiple environments or create dynamic configuration. Configuration files are more complicated than database configuration, which is why they are not used by default. == Creating a Configuration File == To create a configuration file, first choose a name for the config (like "devserver" or "live"). For the purposes of this section, we'll assume you chose `exampleconfig`. Replace "exampleconfig" with whatever you actually chose in the examples below. First, write an `exampleconfig.conf.php` file here (rename it according to the name you chose): phabricator/conf/custom/exampleconfig.conf.php Its contents should look like this: 'examplevalue', ); For example, to specify MySQL credentials in your config file, you might create a config like this: 'localhost', 'mysql.user' => 'root', 'mysql.pass' => 'hunter2trustno1', ); == Selecting a Configuration File == To select a configuration file, write the name of the file (relative to `phabricator/conf/`) to `phabricator/conf/local/ENVIRONMENT`. For example, to select `phabricator/conf/custom/exampleconfig.conf.php`, you would write -"custom/exampleconfig" to `phabrictor/conf/local/ENVIRONMENT`: +"custom/exampleconfig" to `phabricator/conf/local/ENVIRONMENT`: phabricator/ $ echo custom/exampleconfig > conf/local/ENVIRONMENT phabricator/ $ cat conf/local/ENVIRONMENT custom/exampleconfig phabricator/ $ You can also set the environmental variable `PHABRICATOR_ENV`. This is more involved but may be easier in some deployment environments. Note that this needs to be set in your webserver environment, and also in your shell whenever you run a script: ``` # Shell export PHABRICATOR_ENV=custom/exampleconfig # Apache SetEnv PHABRICATOR_ENV custom/exampleconfig # nginx fastcgi_param PHABRICATOR_ENV "custom/exampleconfig"; # lighttpd setenv.add-environment = ( "PHABRICATOR_ENV" => "custom/exampleconfig", ) ``` After creating and selecting a configuration file, restart Phabricator (for help, see @{article:Restarting Phabricator}). Any configuration you set should take effect immediately, and your file should be visible in the Config application when examining configuration. = Next Steps = Return to the @{article:Configuration Guide}. diff --git a/src/docs/user/userguide/audit.diviner b/src/docs/user/userguide/audit.diviner index 806d37adab..0d10867906 100644 --- a/src/docs/user/userguide/audit.diviner +++ b/src/docs/user/userguide/audit.diviner @@ -1,202 +1,202 @@ @title Audit User Guide @group userguide Guide to using Phabricator to audit published commits. Overview ======== Phabricator supports two code review workflows, "review" (pre-publish) and "audit" (post-publish). To understand the differences between the two, see @{article:User Guide: Review vs Audit}. How Audit Works =============== The audit workflow occurs after changes have been published. It provides ways to track, discuss, and resolve issues with commits that are discovered after they go through whatever review process you have in place (if you have one). Two examples of how you might use audit are: **Fix Issues**: If a problem is discovered after a change has already been published, users can find the commit which introduced the problem and raise a concern on it. This notifies the author of the commit and prompts them to remedy the issue. **Watch Changes**: In some cases, you may want to passively look over changes that satisfy some criteria as they are published. For example, you may want to review all Javascript changes at the end of the week to keep an eye on things, or make sure that code which impacts a subsystem is looked at by someone on that team, eventually. Developers may also want other developers to take a second look at things if they realize they aren't sure about something after a change has been published, or just want to provide a heads-up. You can configure Herald rules and Owners packages to automatically trigger audits of commits that satisfy particular criteria. Audit States and Actions ======================== The audit workflow primarily keeps track of two things: - **Commits** and their audit state (like "Not Audited", "Approved", or "Concern Raised"). - **Audit Requests** which ask a user (or some other entity, like a project or package) to audit a commit. These can be triggered in a number of ways (see below). Users interact with commits by leaving comments and applying actions, like accepting the changes or raising a concern. These actions change the state of their own audit and the overall audit state of the commit. Here's an example of a typical audit workflow: - Alice publishes a commit containing some Javascript. - This triggers an audit request to Bailey, the Javascript technical lead on the project (see below for a description of trigger mechanisms). - - Later, Bailey logs into Phabrictor and sees the audit request. She ignores + - Later, Bailey logs into Phabricator and sees the audit request. She ignores it for the moment, since it isn't blocking anything. At the end of the week she looks through her open requests to see what the team has been up to. - Bailey notices a few minor problems with Alice's commit. She leaves comments describing improvements and uses "Raise Concern" to send the commit back into Alice's queue. - Later, Alice logs into Phabricator and sees that Bailey has raised a concern (usually, Alice will also get an email). She resolves the issue somehow, maybe by making a followup commit with fixes. - After the issues have been dealt with, she uses "Request Verification" to return the change to Bailey so Bailey can verify that the concerns have been addressed. - Bailey uses "Accept Commit" to close the audit. In {nav Diffusion > Browse Commits}, you can review commits and query for commits with certain audit states. The default "Active Audits" view shows all of the commits which are relevant to you given their audit state, divided into buckets: - **Needs Attention**: These are commits which you authored that another user has raised a concern about: for example, maybe they believe they have found a bug or some other problem. You should address the concerns. - **Needs Verification**: These are commits which someone else authored that you previously raised a concern about. The author has indicated that they believe the concern has been addressed. You should verify that the remedy is satisfactory and accept the change, or raise a further concern. - **Ready to Audit**: These are commits which someone else authored that you have been asked to audit, either by a user or by a system rule. You should look over the changes and either accept them or raise concerns. - **Waiting on Authors**: These are commits which someone else authored that you previously raised a concern about. The author has not responded to the concern yet. You may want to follow up. - **Waiting on Auditors**: These are commits which you authored that someone else needs to audit. You can use the query constraints to filter this list or find commits that match certain criteria. Audit Triggers ============== Audit requests can be triggered in a number of ways: - You can add auditors explicitly from the web UI, using either "Edit Commit" or the "Change Auditors" action. You might do this if you realize you are not sure about something that you recently published and want a second opinion. - If you put `Auditors: username1, username2` in your commit message, it will trigger an audit request to those users when you push it to a tracked branch. - You can create rules in Herald that trigger audits based on properties of the commit -- like the files it touches, the text of the change, the author, etc. - You can create an Owners package and enable automatic auditing for the package. Audits in Small Teams ===================== If you have a small team and don't need complicated trigger rules, you can set up a simple audit workflow like this: - Create a new Project, "Code Audits". - Create a new global Herald rule for Commits, which triggers an audit by the "Code Audits" project for every commit where "Differential Revision" "does not exist" (this will allow you to transition partly or fully to review later if you want). - Have every engineer join the "Code Audits" project. This way, everyone will see an audit request for every commit, but it will be dismissed if anyone approves it. Effectively, this enforces the rule "every commit should have //someone// look at it". Once your team gets bigger, you can refine this ruleset so that developers see only changes that are relevant to them. Audit Tips ========== - When viewing a commit, audit requests you are responsible for are highlighted. You are responsible for a request if it's a user request and you're that user, or if it's a project request and you're a member of the project, or if it's a package request and you're a package owner. Any action you take will update the state of all the requests you're responsible for. - You can leave inline comments by clicking the line numbers in the diff. - You can leave a comment across multiple lines by dragging across the line numbers. - Inline comments are initially saved as drafts. They are not submitted until you submit a comment at the bottom of the page. - Press "?" to view keyboard shortcuts. Audit Maintenance ================= The `bin/audit` command allows you to perform several maintenance operations. Get more information about a command by running: ``` phabricator/ $ ./bin/audit help ``` Supported operations are: **Delete Audits**: Delete audits that match certain parameters with `bin/audit delete`. You can use this command to forcibly delete requests which may have triggered incorrectly (for example, because a package or Herald rule was configured in an overbroad way). After deleting audits, you may want to run `bin/audit synchronize` to synchronize audit state. **Synchronize Audit State**: Synchronize the audit state of commits to the current open audit requests with `bin/audit synchronize`. Normally, overall audit state is automatically kept up to date as changes are made to an audit. However, if you delete audits or manually update the database to make changes to audit request state, the state of corresponding commits may no longer be correct. This command will update commits so their overall audit state reflects the cumulative state of their actual audit requests. **Update Owners Package Membership**: Update which Owners packages commits belong to with `bin/audit update-owners`. Normally, commits are automatically associated with packages when they are imported. You can use this command to manually rebuild this association if you run into problems with it. Next Steps ========== - Learn more about Herald at @{article:Herald User Guide}. diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner index 6f29e3586c..573c8c4c69 100644 --- a/src/docs/user/userguide/projects.diviner +++ b/src/docs/user/userguide/projects.diviner @@ -1,339 +1,339 @@ @title Projects User Guide @group userguide Organize users and objects with projects. Overview ======== NOTE: This document is only partially complete. Phabricator projects are flexible, general-purpose groups of objects that you can use to organize information. Projects have some basic information like a name and an icon, and may optionally have members. For example, you can create projects to provide: - **Organization**: Create a project to represent a product or initative, then use it to organize related work. - **Groups**: Create a project to represent a group of people (like a team), then add members of the group as project members. - **Tags**: To create a tag, just create a project without any members. Then tag anything you want. - **Access Control Lists**: Add members to a project, then restrict the visibility of objects to members of that project. See "Understanding Policies" below to understand how policies and projects interact in more detail. Understanding Policies ====================== An important rule to understand about projects is that **adding or removing projects to an object never affects who can see the object**. For example, if you tag a task with a project like {nav Backend}, that does not change who can see the task. In particular, it does not limit visibility to only members of the "Backend" project, nor does it allow them to see it if they otherwise could not. Likewise, removing projects does not affect visibility. If you're familiar with other software that works differently, this may be -unexpected, but the rule in Phabrictor is simple: **adding and removing +unexpected, but the rule in Phabricator is simple: **adding and removing projects never affects policies.** Note that you //can// write policy rules which restrict capabilities to members of a specific project or set of projects, but you do this by editing an object's policies and adding rules based on project membership, not by tagging or untagging the object with projects. To manage who can seen an object, use the object's policy controls, Spaces (see @{article:Spaces User Guide}) and Custom Forms (see @{article:User Guide: Customizing Forms}). For more details about rationale, see "Policies In Depth", below. Joining Projects ================ Once you join a project, you become a member and will receive mail sent to the project, like a mailing list. For example, if a project is added as a subscriber on a task or a reviewer on a revision, you will receive mail about that task or revision. If you'd prefer not to receive mail sent to a project, you can go to {nav Members} and select {nav Disable Mail}. If you disable mail for a project, you will no longer receive mail sent to the project. Watching Projects ================= Watching a project allows you to closely follow all activity related to a project. You can **watch** a project by clicking {nav Watch Project} on the project page. To stop watching a project, click {nav Unwatch Project}. When you watch a project, you will receive a copy of mail about any objects (like tasks or revisions) that are tagged with the project, or that the project is a subscriber, reviewer, or auditor for. For moderately active projects, this may be a large volume of mail. Edit Notifications ================== Edit notifications are generated when project details (like the project description, name, or icon) are updated, or when users join or leave projects. By default, these notifications are are only sent to the acting user. These notifications are usually not very interesting, and project mail is already complicated by members and watchers. If you'd like to receive edit notifications for a project, you can write a Herald rule to keep you in the loop. Customizing Menus ================= Projects support profile menus, which are customizable. For full details on managing and customizing profile menus, see @{article:Profile Menu User Guide}. Here are some examples of common ways to customize project profile menus that may be useful: **Link to Tasks or Repositories**: You can add a menu item for "Open Tasks" or "Active Repositories" for a project by running the search in the appropriate application, then adding a link to the search results to the menu. This can let you quickly jump from a project screen to related tasks, revisions, repositories, or other objects. For more details on how to use search and manage queries, see @{article:Search User Guide}. **New Task Button**: To let users easily make a new task that is tagged with the current project, add a link to the "New Task" form with the project prefilled, or to a custom form with appropriate defaults. For information on customizing and prefilling forms, see @{article:User Guide: Customizing Forms}. **Link to Wiki Pages**: You can add links to relevant wiki pages or other documentation to the menu to make it easy to find and access. You could also link to a Conpherence if you have a chatroom for a project. **Link to External Resources**: You can link to external resources outside of Phabricator if you have other pages which are relevant to a project. **Set Workboard as Default**: For projects that are mostly used to organize tasks, change the default item to the workboard instead of the profile to get to the workboard view more easily. **Hide Unused Items**: If you have a project which you don't expect to have members or won't have a workboard, you can hide these items to streamline the menu. Subprojects and Milestones ========================== IMPORTANT: This feature is only partially implemented. After creating a project, you can use the {nav icon="sitemap", name="Subprojects"} menu item to add subprojects or milestones. **Subprojects** are projects that are contained inside the main project. You can use them to break large or complex groups, tags, lists, or undertakings apart into smaller pieces. **Milestones** are a special kind of subproject for organizing tasks into blocks of work. You can use them to implement sprints, iterations, milestones, versions, etc. Subprojects and milestones have some additional special behaviors and rules, particularly around policies and membership. See below for details. This is a brief summary of the major differences between normal projects, subprojects, parent projects, and milestones. | | Normal | Parent | Subproject | Milestone | |---|---|---|---|---| | //Members// | Yes | Union of Subprojects | Yes | Same as Parent | | //Policies// | Yes | Yes | Affected by Parent | Same as Parent | | //Hashtags// | Yes | Yes | Yes | Special | Subprojects =========== Subprojects are full-power projects that are contained inside some parent project. You can use them to divide a large or complex project into smaller parts. Subprojects have normal members and normal policies, but note that the policies of the parent project affect the policies of the subproject (see "Parent Projects", below). Subprojects can have their own subprojects, milestones, or both. If a subproject has its own subprojects, it is both a subproject and a parent project. Thus, the parent project rules apply to it, and are stronger than the subproject rules. Subprojects can have normal workboards. The maximum subproject depth is 16. This limit is intended to grossly exceed the depth necessary in normal usage. Objects may not be tagged with multiple projects that are ancestors or descendants of one another. For example, a task may not be tagged with both {nav Stonework} and {nav Stonework > Masonry}. When a project tag is added that is the ancestor or descendant of one or more existing tags, the old tags are replaced. For example, adding {nav Stonework > Masonry} to a task tagged with {nav Stonework} will replace {nav Stonework} with the newer, more specific tag. This restriction does not apply to projects which share some common ancestor but are not themselves mutual ancestors. For example, a task may be tagged with both {nav Stonework > Masonry} and {nav Stonework > Sculpting}. This restriction //does// apply when the descendant is a milestone. For example, a task may not be tagged with both {nav Stonework} and {nav Stonework > Iteration II}. Milestones ========== Milestones are simple subprojects for tracking sprints, iterations, versions, or other similar blocks of work. Milestones make it easier to create and manage a large number of similar subprojects (for example: {nav Sprint 1}, {nav Sprint 2}, {nav Sprint 3}, etc). Milestones can not have direct members or policies. Instead, the membership and policies of a milestones are always the same as the milestone's parent project. This makes large numbers of milestones more manageable when changes occur. Milestones can not have subprojects, and can not have their own milestones. By default, Milestones do not have their own hashtags. Milestones can have normal workboards. Objects may not be tagged with two different milestones of the same parent project. For example, a task may not be tagged with both {nav Stonework > Iteration III} and {nav Stonework > Iteration V}. When a milestone tag is added to an object which already has a tag from the same series of milestones, the old tag is removed. For example, adding the {nav Stonework > Iteration V} tag to a task which already has the {nav Stonework > Iteration III} tag will remove the {nav Iteration III} tag. This restriction does not apply to milestones which are not part of the same series. For example, a task may be tagged with both {nav Stonework > Iteration V} and {nav Heraldry > Iteration IX}. Parent Projects =============== When you add the first subproject to an existing project, it is converted into a **parent project**. Parent projects have some special rules. **No Direct Members**: Parent projects can not have members of their own. Instead, all of the users who are members of any subproject count as members of the parent project. By joining (or leaving) a subproject, a user is implicitly added to (or removed from) all ancestors of that project. Consequently, when you add the first subproject to an existing project, all of the project's current members are moved to become members of the subproject instead. Implicitly, they will remain members of the parent project because the parent project is an ancestor of the new subproject. You can edit the project afterward to change or remove members if you want to split membership apart in a more granular way across multiple new subprojects. **Searching**: When you search for a parent project, results for any subproject are returned. For example, if you search for {nav Engineering}, your query will match results in {nav Engineering} itself, but also subprojects like {nav Engineering > Warp Drive} and {nav Engineering > Shield Batteries}. **Policy Effects**: To view a subproject or milestone, you must be able to view the parent project. As a result, the parent project's view policy now affects child projects. If you restrict the visibility of the parent, you also restrict the visibility of the children. In contrast, permission to edit a parent project grants permission to edit any subproject. If a user can {nav Root Project}, they can also edit {nav Root Project > Child} and {nav Root Project > Child > Sprint 3}. Policies In Depth ================= As discussed above, adding and removing projects never affects who can see an object. This is an explicit product design choice aimed at reducing the complexity of policy management. Phabricator projects are a flexible, general-purpose, freeform tool. This is a good match for many organizational use cases, but a very poor match for policies. It is important that policies be predictable and rigid, because the cost of making a mistake with policies is high (inadvertent disclosure of private information). In Phabricator, each object (like a task) can be tagged with multiple projects. This is important in a flexible organizational tool, but is a liability in a policy tool. If each project potentially affected visibility, it would become more difficult to predict the visibility of objects and easier to make mistakes with policies. There are different, reasonable expectations about how policies might be affected when tagging objects with projects, but these expectations are in conflict, and different users have different expectations. For example: - if a user adds a project like {nav Backend} to a task, their intent might be to //open// the task up and share it with the "Backend" team; - if a user adds a project like {nav Security Vulnerability} to a task, their intent might be to //close// the task down and restrict it to just the security team; - if a user adds a project like {nav Easy Starter Task} to a task, their intent might be to not affect policies at all; - if a user adds {nav Secret Inner Council} to a task already tagged with {nav Security Vulnerability}, their intent might be to //open// the task to members of //either// project, or //close// the task to just members of //both// projects; - if a user adds {nav Backend} to a task already tagged with {nav Security Vulnerability}, their intent is totally unclear; - in all cases, users may be adding projects purely to organize objects without intending to affect policies. We can't distinguish between these cases without adding substantial complexity, and even if we made an attempt to navigate this it would still be very difficult to predict the effect of tagging an object with multiple policy-affecting projects. Users would need to learn many rules about how these policy types interacted to predict the policy effects of adding or removing a project. Because of the implied complexity, we almost certainly could not prevent some cases where a user intends to take a purely organizational action (like adding a {nav Needs Documentation} tag) and accidentally opens a private object to a wide audience. The policy system is intended to make these catastrophically bad cases very difficult, and allowing projects to affect policies would make these mistakes much easier to make. We believe the only reasonable way we could reduce ambiguity and complexity is by making project policy actions explicit and rule-based. But we already have a system for explicit, rule-based management of policies: the policy system. The policy tools are designed for policy management and aimed at making actions explicit and mistakes very difficult. Many of the use cases where project-based access control seems like it might be a good fit can be satisfied with Spaces instead (see @{article:Spaces User Guide}). Spaces are explicit, unambiguous containers for groups of objects with similar policies. Form customization also provides a powerful tool for making many policy management tasks easier (see @{article:User Guide: Customizing Forms}).