diff --git a/.gitignore b/.gitignore index adeb0c1a2d..c111af5b96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .DS_Store ._* -/webroot/rsrc/custom .#* *# *~ diff --git a/README b/README index 9221dfdb45..fff10b88de 100644 --- a/README +++ b/README @@ -1,12 +1,22 @@ -Phabricator is an open source collection of web applications which make it -easier to write, review, and share source code. Phabricator was developed at -Facebook. +Phabricator is an open source collection of web applications which help +software companies build better software. -It's pretty high-quality and usable, but under active development so things -may change quickly. +Phabricator includes applications for: -You can learn more about the project and find links to documentation and -resources at: http://phabricator.org/ + - reviewing and auditing source code; + - hosting and browsing repositories; + - assembling a party to venture forth; + - tracking bugs; + - hiding stuff from coworkers; and + - also some other things. + +You can learn more about the project (and find links to documentation and +resources) here: + + http://phabricator.org/ + +Phabricator is developed and maintained by Phacility. The first version of +Phabricator was originally built at Facebook. LICENSE diff --git a/bin/celerity b/bin/celerity new file mode 120000 index 0000000000..f4558cb181 --- /dev/null +++ b/bin/celerity @@ -0,0 +1 @@ +../scripts/setup/manage_celerity.php \ No newline at end of file diff --git a/bin/commit-hook b/bin/commit-hook new file mode 120000 index 0000000000..9eda12cfb7 --- /dev/null +++ b/bin/commit-hook @@ -0,0 +1 @@ +../scripts/repository/commit_hook.php \ No newline at end of file diff --git a/bin/harbormaster b/bin/harbormaster new file mode 120000 index 0000000000..448588ebef --- /dev/null +++ b/bin/harbormaster @@ -0,0 +1 @@ +../scripts/setup/manage_harbormaster.php \ No newline at end of file diff --git a/bin/ssh-connect b/bin/ssh-connect new file mode 120000 index 0000000000..79f7c94d03 --- /dev/null +++ b/bin/ssh-connect @@ -0,0 +1 @@ +../scripts/ssh/ssh-connect.php \ No newline at end of file diff --git a/conf/default.conf.php b/conf/default.conf.php index c02b08c4e1..b456b1e12b 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -299,13 +299,6 @@ return array( 'metamta.mail-adapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', - // When email is sent, try to hand it off to the MTA immediately instead of - // queueing it for delivery by the daemons. If you are running the Phabricator - // daemons with "phd start", you should disable this to provide a (sometimes - // substantial) performance boost. It's on by default to make setup and - // configuration a little easier. - 'metamta.send-immediately' => true, - // When email is sent, what format should Phabricator use for user's // email addresses? Valid values are: // - 'short' - 'gwashington ' @@ -550,15 +543,6 @@ return array( // -- Auth ------------------------------------------------------------------ // - // Maximum number of simultaneous web sessions each user is permitted to have. - // Setting this to "1" will prevent a user from logging in on more than one - // browser at the same time. - 'auth.sessions.web' => 5, - - // Maximum number of simultaneous Conduit sessions each user is permitted - // to have. - 'auth.sessions.conduit' => 5, - // If true, email addresses must be verified (by clicking a link in an // email) before a user can login. By default, verification is optional // unless 'auth.email-domains' is nonempty (see below). @@ -865,11 +849,6 @@ return array( // not actually be receiving thorough review. 'differential.enable-email-accept' => false, - // If you set this to true, users won't need to login to view differential - // revisions. Anonymous users will have read-only access and won't be able to - // interact with the revisions. - 'differential.anonymous-access' => false, - // List of file regexps that should be treated as if they are generated by // an automatic process, and thus get hidden by default in differential. 'differential.generated-paths' => array( @@ -1033,10 +1012,6 @@ return array( // "phd debug" are always launched in trace mdoe. See also 'phd.verbose'. 'phd.trace' => false, - // Path to custom celerity resource map relative to 'phabricator/src'. - // See also `scripts/celerity_mapper.php`. - 'celerity.resource-path' => '__celerity_resource_map__.php', - // This value is an input to the hash function when building resource hashes. // It has no security value, but if you accidentally poison user caches (by // pushing a bad patch or having something go wrong with a CDN, e.g.) you can diff --git a/externals/amazon-ses/ses.php b/externals/amazon-ses/ses.php index 01a1b234c4..b0eebce690 100644 --- a/externals/amazon-ses/ses.php +++ b/externals/amazon-ses/ses.php @@ -568,6 +568,7 @@ final class SimpleEmailServiceRequest * @return integer */ private function __responseWriteCallback(&$curl, &$data) { + if(!isset($this->response->body)) $this->response->body = ''; $this->response->body .= $data; return strlen($data); } @@ -739,4 +740,4 @@ final class SimpleEmailServiceMessage { */ final class SimpleEmailServiceException extends Exception { -} \ No newline at end of file +} diff --git a/externals/diff_match_patch/diff_match_patch.php b/externals/diff_match_patch/diff_match_patch.php new file mode 100644 index 0000000000..35bdb82068 --- /dev/null +++ b/externals/diff_match_patch/diff_match_patch.php @@ -0,0 +1,2117 @@ +>} Array of diff tuples. + */ + function diff_main($text1, $text2, $checklines = true) { + // Check for equality (speedup) + if ($text1 === $text2) { + return array ( array ( DIFF_EQUAL, $text1) ); + } + + // Trim off common prefix (speedup) + $commonlength = $this->diff_commonPrefix($text1, $text2); + $commonprefix = mb_substr($text1, 0, $commonlength); + $text1 = mb_substr($text1, $commonlength); + $text2 = mb_substr($text2, $commonlength); + + // Trim off common suffix (speedup) + $commonlength = $this->diff_commonSuffix($text1, $text2); + $commonsuffix = mb_substr($text1, mb_strlen($text1) - $commonlength); + $text1 = mb_substr($text1, 0, mb_strlen($text1) - $commonlength); + $text2 = mb_substr($text2, 0, mb_strlen($text2) - $commonlength); + + // Compute the diff on the middle block + $diffs = $this->diff_compute($text1, $text2, $checklines); + + // Restore the prefix and suffix + if ($commonprefix !== '') { + array_unshift($diffs, array ( DIFF_EQUAL, $commonprefix )); + } + if ($commonsuffix !== '') { + array_push($diffs, array ( DIFF_EQUAL, $commonsuffix )); + } + $this->diff_cleanupMerge($diffs); + return $diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff + * @return {Array.>} Array of diff tuples. + * @private + */ + function diff_compute($text1, $text2, $checklines) { + + if ($text1 === '') { + // Just add some text (speedup) + return array ( array ( DIFF_INSERT, $text2 ) ); + } + + if ($text2 === '') { + // Just delete some text (speedup) + return array ( array ( DIFF_DELETE, $text1 ) ); + } + + $longtext = mb_strlen($text1) > mb_strlen($text2) ? $text1 : $text2; + $shorttext = mb_strlen($text1) > mb_strlen($text2) ? $text2 : $text1; + $i = mb_strpos($longtext, $shorttext); + if ($i !== false) { + // Shorter text is inside the longer text (speedup) + $diffs = array ( + array ( DIFF_INSERT, mb_substr($longtext, 0, $i) ), + array ( DIFF_EQUAL, $shorttext ), + array ( DIFF_INSERT, mb_substr($longtext, $i +mb_strlen($shorttext)) ) + ); + + // Swap insertions for deletions if diff is reversed. + if (mb_strlen($text1) > mb_strlen($text2)) { + $diffs[0][0] = $diffs[2][0] = DIFF_DELETE; + } + return $diffs; + } + $longtext = $shorttext = null; // Garbage collect + + // Check to see if the problem can be split in two. + $hm = $this->diff_halfMatch($text1, $text2); + if ($hm) { + // A half-match was found, sort out the return data. + $text1_a = $hm[0]; + $text1_b = $hm[1]; + $text2_a = $hm[2]; + $text2_b = $hm[3]; + $mid_common = $hm[4]; + // Send both pairs off for separate processing. + $diffs_a = $this->diff_main($text1_a, $text2_a, $checklines); + $diffs_b = $this->diff_main($text1_b, $text2_b, $checklines); + // Merge the results. + return array_merge($diffs_a, array ( + array ( + DIFF_EQUAL, + $mid_common + ) + ), $diffs_b); + } + + // Perform a real diff. + if ($checklines && (mb_strlen($text1) < 100 || mb_strlen($text2) < 100)) { + // Too trivial for the overhead. + $checklines = false; + } + $linearray = null; + if ($checklines) { + // Scan the text on a line-by-line basis first. + $a = $this->diff_linesToChars($text1, $text2); + $text1 = $a[0]; + $text2 = $a[1]; + $linearray = $a[2]; + } + $diffs = $this->diff_map($text1, $text2); + if (!$diffs) { + // No acceptable result. + $diffs = array ( + array ( + DIFF_DELETE, + $text1 + ), + array ( + DIFF_INSERT, + $text2 + ) + ); + } + if ($checklines) { + // Convert the diff back to original text. + $this->diff_charsToLines($diffs, $linearray); + // Eliminate freak matches (e.g. blank lines) + $this->diff_cleanupSemantic($diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + array_push($diffs, array ( + DIFF_EQUAL, + '' + )); + $pointer = 0; + $count_delete = 0; + $count_insert = 0; + $text_delete = ''; + $text_insert = ''; + while ($pointer < count($diffs)) { + switch ($diffs[$pointer][0]) { + case DIFF_INSERT : + $count_insert++; + $text_insert .= $diffs[$pointer][1]; + break; + case DIFF_DELETE : + $count_delete++; + $text_delete .= $diffs[$pointer][1]; + break; + case DIFF_EQUAL : + // Upon reaching an equality, check for prior redundancies. + if ($count_delete >= 1 && $count_insert >= 1) { + // Delete the offending records and add the merged ones. + $a = $this->diff_main($text_delete, $text_insert, false); + array_splice($diffs, $pointer - $count_delete - $count_insert, $count_delete + $count_insert); + + $pointer = $pointer - $count_delete - $count_insert; + for ($j = count($a) - 1; $j >= 0; $j--) { + array_splice($diffs, $pointer, 0, array($a[$j])); + } + $pointer = $pointer +count($a); + } + $count_insert = 0; + $count_delete = 0; + $text_delete = ''; + $text_insert = ''; + break; + } + $pointer++; + } + array_pop($diffs); // Remove the dummy entry at the end. + } + return $diffs; + } + + /** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.>} Three element Array, containing the + * encoded text1, the encoded text2 and the array of unique strings. The + * zeroth element of the array of unique strings is intentionally blank. + * @private + */ + function diff_linesToChars($text1, $text2) { + $lineArray = array(); // e.g. lineArray[4] == 'Hello\n' + $lineHash = array(); // e.g. lineHash['Hello\n'] == 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + $lineArray[0] = ''; + + $chars1 = $this->diff_linesToCharsMunge($text1, $lineArray, $lineHash); + $chars2 = $this->diff_linesToCharsMunge($text2, $lineArray, $lineHash); + return array ( + $chars1, + $chars2, + $lineArray + ); + } + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode + * @return {string} Encoded string + * @private + */ + function diff_linesToCharsMunge($text, &$lineArray, &$lineHash) { + $chars = ''; + // Walk the text, pulling out a mb_substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + $lineStart = 0; + $lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + $lineArrayLength = count($lineArray); + while ($lineEnd < mb_strlen($text) - 1) { + $lineEnd = mb_strpos($text, "\n", $lineStart); + if ($lineEnd === false) { + $lineEnd = mb_strlen($text) - 1; + } + $line = mb_substr($text, $lineStart, $lineEnd +1 -$lineStart); + $lineStart = $lineEnd +1; + + if ( isset($lineHash[$line]) ) { + $chars .= mb_chr($lineHash[$line]); + } else { + $chars .= mb_chr($lineArrayLength); + $lineHash[$line] = $lineArrayLength; + $lineArray[$lineArrayLength++] = $line; + } + } + return $chars; + } + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {Array.>} diffs Array of diff tuples. + * @param {Array.} lineArray Array of unique strings. + * @private + */ + function diff_charsToLines(&$diffs, $lineArray) { + for ($x = 0; $x < count($diffs); $x++) { + $chars = $diffs[$x][1]; + $text = array (); + for ($y = 0; $y < mb_strlen($chars); $y++) { + $text[$y] = $lineArray[charCodeAt($chars, $y)]; + } + $diffs[$x][1] = implode('',$text); + } + } + + /** + * Explore the intersection points between the two texts. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @return {Array.>?} Array of diff tuples or null if no + * diff available. + * @private + */ + function diff_map($text1, $text2) { + // Don't run for too long. + $ms_end = microtime(true) + $this->Diff_Timeout; + + // Cache the text lengths to prevent multiple calls. + $text1_length = mb_strlen($text1); + $text2_length = mb_strlen($text2); + $max_d = $text1_length + $text2_length -1; + $doubleEnd = $this->Diff_DualThreshold * 2 < $max_d; + $v_map1 = array(); + $v_map2 = array(); + $v1 = array(); + $v2 = array(); + $v1[1] = 0; + $v2[1] = 0; + $x = null; + $y = null; + $footstep = null; // Used to track overlapping paths. + $footsteps = array(); + $done = false; + // Safari 1.x doesn't have hasOwnProperty + //? $hasOwnProperty = !!(footsteps.hasOwnProperty); + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + $front = ($text1_length + $text2_length) % 2; + for ($d = 0; $d < $max_d; $d++) { + // Bail out if timeout reached. + if ($this->Diff_Timeout > 0 && microtime(true) > $ms_end) { + return null; // zzz + } + + // Walk the front path one step. + $v_map1[$d] = array (); + for ($k = -$d; $k <= $d; $k += 2) { + if ($k == -$d || $k != $d && $v1[$k -1] < $v1[$k +1]) { + $x = $v1[$k +1]; + } else { + $x = $v1[$k -1] + 1; + } + $y = $x - $k; + if ($doubleEnd) { + $footstep = $x . ',' . $y; + if ($front && isset ($footsteps[$footstep])) { + $done = true; + } + if (!$front) { + $footsteps[$footstep] = $d; + } + } + while (!$done && ($x < $text1_length) && ($y < $text2_length) && (mb_substr($text1, $x, 1) == mb_substr($text2, $y, 1)) ) { + $x++; + $y++; + if ($doubleEnd) { + $footstep = $x . ',' . $y; + if ($front && isset ($footsteps[$footstep])) { + $done = true; + } + if (!$front) { + $footsteps[$footstep] = $d; + } + } + } + $v1[$k] = $x; + $v_map1[$d][$x . ',' . $y] = true; + if ($x == $text1_length && $y == $text2_length) { + // Reached the end in single-path mode. + return $this->diff_path1($v_map1, $text1, $text2); + } + elseif ($done) { + // Front path ran over reverse path. + + $v_map2 = array_slice($v_map2, 0, $footsteps[$footstep] + 1); + $a = $this->diff_path1($v_map1, mb_substr($text1, 0, $x), mb_substr($text2, 0, $y)); + + return array_merge($a, $this->diff_path2($v_map2, mb_substr($text1, $x), mb_substr($text2, $y))); + } + } + + if ($doubleEnd) { + // Walk the reverse path one step. + $v_map2[$d] = array(); + for ($k = -$d; $k <= $d; $k += 2) { + if ($k == -$d || $k != $d && $v2[$k -1] < $v2[$k +1]) { + $x = $v2[$k +1]; + } else { + $x = $v2[$k -1] + 1; + } + $y = $x - $k; + $footstep = ($text1_length - $x) . ',' . ($text2_length - $y); + if (!$front && isset ($footsteps[$footstep])) { + $done = true; + } + if ($front) { + $footsteps[$footstep] = $d; + } + while (!$done && $x < $text1_length && $y < $text2_length && mb_substr($text1, $text1_length - $x -1, 1) == mb_substr($text2, $text2_length - $y -1, 1) ) { + $x++; + $y++; + $footstep = ($text1_length - $x) . ',' . ($text2_length - $y); + if (!$front && isset ($footsteps[$footstep])) { + $done = true; + } + if ($front) { + $footsteps[$footstep] = $d; + } + } + $v2[$k] = $x; + $v_map2[$d][$x . ',' . $y] = true; + if ($done) { + // Reverse path ran over front path. + $v_map1 = array_slice($v_map1, 0, $footsteps[$footstep] + 1); + $a = $this->diff_path1($v_map1, mb_substr($text1, 0, $text1_length - $x), mb_substr($text2, 0, $text2_length - $y)); + return array_merge($a, $this->diff_path2($v_map2, mb_substr($text1, $text1_length - $x), mb_substr($text2, $text2_length - $y))); + } + } + } + } + // Number of diffs equals number of characters, no commonality at all. + return null; + } + + /** + * Work from the middle back to the start to determine the path. + * @param {Array.} v_map Array of paths.ers + * @param {string} text1 Old string fragment to be diffed. + * @param {string} text2 New string fragment to be diffed. + * @return {Array.>} Array of diff tuples. + * @private + */ + function diff_path1($v_map, $text1, $text2) { + $path = array (); + $x = mb_strlen($text1); + $y = mb_strlen($text2); + /** @type {number?} */ + $last_op = null; + for ($d = count($v_map) - 2; $d >= 0; $d--) { + while (1) { + if (isset ($v_map[$d][($x -1) . ',' . $y])) { + $x--; + if ($last_op === DIFF_DELETE) { + $path[0][1] = mb_substr($text1, $x, 1) . $path[0][1]; + } else { + array_unshift($path, array ( + DIFF_DELETE, + mb_substr($text1, $x, 1) + )); + } + $last_op = DIFF_DELETE; + break; + } elseif (isset ($v_map[$d][$x . ',' . ($y -1)])) { + $y--; + if ($last_op === DIFF_INSERT) { + $path[0][1] = mb_substr($text2, $y, 1) . $path[0][1]; + } else { + array_unshift($path, array ( + DIFF_INSERT, + mb_substr($text2, $y, 1) + )); + } + $last_op = DIFF_INSERT; + break; + } else { + $x--; + $y--; + //if (text1.charAt(x) != text2.charAt(y)) { + // throw new Error('No diagonal. Can\'t happen. (diff_path1)'); + //} + if ($last_op === DIFF_EQUAL) { + $path[0][1] = mb_substr($text1, $x, 1) . $path[0][1]; + } else { + array_unshift($path, array ( + DIFF_EQUAL, + mb_substr($text1, $x, 1) + )); + } + $last_op = DIFF_EQUAL; + } + } + } + return $path; + } + + /** + * Work from the middle back to the end to determine the path. + * @param {Array.} v_map Array of paths. + * @param {string} text1 Old string fragment to be diffed. + * @param {string} text2 New string fragment to be diffed. + * @return {Array.>} Array of diff tuples. + * @private + */ + function diff_path2($v_map, $text1, $text2) { + $path = array (); + $pathLength = 0; + $x = mb_strlen($text1); + $y = mb_strlen($text2); + /** @type {number?} */ + $last_op = null; + for ($d = count($v_map) - 2; $d >= 0; $d--) { + while (1) { + if (isset ($v_map[$d][($x -1) . ',' . $y])) { + $x--; + if ($last_op === DIFF_DELETE) { + $path[$pathLength -1][1] .= $text1[mb_strlen($text1) - $x -1]; + } else { + $path[$pathLength++] = array ( + DIFF_DELETE, + $text1[mb_strlen($text1) - $x -1] + ); + } + $last_op = DIFF_DELETE; + break; + } + elseif (isset ($v_map[$d][$x . ',' . ($y -1)])) { + $y--; + if ($last_op === DIFF_INSERT) { + $path[$pathLength -1][1] .= $text2[mb_strlen($text2) - $y -1]; + } else { + $path[$pathLength++] = array ( + DIFF_INSERT, + $text2[mb_strlen($text2) - $y -1] + ); + } + $last_op = DIFF_INSERT; + break; + } else { + $x--; + $y--; + //if (text1.charAt(text1.length - x - 1) != + // text2.charAt(text2.length - y - 1)) { + // throw new Error('No diagonal. Can\'t happen. (diff_path2)'); + //} + if ($last_op === DIFF_EQUAL) { + $path[$pathLength -1][1] .= $text1[mb_strlen($text1) - $x -1]; + } else { + $path[$pathLength++] = array ( + DIFF_EQUAL, + $text1[mb_strlen($text1) - $x -1] + ); + } + $last_op = DIFF_EQUAL; + } + } + } + return $path; + } + + /** + * Determine the common prefix of two strings + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ + function diff_commonPrefix($text1, $text2) { + for ($i = 0; 1; $i++) { + $t1 = mb_substr($text1, $i, 1); + $t2 = mb_substr($text2, $i, 1); + if($t1==='' || $t2==='' || $t1 !== $t2 ){ + return $i; + } + } + } + + /** + * Determine the common suffix of two strings + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ + function diff_commonSuffix($text1, $text2) { + return $this->diff_commonPrefix( strrev($text1), strrev($text2) ); + } + + /** + * Do the two texts share a mb_substring which is at least half the length of the + * longer text? + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.?} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + */ + function diff_halfMatch($text1, $text2) { + $longtext = mb_strlen($text1) > mb_strlen($text2) ? $text1 : $text2; + $shorttext = mb_strlen($text1) > mb_strlen($text2) ? $text2 : $text1; + if (mb_strlen($longtext) < 10 || mb_strlen($shorttext) < 1) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + $hm1 = $this->diff_halfMatchI($longtext, $shorttext, ceil(mb_strlen($longtext) / 4)); + // Check again based on the third quarter. + $hm2 = $this->diff_halfMatchI($longtext, $shorttext, ceil(mb_strlen($longtext) / 2)); + + if (!$hm1 && !$hm2) { + return null; + } elseif (!$hm2) { + $hm = $hm1; + } elseif (!$hm1) { + $hm = $hm2; + } else { + // Both matched. Select the longest. + $hm = mb_strlen($hm1[4]) > mb_strlen($hm2[4]) ? $hm1 : $hm2; + } + + // A half-match was found, sort out the return data. + if (mb_strlen($text1) > mb_strlen($text2)) { + $text1_a = $hm[0]; + $text1_b = $hm[1]; + $text2_a = $hm[2]; + $text2_b = $hm[3]; + } else { + $text2_a = $hm[0]; + $text2_b = $hm[1]; + $text1_a = $hm[2]; + $text1_b = $hm[3]; + } + $mid_common = $hm[4]; + return array( $text1_a, $text1_b, $text2_a, $text2_b, $mid_common ); + } + + /** + * Does a mb_substring of shorttext exist within longtext such that the mb_substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length mb_substring within longtext + * @return {Array.?} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diff_halfMatchI($longtext, $shorttext, $i) { + // Start with a 1/4 length mb_substring at position i as a seed. + $seed = mb_substr($longtext, $i, floor(mb_strlen($longtext) / 4)); + + $j = -1; + $best_common = ''; + $best_longtext_a = null; + $best_longtext_b = null; + $best_shorttext_a = null; + $best_shorttext_b = null; + while ( ($j = mb_strpos($shorttext, $seed, $j + 1)) !== false ) { + $prefixLength = $this->diff_commonPrefix(mb_substr($longtext, $i), mb_substr($shorttext, $j)); + $suffixLength = $this->diff_commonSuffix(mb_substr($longtext, 0, $i), mb_substr($shorttext, 0, $j)); + if (mb_strlen($best_common) < $suffixLength + $prefixLength) { + $best_common = mb_substr($shorttext, $j - $suffixLength, $suffixLength) . mb_substr($shorttext, $j, $prefixLength); + $best_longtext_a = mb_substr($longtext, 0, $i - $suffixLength); + $best_longtext_b = mb_substr($longtext, $i + $prefixLength); + $best_shorttext_a = mb_substr($shorttext, 0, $j - $suffixLength); + $best_shorttext_b = mb_substr($shorttext, $j + $prefixLength); + } + } + if (mb_strlen($best_common) >= mb_strlen($longtext) / 2) { + return array ( + $best_longtext_a, + $best_longtext_b, + $best_shorttext_a, + $best_shorttext_b, + $best_common + ); + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {Array.>} diffs Array of diff tuples. + */ + function diff_cleanupSemantic(&$diffs) { + $changes = false; + $equalities = array (); // Stack of indices where equalities are found. + $equalitiesLength = 0; // Keeping our own length var is faster in JS. + $lastequality = null; // Always equal to equalities[equalitiesLength-1][1] + $pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + $length_changes1 = 0; + // Number of characters that changed after the equality. + $length_changes2 = 0; + while ($pointer < count($diffs)) { + if ($diffs[$pointer][0] == DIFF_EQUAL) { // equality found + $equalities[$equalitiesLength++] = $pointer; + $length_changes1 = $length_changes2; + $length_changes2 = 0; + $lastequality = $diffs[$pointer][1]; + } else { // an insertion or deletion + $length_changes2 += mb_strlen($diffs[$pointer][1]); + if ($lastequality !== null && (mb_strlen($lastequality) <= $length_changes1) && (mb_strlen($lastequality) <= $length_changes2)) { + // Duplicate record + $zzz_diffs = array_splice($diffs, $equalities[$equalitiesLength -1], 0, array(array ( + DIFF_DELETE, + $lastequality + ))); + // Change second copy to insert. + $diffs[$equalities[$equalitiesLength -1] + 1][0] = DIFF_INSERT; + // Throw away the equality we just deleted. + $equalitiesLength--; + // Throw away the previous equality (it needs to be reevaluated). + $equalitiesLength--; + $pointer = $equalitiesLength > 0 ? $equalities[$equalitiesLength -1] : -1; + $length_changes1 = 0; // Reset the counters. + $length_changes2 = 0; + $lastequality = null; + $changes = true; + } + } + $pointer++; + } + if ($changes) { + $this->diff_cleanupMerge($diffs); + } + $this->diff_cleanupSemanticLossless($diffs); + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param {Array.>} diffs Array of diff tuples. + */ + function diff_cleanupSemanticLossless(&$diffs) { + + $pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while ($pointer < count($diffs) - 1) { + if ($diffs[$pointer -1][0] == DIFF_EQUAL && $diffs[$pointer +1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + $equality1 = $diffs[$pointer -1][1]; + $edit = $diffs[$pointer][1]; + $equality2 = $diffs[$pointer +1][1]; + + // First, shift the edit as far left as possible. + $commonOffset = $this->diff_commonSuffix($equality1, $edit); + if ($commonOffset !== '') { + $commonString = mb_substr($edit, mb_strlen($edit) - $commonOffset); + $equality1 = mb_substr($equality1, 0, mb_strlen($equality1) - $commonOffset); + $edit = $commonString . mb_substr($edit, 0, mb_strlen($edit) - $commonOffset); + $equality2 = $commonString . $equality2; + } + + // Second, step character by character right, looking for the best fit. + $bestEquality1 = $equality1; + $bestEdit = $edit; + $bestEquality2 = $equality2; + $bestScore = $this->diff_cleanupSemanticScore($equality1, $edit) + $this->diff_cleanupSemanticScore($edit, $equality2); + while (isset($equality2[0]) && $edit[0] === $equality2[0]) { + $equality1 .= $edit[0]; + $edit = mb_substr($edit, 1) . $equality2[0]; + $equality2 = mb_substr($equality2, 1); + $score = $this->diff_cleanupSemanticScore($equality1, $edit) + $this->diff_cleanupSemanticScore($edit, $equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if ($score >= $bestScore) { + $bestScore = $score; + $bestEquality1 = $equality1; + $bestEdit = $edit; + $bestEquality2 = $equality2; + } + } + + if ($diffs[$pointer -1][1] != $bestEquality1) { + // We have an improvement, save it back to the diff. + if ($bestEquality1) { + $diffs[$pointer -1][1] = $bestEquality1; + } else { + $zzz_diffs = array_splice($diffs, $pointer -1, 1); + $pointer--; + } + $diffs[$pointer][1] = $bestEdit; + if ($bestEquality2) { + $diffs[$pointer +1][1] = $bestEquality2; + } else { + $zzz_diffs = array_splice($diffs, $pointer +1, 1); + $pointer--; + } + } + } + $pointer++; + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 5 (best) to 0 (worst). + * Closure, makes reference to regex patterns defined above. + * @param {string} one First string + * @param {string} two Second string + * @return {number} The score. + */ + function diff_cleanupSemanticScore($one, $two) { + // Define some regex patterns for matching boundaries. + $punctuation = '/[^a-zA-Z0-9]/'; + $whitespace = '/\s/'; + $linebreak = '/[\r\n]/'; + $blanklineEnd = '/\n\r?\n$/'; + $blanklineStart = '/^\r?\n\r?\n/'; + + if (!$one || !$two) { + // Edges are the best. + return 5; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + $score = 0; + // One point for non-alphanumeric. + if (preg_match($punctuation, $one[mb_strlen($one) - 1]) || preg_match($punctuation, $two[0])) { + $score++; + // Two points for whitespace. + if (preg_match($whitespace, $one[mb_strlen($one) - 1] ) || preg_match($whitespace, $two[0])) { + $score++; + // Three points for line breaks. + if (preg_match($linebreak, $one[mb_strlen($one) - 1]) || preg_match($linebreak, $two[0])) { + $score++; + // Four points for blank lines. + if (preg_match($blanklineEnd, $one) || preg_match($blanklineStart, $two)) { + $score++; + } + } + } + } + return $score; + } + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {Array.>} diffs Array of diff tuples. + */ + function diff_cleanupEfficiency(&$diffs) { + $changes = false; + $equalities = array (); // Stack of indices where equalities are found. + $equalitiesLength = 0; // Keeping our own length var is faster in JS. + $lastequality = ''; // Always equal to equalities[equalitiesLength-1][1] + $pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + $pre_ins = false; + // Is there a deletion operation before the last equality. + $pre_del = false; + // Is there an insertion operation after the last equality. + $post_ins = false; + // Is there a deletion operation after the last equality. + $post_del = false; + while ($pointer < count($diffs)) { + if ($diffs[$pointer][0] == DIFF_EQUAL) { // equality found + if (mb_strlen($diffs[$pointer][1]) < $this->Diff_EditCost && ($post_ins || $post_del)) { + // Candidate found. + $equalities[$equalitiesLength++] = $pointer; + $pre_ins = $post_ins; + $pre_del = $post_del; + $lastequality = $diffs[$pointer][1]; + } else { + // Not a candidate, and can never become one. + $equalitiesLength = 0; + $lastequality = ''; + } + $post_ins = $post_del = false; + } else { // an insertion or deletion + if ($diffs[$pointer][0] == DIFF_DELETE) { + $post_del = true; + } else { + $post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ($lastequality && (($pre_ins && $pre_del && $post_ins && $post_del) || ((mb_strlen($lastequality) < $this->Diff_EditCost / 2) && ($pre_ins + $pre_del + $post_ins + $post_del) == 3))) { + // Duplicate record + $zzz_diffs = array_splice($diffs, $equalities[$equalitiesLength -1], 0, array(array ( + DIFF_DELETE, + $lastequality + ))); + // Change second copy to insert. + $diffs[$equalities[$equalitiesLength -1] + 1][0] = DIFF_INSERT; + $equalitiesLength--; // Throw away the equality we just deleted; + $lastequality = ''; + if ($pre_ins && $pre_del) { + // No changes made which could affect previous entry, keep going. + $post_ins = $post_del = true; + $equalitiesLength = 0; + } else { + $equalitiesLength--; // Throw away the previous equality; + $pointer = $equalitiesLength > 0 ? $equalities[$equalitiesLength -1] : -1; + $post_ins = $post_del = false; + } + $changes = true; + } + } + $pointer++; + } + + if ($changes) { + $this->diff_cleanupMerge($diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {Array.>} diffs Array of diff tuples. + */ + function diff_cleanupMerge(&$diffs) { + array_push($diffs, array ( DIFF_EQUAL, '' )); // Add a dummy entry at the end. + $pointer = 0; + $count_delete = 0; + $count_insert = 0; + $text_delete = ''; + $text_insert = ''; + $commonlength = null; + while ($pointer < count($diffs)) { + switch ($diffs[$pointer][0]) { + case DIFF_INSERT : + $count_insert++; + $text_insert .= $diffs[$pointer][1]; + $pointer++; + break; + case DIFF_DELETE : + $count_delete++; + $text_delete .= $diffs[$pointer][1]; + $pointer++; + break; + case DIFF_EQUAL : + // Upon reaching an equality, check for prior redundancies. + if ($count_delete !== 0 || $count_insert !== 0) { + if ($count_delete !== 0 && $count_insert !== 0) { + // Factor out any common prefixies. + $commonlength = $this->diff_commonPrefix($text_insert, $text_delete); + if ($commonlength !== 0) { + if (($pointer - $count_delete - $count_insert) > 0 && $diffs[$pointer - $count_delete - $count_insert -1][0] == DIFF_EQUAL) { + $diffs[$pointer - $count_delete - $count_insert -1][1] .= mb_substr($text_insert, 0, $commonlength); + } else { + array_splice($diffs, 0, 0, array(array ( + DIFF_EQUAL, + mb_substr($text_insert, 0, $commonlength) + ))); + $pointer++; + } + $text_insert = mb_substr($text_insert, $commonlength); + $text_delete = mb_substr($text_delete, $commonlength); + } + // Factor out any common suffixies. + $commonlength = $this->diff_commonSuffix($text_insert, $text_delete); + if ($commonlength !== 0) { + $diffs[$pointer][1] = mb_substr($text_insert, mb_strlen($text_insert) - $commonlength) . $diffs[$pointer][1]; + $text_insert = mb_substr($text_insert, 0, mb_strlen($text_insert) - $commonlength); + $text_delete = mb_substr($text_delete, 0, mb_strlen($text_delete) - $commonlength); + } + } + // Delete the offending records and add the merged ones. + if ($count_delete === 0) { + array_splice($diffs, $pointer-$count_delete-$count_insert, $count_delete+$count_insert, array(array( + DIFF_INSERT, + $text_insert + ))); + } elseif ($count_insert === 0) { + array_splice($diffs, $pointer-$count_delete-$count_insert, $count_delete+$count_insert, array(array( + DIFF_DELETE, + $text_delete + ))); + } else { + array_splice($diffs, $pointer-$count_delete-$count_insert, $count_delete+$count_insert, array(array( + DIFF_DELETE, + $text_delete + ), array ( + DIFF_INSERT, + $text_insert + ))); + } + $pointer = $pointer - $count_delete - $count_insert + ($count_delete ? 1 : 0) + ($count_insert ? 1 : 0) + 1; + } elseif ($pointer !== 0 && $diffs[$pointer -1][0] == DIFF_EQUAL) { + // Merge this equality with the previous one. + $diffs[$pointer -1][1] .= $diffs[$pointer][1]; + array_splice($diffs, $pointer, 1); + } else { + $pointer++; + } + $count_insert = 0; + $count_delete = 0; + $text_delete = ''; + $text_insert = ''; + break; + } + } + if ($diffs[count($diffs) - 1][1] === '') { + array_pop($diffs); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + $changes = false; + $pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while ($pointer < count($diffs) - 1) { + if ($diffs[$pointer-1][0] == DIFF_EQUAL && $diffs[$pointer+1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if ( mb_substr($diffs[$pointer][1], mb_strlen($diffs[$pointer][1]) - mb_strlen($diffs[$pointer -1][1])) == $diffs[$pointer -1][1]) { + // Shift the edit over the previous equality. + $diffs[$pointer][1] = $diffs[$pointer -1][1] . mb_substr($diffs[$pointer][1], 0, mb_strlen($diffs[$pointer][1]) - mb_strlen($diffs[$pointer -1][1])); + $diffs[$pointer +1][1] = $diffs[$pointer -1][1] . $diffs[$pointer +1][1]; + array_splice($diffs, $pointer -1, 1); + $changes = true; + } elseif (mb_substr($diffs[$pointer][1], 0, mb_strlen($diffs[$pointer +1][1])) == $diffs[$pointer +1][1]) { + // Shift the edit over the next equality. + $diffs[$pointer -1][1] .= $diffs[$pointer +1][1]; + + $diffs[$pointer][1] = mb_substr($diffs[$pointer][1], mb_strlen($diffs[$pointer +1][1])) . $diffs[$pointer +1][1]; + array_splice($diffs, $pointer +1, 1); + $changes = true; + } + } + $pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if ($changes) { + $this->diff_cleanupMerge($diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 + * @param {Array.>} diffs Array of diff tuples. + * @param {number} loc Location within text1. + * @return {number} Location within text2. + */ + function diff_xIndex($diffs, $loc) { + $chars1 = 0; + $chars2 = 0; + $last_chars1 = 0; + $last_chars2 = 0; + for ($x = 0; $x < count($diffs); $x++) { + if ($diffs[$x][0] !== DIFF_INSERT) { // Equality or deletion. + $chars1 += mb_strlen($diffs[$x][1]); + } + if ($diffs[$x][0] !== DIFF_DELETE) { // Equality or insertion. + $chars2 += mb_strlen($diffs[$x][1]); + } + if ($chars1 > $loc) { // Overshot the location. + break; + } + $last_chars1 = $chars1; + $last_chars2 = $chars2; + } + // Was the location was deleted? + if (count($diffs) != $x && $diffs[$x][0] === DIFF_DELETE) { + return $last_chars2; + } + // Add the remaining character length. + return $last_chars2 + ($loc - $last_chars1); + } + + /** + * Convert a diff array into a pretty HTML report. + * @param {Array.>} diffs Array of diff tuples. + * @return {string} HTML representation. + */ + function diff_prettyHtml($diffs) { + $html = array (); + $i = 0; + for ($x = 0; $x < count($diffs); $x++) { + $op = $diffs[$x][0]; // Operation (insert, delete, equal) + $data = $diffs[$x][1]; // Text of change. + $text = preg_replace(array ( + '/&/', + '//', + "/\n/" + ), array ( + '&', + '<', + '>', + '¶
' + ), $data); + + switch ($op) { + case DIFF_INSERT : + $html[$x] = '' . $text . ''; + break; + case DIFF_DELETE : + $html[$x] = '' . $text . ''; + break; + case DIFF_EQUAL : + $html[$x] = '' . $text . ''; + break; + } + if ($op !== DIFF_DELETE) { + $i += mb_strlen($data); + } + } + return implode('',$html); + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param {Array.>} diffs Array of diff tuples. + * @return {string} Source text. + */ + function diff_text1($diffs) { + $text = array (); + for ($x = 0; $x < count($diffs); $x++) { + if ($diffs[$x][0] !== DIFF_INSERT) { + $text[$x] = $diffs[$x][1]; + } + } + return implode('',$text); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param {Array.>} diffs Array of diff tuples. + * @return {string} Destination text. + */ + function diff_text2($diffs) { + $text = array (); + for ($x = 0; $x < count($diffs); $x++) { + if ($diffs[$x][0] !== DIFF_DELETE) { + $text[$x] = $diffs[$x][1]; + } + } + return implode('',$text); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param {Array.>} diffs Array of diff tuples. + * @return {number} Number of changes. + */ + function diff_levenshtein($diffs) { + $levenshtein = 0; + $insertions = 0; + $deletions = 0; + for ($x = 0; $x < count($diffs); $x++) { + $op = $diffs[$x][0]; + $data = $diffs[$x][1]; + switch ($op) { + case DIFF_INSERT : + $insertions += mb_strlen($data); + break; + case DIFF_DELETE : + $deletions += mb_strlen($data); + break; + case DIFF_EQUAL : + // A deletion and an insertion is one substitution. + $levenshtein += max($insertions, $deletions); + $insertions = 0; + $deletions = 0; + break; + } + } + $levenshtein += max($insertions, $deletions); + return $levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param {Array.>} diffs Array of diff tuples. + * @return {string} Delta text. + */ + function diff_toDelta($diffs) { + $text = array (); + for ($x = 0; $x < count($diffs); $x++) { + switch ($diffs[$x][0]) { + case DIFF_INSERT : + $text[$x] = '+' .encodeURI($diffs[$x][1]); + break; + case DIFF_DELETE : + $text[$x] = '-' .mb_strlen($diffs[$x][1]); + break; + case DIFF_EQUAL : + $text[$x] = '=' .mb_strlen($diffs[$x][1]); + break; + } + } + return str_replace('%20', ' ', implode("\t", $text)); + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param {string} text1 Source string for the diff. + * @param {string} delta Delta text. + * @return {Array.>} Array of diff tuples. + * @throws {Error} If invalid input. + */ + function diff_fromDelta($text1, $delta) { + $diffs = array (); + $diffsLength = 0; // Keeping our own length var is faster in JS. + $pointer = 0; // Cursor in text1 + $tokens = preg_split("/\t/", $delta); + + for ($x = 0; $x < count($tokens); $x++) { + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + $param = mb_substr($tokens[$x], 1); + switch ($tokens[$x][0]) { + case '+' : + try { + $diffs[$diffsLength++] = array ( + DIFF_INSERT, + decodeURI($param) + ); + } catch (Exception $ex) { + echo_Exception('Illegal escape in diff_fromDelta: ' . $param); + // Malformed URI sequence. + } + break; + case '-' : + // Fall through. + case '=' : + $n = (int) $param; + if ($n < 0) { + echo_Exception('Invalid number in diff_fromDelta: ' . $param); + } + $text = mb_substr($text1, $pointer, $n); + $pointer += $n; + if ($tokens[$x][0] == '=') { + $diffs[$diffsLength++] = array ( + DIFF_EQUAL, + $text + ); + } else { + $diffs[$diffsLength++] = array ( + DIFF_DELETE, + $text + ); + } + break; + default : + // Blank tokens are ok (from a trailing \t). + // Anything else is an error. + if ($tokens[$x]) { + echo_Exception('Invalid diff operation in diff_fromDelta: ' . $tokens[$x]); + } + } + } + if ($pointer != mb_strlen($text1)) { +// throw new Exception('Delta length (' . $pointer . ') does not equal source text length (' . mb_strlen($text1) . ').'); + echo_Exception('Delta length (' . $pointer . ') does not equal source text length (' . mb_strlen($text1) . ').'); + } + return $diffs; + } + + // MATCH FUNCTIONS + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + */ + function match_main($text, $pattern, $loc) { + $loc = max(0, min($loc, mb_strlen($text))); + if ($text == $pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } + elseif (!mb_strlen($text)) { + // Nothing to match. + return -1; + } + elseif (mb_substr($text, $loc, mb_strlen($pattern)) == $pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return $loc; + } else { + // Do a fuzzy compare. + return $this->match_bitap($text, $pattern, $loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + * @private + */ + function match_bitap($text, $pattern, $loc) { + if (mb_strlen($pattern) > Match_MaxBits) { + echo_Exception('Pattern too long for this browser.'); + } + + // Initialise the alphabet. + $s = $this->match_alphabet($pattern); + + // Highest score beyond which we give up. + $score_threshold = $this->Match_Threshold; + + // Is there a nearby exact match? (speedup) + $best_loc = mb_strpos($text, $pattern, $loc); + if ($best_loc !== false) { + $score_threshold = min($this->match_bitapScore(0, $best_loc, $pattern, $loc), $score_threshold); + } + + // What about in the other direction? (speedup) + $best_loc = mb_strrpos( $text, $pattern, min($loc + mb_strlen($pattern), mb_strlen($text)) ); + if ($best_loc !== false) { + $score_threshold = min($this->match_bitapScore(0, $best_loc, $pattern, $loc), $score_threshold); + } + + // Initialise the bit arrays. + $matchmask = 1 << (mb_strlen($pattern) - 1); + $best_loc = -1; + + $bin_min = null; + $bin_mid = null; + $bin_max = mb_strlen($pattern) + mb_strlen($text); + $last_rd = null; + for ($d = 0; $d < mb_strlen($pattern); $d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at this + // error level. + $bin_min = 0; + $bin_mid = $bin_max; + while ($bin_min < $bin_mid) { + if ($this->match_bitapScore($d, $loc + $bin_mid, $pattern, $loc) <= $score_threshold) { + $bin_min = $bin_mid; + } else { + $bin_max = $bin_mid; + } + $bin_mid = floor(($bin_max - $bin_min) / 2 + $bin_min); + } + // Use the result from this iteration as the maximum for the next. + $bin_max = $bin_mid; + $start = max(1, $loc - $bin_mid +1); + $finish = min($loc + $bin_mid, mb_strlen($text)) + mb_strlen($pattern); + + $rd = Array ( + $finish +2 + ); + $rd[$finish +1] = (1 << $d) - 1; + for ($j = $finish; $j >= $start; $j--) { + // The alphabet (s) is a sparse hash, so the following line generates + // warnings. +@ $charMatch = $s[ $text[$j -1] ]; + if ($d === 0) { // First pass: exact match. + $rd[$j] = (($rd[$j +1] << 1) | 1) & $charMatch; + } else { // Subsequent passes: fuzzy match. + $rd[$j] = (($rd[$j +1] << 1) | 1) & $charMatch | ((($last_rd[$j +1] | $last_rd[$j]) << 1) | 1) | $last_rd[$j +1]; + } + if ($rd[$j] & $matchmask) { + $score = $this->match_bitapScore($d, $j -1, $pattern, $loc); + // This match will almost certainly be better than any existing match. + // But check anyway. + if ($score <= $score_threshold) { + // Told you so. + $score_threshold = $score; + $best_loc = $j -1; + if ($best_loc > $loc) { + // When passing loc, don't exceed our current distance from loc. + $start = max(1, 2 * $loc - $best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if ($this->match_bitapScore($d +1, $loc, $pattern, $loc) > $score_threshold) { + break; + } + $last_rd = $rd; + } + return (int)$best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + */ + function match_bitapScore($e, $x, $pattern, $loc) { + $accuracy = $e / mb_strlen($pattern); + $proximity = abs($loc - $x); + if (!$this->Match_Distance) { + // Dodge divide by zero error. + return $proximity ? 1.0 : $accuracy; + } + return $accuracy + ($proximity / $this->Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param {string} pattern The text to encode. + * @return {Object} Hash of character locations. + * @private + */ + function match_alphabet($pattern) { + $s = array (); + for ($i = 0; $i < mb_strlen($pattern); $i++) { + $s[ $pattern[$i] ] = 0; + } + for ($i = 0; $i < mb_strlen($pattern); $i++) { + $s[ $pattern[$i] ] |= 1 << (mb_strlen($pattern) - $i - 1); + } + return $s; + } + + // PATCH FUNCTIONS + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param {patch_obj} patch The patch to grow. + * @param {string} text Source text. + * @private + */ + function patch_addContext($patch, $text) { + $pattern = mb_substr($text, $patch->start2, $patch->length1 ); + $previousPattern = null; + $padding = 0; + $i = 0; + while ( + ( mb_strlen($pattern) === 0 // Javascript's indexOf/lastIndexOd return 0/strlen respectively if pattern = '' + || mb_strpos($text, $pattern) !== mb_strrpos($text, $pattern) + ) + && $pattern !== $previousPattern // avoid infinte loop + && mb_strlen($pattern) < Match_MaxBits - $this->Patch_Margin - $this->Patch_Margin ) { + $padding += $this->Patch_Margin; + $previousPattern = $pattern; + $pattern = mb_substr($text, max($patch->start2 - $padding,0), ($patch->start2 + $patch->length1 + $padding) - max($patch->start2 - $padding,0) ); + } + // Add one chunk for good luck. + $padding += $this->Patch_Margin; + // Add the prefix. + $prefix = mb_substr($text, max($patch->start2 - $padding,0), $patch->start2 - max($patch->start2 - $padding,0) ); + if ($prefix!=='') { + array_unshift($patch->diffs, array ( + DIFF_EQUAL, + $prefix + )); + } + // Add the suffix. + $suffix = mb_substr($text, $patch->start2 + $patch->length1, ($patch->start2 + $patch->length1 + $padding) - ($patch->start2 + $patch->length1) ); + if ($suffix!=='') { + array_push($patch->diffs, array ( + DIFF_EQUAL, + $suffix + )); + } + + // Roll back the start points. + $patch->start1 -= mb_strlen($prefix); + $patch->start2 -= mb_strlen($prefix); + // Extend the lengths. + $patch->length1 += mb_strlen($prefix) + mb_strlen($suffix); + $patch->length2 += mb_strlen($prefix) + mb_strlen($suffix); + } + + /** + * Compute a list of patches to turn text1 into text2. + * Use diffs if provided, otherwise compute it ourselves. + * There are four ways to call this function, depending on what data is + * available to the caller: + * Method 1: + * a = text1, b = text2 + * Method 2: + * a = diffs + * Method 3 (optimal): + * a = text1, b = diffs + * Method 4 (deprecated, use method 3): + * a = text1, b = text2, c = diffs + * + * @param {string|Array.>} a text1 (methods 1,3,4) or + * Array of diff tuples for text1 to text2 (method 2). + * @param {string|Array.>} opt_b text2 (methods 1,4) or + * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). + * @param {string|Array.>} opt_c Array of diff tuples for + * text1 to text2 (method 4) or undefined (methods 1,2,3). + * @return {Array.} Array of patch objects. + */ + function patch_make($a, $opt_b = null, $opt_c = null ) { + if (is_string($a) && is_string($opt_b) && $opt_c === null ) { + // Method 1: text1, text2 + // Compute diffs from text1 and text2. + $text1 = $a; + $diffs = $this->diff_main($text1, $opt_b, true); + if ( count($diffs) > 2) { + $this->diff_cleanupSemantic($diffs); + $this->diff_cleanupEfficiency($diffs); + } + } elseif ( is_array($a) && $opt_b === null && $opt_c === null) { + // Method 2: diffs + // Compute text1 from diffs. + $diffs = $a; + $text1 = $this->diff_text1($diffs); + } elseif ( is_string($a) && is_array($opt_b) && $opt_c === null) { + // Method 3: text1, diffs + $text1 = $a; + $diffs = $opt_b; + } elseif ( is_string($a) && is_string($opt_b) && is_array($opt_c) ) { + // Method 4: text1, text2, diffs + // text2 is not used. + $text1 = $a; + $diffs = $opt_c; + } else { + echo_Exception('Unknown call format to patch_make.'); + } + + if ( count($diffs) === 0) { + return array(); // Get rid of the null case. + } + $patches = array(); + $patch = new patch_obj(); + $patchDiffLength = 0; // Keeping our own length var is faster in JS. + $char_count1 = 0; // Number of characters into the text1 string. + $char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + $prepatch_text = $text1; + $postpatch_text = $text1; + for ($x = 0; $x < count($diffs); $x++) { + $diff_type = $diffs[$x][0]; + $diff_text = $diffs[$x][1]; + + if (!$patchDiffLength && $diff_type !== DIFF_EQUAL) { + // A new patch starts here. + $patch->start1 = $char_count1; + $patch->start2 = $char_count2; + } + + switch ($diff_type) { + case DIFF_INSERT : + $patch->diffs[$patchDiffLength++] = $diffs[$x]; + $patch->length2 += mb_strlen($diff_text); + $postpatch_text = mb_substr($postpatch_text, 0, $char_count2) . $diff_text . mb_substr($postpatch_text,$char_count2); + break; + case DIFF_DELETE : + $patch->length1 += mb_strlen($diff_text); + $patch->diffs[$patchDiffLength++] = $diffs[$x]; + $postpatch_text = mb_substr($postpatch_text, 0, $char_count2) . mb_substr($postpatch_text, $char_count2 + mb_strlen($diff_text) ); + break; + case DIFF_EQUAL : + if ( mb_strlen($diff_text) <= 2 * $this->Patch_Margin && $patchDiffLength && count($diffs) != $x + 1) { + // Small equality inside a patch. + $patch->diffs[$patchDiffLength++] = $diffs[$x]; + $patch->length1 += mb_strlen($diff_text); + $patch->length2 += mb_strlen($diff_text); + } elseif ( mb_strlen($diff_text) >= 2 * $this->Patch_Margin ) { + // Time for a new patch. + if ($patchDiffLength) { + $this->patch_addContext($patch, $prepatch_text); + array_push($patches,$patch); + $patch = new patch_obj(); + $patchDiffLength = 0; + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + $prepatch_text = $postpatch_text; + $char_count1 = $char_count2; + } + } + break; + } + + // Update the current character count. + if ($diff_type !== DIFF_INSERT) { + $char_count1 += mb_strlen($diff_text); + } + if ($diff_type !== DIFF_DELETE) { + $char_count2 += mb_strlen($diff_text); + } + } + // Pick up the leftover patch if not empty. + if ($patchDiffLength) { + $this->patch_addContext($patch, $prepatch_text); + array_push($patches, $patch); + } + + return $patches; + } + + /** + * Given an array of patches, return another array that is identical. + * @param {Array.} patches Array of patch objects. + * @return {Array.} Array of patch objects. + */ + function patch_deepCopy($patches) { + // Making deep copies is hard in JavaScript. + $patchesCopy = array(); + for ($x = 0; $x < count($patches); $x++) { + $patch = $patches[$x]; + $patchCopy = new patch_obj(); + for ($y = 0; $y < count($patch->diffs); $y++) { + $patchCopy->diffs[$y] = $patch->diffs[$y]; // ?? . slice(); + } + $patchCopy->start1 = $patch->start1; + $patchCopy->start2 = $patch->start2; + $patchCopy->length1 = $patch->length1; + $patchCopy->length2 = $patch->length2; + $patchesCopy[$x] = $patchCopy; + } + return $patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as a list of true/false values indicating which patches were applied. + * @param {Array.} patches Array of patch objects. + * @param {string} text Old text. + * @return {Array.>} Two element Array, containing the + * new text and an array of boolean values. + */ + function patch_apply($patches, $text) { + if ( count($patches) == 0) { + return array($text,array()); + } + + // Deep copy the patches so that no changes are made to originals. + $patches = $this->patch_deepCopy($patches); + + $nullPadding = $this->patch_addPadding($patches); + $text = $nullPadding . $text . $nullPadding; + + $this->patch_splitMax($patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + $delta = 0; + $results = array(); + for ($x = 0; $x < count($patches) ; $x++) { + $expected_loc = $patches[$x]->start2 + $delta; + $text1 = $this->diff_text1($patches[$x]->diffs); + $start_loc = null; + $end_loc = -1; + if (mb_strlen($text1) > Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + $start_loc = $this->match_main($text, mb_substr($text1, 0, Match_MaxBits ), $expected_loc); + if ($start_loc != -1) { + $end_loc = $this->match_main($text, mb_substr($text1,mb_strlen($text1) - Match_MaxBits), $expected_loc + mb_strlen($text1) - Match_MaxBits); + if ($end_loc == -1 || $start_loc >= $end_loc) { + // Can't find valid trailing context. Drop this patch. + $start_loc = -1; + } + } + } else { + $start_loc = $this->match_main($text, $text1, $expected_loc); + } + if ($start_loc == -1) { + // No match found. :( + $results[$x] = false; + // Subtract the delta for this failed patch from subsequent patches. + $delta -= $patches[$x]->length2 - $patches[$x]->length1; + } else { + // Found a match. :) + $results[$x] = true; + $delta = $start_loc - $expected_loc; + $text2 = null; + if ($end_loc == -1) { + $text2 = mb_substr($text, $start_loc, mb_strlen($text1) ); + } else { + $text2 = mb_substr($text, $start_loc, $end_loc + Match_MaxBits - $start_loc); + } + if ($text1 == $text2) { + // Perfect match, just shove the replacement text in. + $text = mb_substr($text, 0, $start_loc) . $this->diff_text2($patches[$x]->diffs) . mb_substr($text,$start_loc + mb_strlen($text1) ); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + $diffs = $this->diff_main($text1, $text2, false); + if ( mb_strlen($text1) > Match_MaxBits && $this->diff_levenshtein($diffs) / mb_strlen($text1) > $this->Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + $results[$x] = false; + } else { + $this->diff_cleanupSemanticLossless($diffs); + $index1 = 0; + $index2 = NULL; + for ($y = 0; $y < count($patches[$x]->diffs); $y++) { + $mod = $patches[$x]->diffs[$y]; + if ($mod[0] !== DIFF_EQUAL) { + $index2 = $this->diff_xIndex($diffs, $index1); + } + if ($mod[0] === DIFF_INSERT) { // Insertion + $text = mb_substr($text, 0, $start_loc + $index2) . $mod[1] . mb_substr($text, $start_loc + $index2); + } elseif ($mod[0] === DIFF_DELETE) { // Deletion + $text = mb_substr($text, 0, $start_loc + $index2) . mb_substr($text,$start_loc + $this->diff_xIndex($diffs, $index1 + mb_strlen($mod[1]) )); + } + if ($mod[0] !== DIFF_DELETE) { + $index1 += mb_strlen($mod[1]); + } + } + } + } + } + } + // Strip the padding off. + $text = mb_substr($text, mb_strlen($nullPadding), mb_strlen($text) - 2*mb_strlen($nullPadding) ); + return array($text, $results); + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param {Array.} patches Array of patch objects. + * @return {string} The padding string added to each side. + */ + function patch_addPadding(&$patches){ + $paddingLength = $this->Patch_Margin; + $nullPadding = ''; + for ($x = 1; $x <= $paddingLength; $x++) { + $nullPadding .= mb_chr($x); + } + + // Bump all the patches forward. + for ($x = 0; $x < count($patches); $x++) { + $patches[$x]->start1 += $paddingLength; + $patches[$x]->start2 += $paddingLength; + } + + // Add some padding on start of first diff. + $patch = &$patches[0]; + $diffs = &$patch->diffs; + if (count($diffs) == 0 || $diffs[0][0] != DIFF_EQUAL) { + // Add nullPadding equality. + array_unshift($diffs, array(DIFF_EQUAL, $nullPadding)); + $patch->start1 -= $paddingLength; // Should be 0. + $patch->start2 -= $paddingLength; // Should be 0. + $patch->length1 += $paddingLength; + $patch->length2 += $paddingLength; + } elseif ($paddingLength > mb_strlen($diffs[0][1]) ) { + // Grow first equality. + $extraLength = $paddingLength - mb_strlen($diffs[0][1]); + $diffs[0][1] = mb_substr( $nullPadding , mb_strlen($diffs[0][1]) ) . $diffs[0][1]; + $patch->start1 -= $extraLength; + $patch->start2 -= $extraLength; + $patch->length1 += $extraLength; + $patch->length2 += $extraLength; + } + + // Add some padding on end of last diff. + $patch = &$patches[count($patches) - 1]; + $diffs = &$patch->diffs; + if ( count($diffs) == 0 || $diffs[ count($diffs) - 1][0] != DIFF_EQUAL) { + // Add nullPadding equality. + array_push($diffs, array(DIFF_EQUAL, $nullPadding) ); + $patch->length1 += $paddingLength; + $patch->length2 += $paddingLength; + } elseif ($paddingLength > mb_strlen( $diffs[count($diffs)-1][1] ) ) { + // Grow last equality. + $extraLength = $paddingLength - mb_strlen( $diffs[count($diffs)-1][1] ); + $diffs[ count($diffs)-1][1] .= mb_substr($nullPadding,0,$extraLength); + $patch->length1 += $extraLength; + $patch->length2 += $extraLength; + } + + return $nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the maximum + * limit of the match algorithm. + * @param {Array.} patches Array of patch objects. + */ + function patch_splitMax(&$patches) { + for ($x = 0; $x < count($patches); $x++) { + if ( $patches[$x]->length1 > Match_MaxBits) { + $bigpatch = $patches[$x]; + // Remove the big old patch. + array_splice($patches,$x--,1); + $patch_size = Match_MaxBits; + $start1 = $bigpatch->start1; + $start2 = $bigpatch->start2; + $precontext = ''; + while ( count($bigpatch->diffs) !== 0) { + // Create one of several smaller patches. + $patch = new patch_obj(); + $empty = true; + $patch->start1 = $start1 - mb_strlen($precontext); + $patch->start2 = $start2 - mb_strlen($precontext); + if ($precontext !== '') { + $patch->length1 = $patch->length2 = mb_strlen($precontext); + array_push($patch->diffs, array(DIFF_EQUAL, $precontext) ); + } + while ( count($bigpatch->diffs) !== 0 && $patch->length1 < $patch_size - $this->Patch_Margin) { + $diff_type = $bigpatch->diffs[0][0]; + $diff_text = $bigpatch->diffs[0][1]; + if ($diff_type === DIFF_INSERT) { + // Insertions are harmless. + $patch->length2 += mb_strlen($diff_text); + $start2 += mb_strlen($diff_text); + array_push($patch->diffs, array_shift($bigpatch->diffs) ); + $empty = false; + } else + if ($diff_type === DIFF_DELETE && count($patch->diffs) == 1 && $patch->diffs[0][0] == DIFF_EQUAL && (mb_strlen($diff_text) > 2 * $patch_size) ) { + // This is a large deletion. Let it pass in one chunk. + $patch->length1 += mb_strlen($diff_text); + $start1 += mb_strlen($diff_text); + $empty = false; + array_push( $patch->diffs, array($diff_type, $diff_text) ); + array_shift($bigpatch->diffs); + } else { + // Deletion or equality. Only take as much as we can stomach. + $diff_text = mb_substr($diff_text, 0, $patch_size - $patch->length1 - $this->Patch_Margin); + $patch->length1 += mb_strlen($diff_text); + $start1 += mb_strlen($diff_text); + if ($diff_type === DIFF_EQUAL) { + $patch->length2 += mb_strlen($diff_text); + $start2 += mb_strlen($diff_text); + } else { + $empty = false; + } + array_push($patch->diffs, array($diff_type, $diff_text) ); + if ($diff_text == $bigpatch->diffs[0][1]) { + array_shift($bigpatch->diffs); + } else { + $bigpatch->diffs[0][1] = mb_substr( $bigpatch->diffs[0][1],mb_strlen($diff_text) ); + } + } + } + // Compute the head context for the next patch. + $precontext = $this->diff_text2($patch->diffs); + $precontext = mb_substr($precontext, mb_strlen($precontext)-$this->Patch_Margin); + // Append the end context for this patch. + $postcontext = mb_substr( $this->diff_text1($bigpatch->diffs), 0, $this->Patch_Margin ); + if ($postcontext !== '') { + $patch->length1 += mb_strlen($postcontext); + $patch->length2 += mb_strlen($postcontext); + if ( count($patch->diffs) !== 0 && $patch->diffs[ count($patch->diffs) - 1][0] === DIFF_EQUAL) { + $patch->diffs[ count($patch->diffs) - 1][1] .= $postcontext; + } else { + array_push($patch->diffs, array(DIFF_EQUAL, $postcontext)); + } + } + if (!$empty) { + array_splice($patches, ++$x, 0, array($patch)); + } + } + } + } + } + + /** + * Take a list of patches and return a textual representation. + * @param {Array.} patches Array of patch objects. + * @return {string} Text representation of patches. + */ + function patch_toText($patches) { + $text = array(); + for ($x = 0; $x < count($patches) ; $x++) { + $text[$x] = $patches[$x]; + } + return implode('',$text); + } + + /** + * Parse a textual representation of patches and return a list of patch objects. + * @param {string} textline Text representation of patches. + * @return {Array.} Array of patch objects. + * @throws {Error} If invalid input. + */ + function patch_fromText($textline) { + $patches = array(); + if ($textline === '') { + return $patches; + } + $text = explode("\n",$textline); + foreach($text as $i=>$t){ if($t===''){ unset($text[$i]); } } + $textPointer = 0; + while ($textPointer < count($text) ) { + $m = null; + preg_match('/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/',$text[$textPointer],$m); + if (!$m) { + echo_Exception('Invalid patch string: ' . $text[$textPointer]); + } + $patch = new patch_obj(); + array_push($patches, $patch); + @$patch->start1 = (int)$m[1]; + if (@$m[2] === '') { + $patch->start1--; + $patch->length1 = 1; + } elseif ( @$m[2] == '0') { + $patch->length1 = 0; + } else { + $patch->start1--; + @$patch->length1 = (int)$m[2]; + } + + @$patch->start2 = (int)$m[3]; + if (@$m[4] === '') { + $patch->start2--; + $patch->length2 = 1; + } elseif ( @$m[4] == '0') { + $patch->length2 = 0; + } else { + $patch->start2--; + @$patch->length2 = (int)$m[4]; + } + $textPointer++; + + while ($textPointer < count($text) ) { + $sign = $text[$textPointer][0]; + try { + $line = decodeURI( mb_substr($text[$textPointer],1) ); + } catch (Exception $ex) { + // Malformed URI sequence. + throw new Exception('Illegal escape in patch_fromText: ' . $line); + } + if ($sign == '-') { + // Deletion. + array_push( $patch->diffs, array(DIFF_DELETE, $line) ); + } elseif ($sign == '+') { + // Insertion. + array_push($patch->diffs, array(DIFF_INSERT, $line) ); + } elseif ($sign == ' ') { + // Minor equality. + array_push($patch->diffs, array(DIFF_EQUAL, $line) ); + } elseif ($sign == '@') { + // Start of next patch. + break; + } elseif ($sign === '') { + // Blank line? Whatever. + } else { + // WTF? + echo_Exception('Invalid patch mode "' . $sign . '" in: ' . $line); + } + $textPointer++; + } + } + return $patches; + } +} + +/** + * Class representing one patch operation. + * @constructor + */ +class patch_obj { + /** @type {Array.>} */ + public $diffs = array(); + /** @type {number?} */ + public $start1 = null; + /** @type {number?} */ + public $start2 = null; + /** @type {number} */ + public $length1 = 0; + /** @type {number} */ + public $length2 = 0; + + /** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return {string} The GNU diff string. + */ + function toString() { + if ($this->length1 === 0) { + $coords1 = $this->start1 . ',0'; + } elseif ($this->length1 == 1) { + $coords1 = $this->start1 + 1; + } else { + $coords1 = ($this->start1 + 1) . ',' . $this->length1; + } + if ($this->length2 === 0) { + $coords2 = $this->start2 . ',0'; + } elseif ($this->length2 == 1) { + $coords2 = $this->start2 + 1; + } else { + $coords2 = ($this->start2 + 1) . ',' . $this->length2; + } + $text = array ( '@@ -' . $coords1 . ' +' . $coords2 . " @@\n" ); + + // Escape the body of the patch with %xx notation. + for ($x = 0; $x < count($this->diffs); $x++) { + switch ($this->diffs[$x][0]) { + case DIFF_INSERT : + $op = '+'; + break; + case DIFF_DELETE : + $op = '-'; + break; + case DIFF_EQUAL : + $op = ' '; + break; + } + $text[$x +1] = $op . encodeURI($this->diffs[$x][1]) . "\n"; + } + return str_replace('%20', ' ', implode('',$text)); + } + function __toString(){ + return $this->toString(); + } +} + +define('DIFF_DELETE', -1); +define('DIFF_INSERT', 1); +define('DIFF_EQUAL', 0); + +define('Match_MaxBits', PHP_INT_SIZE * 8); + + +function charCodeAt($str, $pos) { + return mb_ord(mb_substr($str, $pos, 1)); +} +function mb_ord($v) { + $k = mb_convert_encoding($v, 'UCS-2LE', 'UTF-8'); + $k1 = ord(substr($k, 0, 1)); + $k2 = ord(substr($k, 1, 1)); + return $k2 * 256 + $k1; +} +function mb_chr($num){ + return mb_convert_encoding('&#'.intval($num).';', 'UTF-8', 'HTML-ENTITIES'); +} + +/** + * as in javascript encodeURI() following the MDN description + * + * @link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI + * @param $url + * @return string + */ +function encodeURI($url) { + return strtr(rawurlencode($url), array ( + '%3B' => ';', '%2C' => ',', '%2F' => '/', '%3F' => '?', '%3A' => ':', '%40' => '@', '%26' => '&', '%3D' => '=', + '%2B' => '+', '%24' => '$', '%21' => '!', '%2A' => '*', '%27' => '\'', '%28' => '(', '%29' => ')', '%23' => '#', + )); +} + +function decodeURI($encoded) { + static $dontDecode; + if (!$dontDecode) { + $table = array ( + '%3B' => ';', '%2C' => ',', '%2F' => '/', '%3F' => '?', '%3A' => ':', '%40' => '@', '%26' => '&', '%3D' => '=', + '%2B' => '+', '%24' => '$', '%21' => '!', '%2A' => '*', '%27' => '\'', '%28' => '(', '%29' => ')', '%23' => '#', + ); + $dontDecode = array(); + foreach ($table as $k => $v) { + $dontDecode[$k] = encodeURI($k); + } + } + return rawurldecode(strtr($encoded, $dontDecode)); +} + +function echo_Exception($str){ + global $lastException; + $lastException = $str; + echo $str; +} +//mb_internal_encoding("UTF-8"); + +?> \ No newline at end of file diff --git a/externals/wepay/iframe_demoapp/checkout.php b/externals/wepay/iframe_demoapp/checkout.php old mode 100755 new mode 100644 diff --git a/externals/wepay/iframe_demoapp/list_accounts.php b/externals/wepay/iframe_demoapp/list_accounts.php old mode 100755 new mode 100644 diff --git a/externals/wepay/wepay.php b/externals/wepay/wepay.php old mode 100755 new mode 100644 diff --git a/externals/wordlist/LICENSE.txt b/externals/wordlist/LICENSE.txt new file mode 100644 index 0000000000..26d7b1666b --- /dev/null +++ b/externals/wordlist/LICENSE.txt @@ -0,0 +1,15 @@ +The following copyright statement applies to this wordlists collection as a whole: +Copyright (c) 2002,2003 by Solar Designer of Openwall Project + +The homepage URL for this wordlists collection is: + +http://www.openwall.com/wordlists/ + +You're allowed to use and redistribute this wordlists collection or parts thereof, with or without modification, provided that credit is given where it is due, any modified versions are marked as such, this license is kept intact and included with each copy, and NO FEE IS CHARGED FOR OBTAINING A COPY except as negotiated with the copyright holder. In particular, you are NOT permitted to charge for bandwidth, physical media, and/or shipping. You're also not permitted to bundle this wordlists collection with a product you charge for. + +If redistribution for a fee is what you're after, please contact the copyright holder to negotiate special terms for the downloadable or the extended CD-ready version of this collection. + +It was a significant amount of work to compile this collection and having a monopoly on regulating the CD sales is my way to compensate for the time already spent and to allow for further work. + +-- +Alexander Peslyak aka Solar Designer \ No newline at end of file diff --git a/externals/wordlist/password.lst b/externals/wordlist/password.lst new file mode 100644 index 0000000000..3c75bf2e05 --- /dev/null +++ b/externals/wordlist/password.lst @@ -0,0 +1,3557 @@ +#!comment: This list has been compiled by Solar Designer of Openwall Project, +#!comment: http://www.openwall.com/wordlists/ +#!comment: +#!comment: This list is based on passwords most commonly seen on a set of Unix +#!comment: systems in mid-1990's, sorted for decreasing number of occurrences +#!comment: (that is, more common passwords are listed first). It has been +#!comment: revised to also include common website passwords from public lists +#!comment: of "top N passwords" from major community website compromises that +#!comment: occurred in 2006 through 2010. +#!comment: +#!comment: Last update: 2011/11/20 (3546 entries) +123456 +12345 +password +password1 +123456789 +12345678 +1234567890 +abc123 +computer +tigger +1234 +qwerty +money +carmen +mickey +secret +summer +internet +a1b2c3 +123 +service + +canada +hello +ranger +shadow +baseball +donald +harley +hockey +letmein +maggie +mike +mustang +snoopy +buster +dragon +jordan +michael +michelle +mindy +patrick +123abc +andrew +bear +calvin +changeme +diamond +fuckme +fuckyou +matthew +miller +tiger +trustno1 +alex +apple +avalon +brandy +chelsea +coffee +falcon +freedom +gandalf +green +helpme +linda +magic +merlin +newyork +soccer +thomas +wizard +asdfgh +bandit +batman +boris +butthead +dorothy +eeyore +fishing +football +george +happy +iloveyou +jennifer +jonathan +love +marina +master +missy +monday +monkey +natasha +ncc1701 +pamela +pepper +piglet +poohbear +pookie +rabbit +rachel +rocket +rose +smile +sparky +spring +steven +success +sunshine +victoria +whatever +zapata +8675309 +amanda +andy +angel +august +barney +biteme +boomer +brian +casey +cowboy +delta +doctor +fisher +island +john +joshua +karen +marley +orange +please +rascal +richard +sarah +scooter +shalom +silver +skippy +stanley +taylor +welcome +zephyr +111111 +aaaaaa +access +albert +alexander +andrea +anna +anthony +asdfjkl; +ashley +basketball +beavis +black +bob +booboo +bradley +brandon +buddy +caitlin +camaro +charlie +chicken +chris +cindy +cricket +dakota +dallas +daniel +david +debbie +dolphin +elephant +emily +friend +fucker +ginger +goodluck +hammer +heather +iceman +jason +jessica +jesus +joseph +jupiter +justin +kevin +knight +lacrosse +lakers +lizard +madison +mary +mother +muffin +murphy +nirvana +paris +pentium +phoenix +picture +rainbow +sandy +saturn +scott +shannon +shithead +skeeter +sophie +special +stephanie +stephen +steve +sweetie +teacher +tennis +test +test123 +tommy +topgun +tristan +wally +william +wilson +1q2w3e +654321 +666666 +a12345 +a1b2c3d4 +alpha +amber +angela +angie +archie +asdf +blazer +bond007 +booger +charles +christin +claire +control +danny +david1 +dennis +digital +disney +edward +elvis +felix +flipper +franklin +frodo +honda +horses +hunter +indigo +james +jasper +jeremy +julian +kelsey +killer +lauren +marie +maryjane +matrix +maverick +mayday +mercury +mitchell +morgan +mountain +niners +nothing +oliver +peace +peanut +pearljam +phantom +popcorn +princess +psycho +pumpkin +purple +randy +rebecca +reddog +robert +rocky +roses +salmon +samson +sharon +sierra +smokey +startrek +steelers +stimpy +sunflower +superman +support +sydney +techno +walter +willie +willow +winner +ziggy +zxcvbnm +alaska +alexis +alice +animal +apples +barbara +benjamin +billy +blue +bluebird +bobby +bonnie +bubba +camera +chocolate +clark +claudia +cocacola +compton +connect +cookie +cruise +douglas +dreamer +dreams +duckie +eagles +eddie +einstein +enter +explorer +faith +family +ferrari +flamingo +flower +foxtrot +francis +freddy +friday +froggy +giants +gizmo +global +goofy +happy1 +hendrix +henry +herman +homer +honey +house +houston +iguana +indiana +insane +inside +irish +ironman +jake +jasmin +jeanne +jerry +joey +justice +katherine +kermit +kitty +koala +larry +leslie +logan +lucky +mark +martin +matt +minnie +misty +mitch +mouse +nancy +nascar +nelson +pantera +parker +penguin +peter +piano +pizza +prince +punkin +pyramid +raymond +robin +roger +rosebud +route66 +royal +running +sadie +sasha +security +sheena +sheila +skiing +snapple +snowball +sparrow +spencer +spike +star +stealth +student +sunny +sylvia +tamara +taurus +teresa +theresa +thunderbird +tigers +tony +toyota +travel +tuesday +victory +viper1 +wesley +whisky +winnie +winter +wolves +xyz123 +zorro +123123 +1234567 +696969 +888888 +Anthony +Joshua +Matthew +Tigger +aaron +abby +abcdef +adidas +adrian +alfred +arthur +athena +austin +awesome +badger +bamboo +beagle +bears +beatles +beautiful +beaver +benny +bigmac +bingo +bitch +blonde +boogie +boston +brenda +bright +bubba1 +bubbles +buffy +button +buttons +cactus +candy +captain +carlos +caroline +carrie +casper +catch22 +chance +charity +charlotte +cheese +cheryl +chloe +chris1 +clancy +compaq +conrad +cooper +cooter +copper +cosmos +cougar +cracker +crawford +crystal +curtis +cyclone +dance +diablo +dollars +dookie +dumbass +dundee +elizabeth +eric +europe +farmer +firebird +fletcher +fluffy +france +freak1 +friends +fuckoff +gabriel +galaxy +gambit +garden +garfield +garnet +genesis +genius +godzilla +golfer +goober +grace +greenday +groovy +grover +guitar +hacker +harry +hazel +hector +herbert +horizon +hornet +howard +icecream +imagine +impala +jack +janice +jasmine +jason1 +jeanette +jeffrey +jenifer +jenni +jesus1 +jewels +joker +julie +julie1 +junior +justin1 +kathleen +keith +kelly +kelly1 +kennedy +kevin1 +knicks +larry1 +leonard +lestat +library +lincoln +lionking +london +louise +lucky1 +lucy +maddog +margaret +mariposa +marlboro +martin1 +marty +master1 +mensuck +mercedes +metal +midori +mikey +millie +mirage +molly +monet +money1 +monica +monopoly +mookie +moose +moroni +music +naomi +nathan +nguyen +nicholas +nicole +nimrod +october +olive +olivia +online +oscar +oxford +pacific +painter +peaches +penelope +pepsi +petunia +philip +phoenix1 +photo +pickle +player +poiuyt +porsche +porter +puppy +python +quality +raquel +raven +remember +robbie +robert1 +roman +rugby +runner +russell +ryan +sailing +sailor +samantha +savage +scarlett +school +sean +seven +shadow1 +sheba +shelby +shit +shoes +simba +simple +skipper +smiley +snake +snickers +sniper +snoopdog +snowman +sonic +spitfire +sprite +spunky +starwars +station +stella +stingray +storm +stormy +stupid +sunny1 +sunrise +surfer +susan +tammy +tango +tanya +teddy1 +theboss +theking +thumper +tina +tintin +tomcat +trebor +trevor +tweety +unicorn +valentine +valerie +vanilla +veronica +victor +vincent +viper +warrior +warriors +weasel +wheels +wilbur +winston +wisdom +wombat +xavier +yellow +zeppelin +1111 +1212 +Andrew +Family +Friends +Michael +Michelle +Snoopy +abcd1234 +abcdefg +abigail +account +adam +alex1 +alice1 +allison +alpine +andre1 +andrea1 +angel1 +anita +annette +antares +apache +apollo +aragorn +arizona +arnold +arsenal +asdfasdf +asdfg +asdfghjk +avenger +baby +babydoll +bailey +banana +barry +basket +batman1 +beaner +beast +beatrice +bella +bertha +bigben +bigdog +biggles +bigman +binky +biology +bishop +blondie +bluefish +bobcat +bosco +braves +brazil +bruce +bruno +brutus +buffalo +bulldog +bullet +bullshit +bunny +business +butch +butler +butter +california +carebear +carol +carol1 +carole +cassie +castle +catalina +catherine +cccccc +celine +center +champion +chanel +chaos +chelsea1 +chester1 +chicago +chico +christian +christy +church +cinder +colleen +colorado +columbia +commander +connie +cookies +cooking +corona +cowboys +coyote +craig +creative +cuddles +cuervo +cutie +daddy +daisy +daniel1 +danielle +davids +death +denis +derek +design +destiny +diana +diane +dickhead +digger +dodger +donna +dougie +dragonfly +dylan +eagle +eclipse +electric +emerald +etoile +excalibur +express +fender +fiona +fireman +flash +florida +flowers +foster +francesco +francine +francois +frank +french +fuckface +gemini +general +gerald +germany +gilbert +goaway +golden +goldfish +goose +gordon +graham +grant +gregory +gretchen +gunner +hannah +harold +harrison +harvey +hawkeye +heaven +heidi +helen +helena +hithere +hobbit +ibanez +idontknow +integra +ireland +irene +isaac +isabel +jackass +jackie +jackson +jaguar +jamaica +japan +jenny1 +jessie +johan +johnny +joker1 +jordan23 +judith +julia +jumanji +kangaroo +karen1 +kathy +keepout +keith1 +kenneth +kimberly +kingdom +kitkat +kramer +kristen +laura +laurie +lawrence +lawyer +legend +liberty +light +lindsay +lindsey +lisa +liverpool +lola +lonely +louis +lovely +loveme +lucas +madonna +malcolm +malibu +marathon +marcel +maria1 +mariah +mariah1 +marilyn +mario +marvin +maurice +maxine +maxwell +me +meggie +melanie +melissa +melody +mexico +michael1 +michele +midnight +mike1 +miracle +misha +mishka +molly1 +monique +montreal +moocow +moore +morris +mouse1 +mulder +nautica +nellie +newton +nick +nirvana1 +nissan +norman +notebook +ocean +olivier +ollie +oranges +oregon +orion +panda +pandora +panther +passion +patricia +pearl +peewee +pencil +penny +people +percy +person +peter1 +petey +picasso +pierre +pinkfloyd +polaris +police +pookie1 +poppy +power +predator +preston +q1w2e3 +queen +queenie +quentin +ralph +random +rangers +raptor +reality +redrum +remote +reynolds +rhonda +ricardo +ricardo1 +ricky +river +roadrunner +robinhood +rocknroll +rocky1 +ronald +roxy +ruthie +sabrina +sakura +sally +sampson +samuel +sandra +santa +sapphire +scarlet +scorpio +scott1 +scottie +scruffy +seattle +serena +shanti +shark +shogun +simon +singer +skull +skywalker +slacker +smashing +smiles +snowflake +snuffy +soccer1 +soleil +sonny +spanky +speedy +spider +spooky +stacey +star69 +start +steven1 +stinky +strawberry +stuart +sugar +sundance +superfly +suzanne +suzuki +swimmer +swimming +system +taffy +tarzan +teddy +teddybear +terry +theatre +thunder +thursday +tinker +tootsie +tornado +tracy +tricia +trident +trojan +truman +trumpet +tucker +turtle +tyler +utopia +voyager +warcraft +warlock +warren +water +wayne +wendy +williams +willy +winona +woody +woofwoof +wrangler +wright +xfiles +xxxxxx +yankees +yvonne +zebra +zenith +zigzag +zombie +zxc123 +zxcvb +000000 +007007 +11111 +11111111 +123321 +171717 +181818 +1a2b3c +1chris +4runner +54321 +55555 +6969 +7777777 +789456 +88888888 +Alexis +Bailey +Charlie +Chris +Daniel +Dragon +Elizabeth +HARLEY +Heather +Jennifer +Jessica +Jordan +KILLER +Nicholas +Password +Princess +Purple +Rebecca +Robert +Shadow +Steven +Summer +Sunshine +Superman +Taylor +Thomas +Victoria +abcd123 +abcde +accord +active +africa +airborne +alfaro +alicia +aliens +alina +aline +alison +allen +aloha +alpha1 +althea +altima +amanda1 +amazing +america +amour +anderson +andre +andrew1 +andromeda +angels +angie1 +annie +anything +apple1 +apple2 +applepie +april +aquarius +ariane +ariel +arlene +artemis +asdf1234 +asdfjkl +ashley1 +ashraf +ashton +asterix +attila +autumn +avatar +babes +bambi +barbie +barney1 +barrett +bball +beaches +beanie +beans +beauty +becca +belize +belle +belmont +benji +benson +bernardo +berry +betsy +betty +bigboss +bigred +billy1 +birdie +birthday +biscuit +bitter +blackjack +blah +blanche +blood +blowjob +blowme +blueeyes +blues +bogart +bombay +boobie +boots +bootsie +boxers +brandi +brent +brewster +bridge +bronco +bronte +brooke +brother +bryan +bubble +buddha +budgie +burton +butterfly +byron +calendar +calvin1 +camel +camille +campbell +camping +cancer +canela +cannon +carbon +carnage +carolyn +carrot +cascade +catfish +cathy +catwoman +cecile +celica +change +chantal +charger +cherry +chiara +chiefs +china +chris123 +christ1 +christmas +christopher +chuck +cindy1 +cinema +civic +claude +clueless +cobain +cobra +cody +colette +college +colors +colt45 +confused +cool +corvette +cosmo +country +crusader +cunningham +cupcake +cynthia +dagger +dammit +dancer +daphne +darkstar +darren +darryl +darwin +deborah +december +deedee +deeznuts +delano +delete +demon +denise +denny +desert +deskjet +detroit +devil +devine +devon +dexter +dianne +diesel +director +dixie +dodgers +doggy +dollar +dolly +dominique +domino +dontknow +doogie +doudou +downtown +dragon1 +driver +dude +dudley +dutchess +dwight +eagle1 +easter +eastern +edith +edmund +eight +element +elissa +ellen +elliot +empire +enigma +enterprise +erin +escort +estelle +eugene +evelyn +explore +family1 +fatboy +felipe +ferguson +ferret +ferris +fireball +fishes +fishie +flight +florida1 +flowerpot +forward +freddie +freebird +freeman +frisco +fritz +froggie +froggies +frogs +fucku +future +gabby +games +garcia +gaston +gateway +george1 +georgia +german +germany1 +getout +ghost +gibson +giselle +gmoney +goblin +goblue +gollum +grandma +gremlin +grizzly +grumpy +guess +guitar1 +gustavo +haggis +haha +hailey +halloween +hamilton +hamlet +hanna +hanson +happy123 +happyday +hardcore +harley1 +harriet +harris +harvard +health +heart +heather1 +heather2 +hedgehog +helene +hello1 +hello123 +hellohello +hermes +heythere +highland +hilda +hillary +history +hitler +hobbes +holiday +holly +honda1 +hongkong +hootie +horse +hotrod +hudson +hummer +huskies +idiot +iforget +iloveu +impact +indonesia +irina +isabelle +israel +italia +italy +jackie1 +jacob +jakey +james1 +jamesbond +jamie +jamjam +jeffrey1 +jennie +jenny +jensen +jesse +jesse1 +jester +jethro +jimbob +jimmy +joanna +joelle +john316 +jordie +jorge +josh +journey +joyce +jubilee +jules +julien +juliet +junebug +juniper +justdoit +karin +karine +karma +katerina +katie +katie1 +kayla +keeper +keller +kendall +kenny +ketchup +kings +kissme +kitten +kittycat +kkkkkk +kristi +kristine +labtec +laddie +ladybug +lance +laurel +lawson +leader +leland +lemon +lester +letter +letters +lexus1 +libra +lights +lionel +little +lizzy +lolita +lonestar +longhorn +looney +loren +lorna +loser +lovers +loveyou +lucia +lucifer +lucky14 +maddie +madmax +magic1 +magnum +maiden +maine +management +manson +manuel +marcus +maria +marielle +marine +marino +marshall +martha +maxmax +meatloaf +medical +megan +melina +memphis +mermaid +miami +michel +michigan +mickey1 +microsoft +mikael +milano +miles +millenium +million +miranda +miriam +mission +mmmmmm +mobile +monkey1 +monroe +montana +monty +moomoo +moonbeam +morpheus +motorola +movies +mozart +munchkin +murray +mustang1 +nadia +nadine +napoleon +nation +national +nestle +newlife +newyork1 +nichole +nikita +nikki +nintendo +nokia +nomore +normal +norton +noway +nugget +number9 +numbers +nurse +nutmeg +ohshit +oicu812 +omega +openup +orchid +oreo +orlando +packard +packers +paloma +pancake +panic +parola +parrot +partner +pascal +patches +patriots +paula +pauline +payton +peach +peanuts +pedro1 +peggy +perfect +perry +peterpan +philips +phillips +phone +pierce +pigeon +pink +pioneer +piper1 +pirate +pisces +playboy +pluto +poetry +pontiac +pookey +popeye +prayer +precious +prelude +premier +puddin +pulsar +pussy +pussy1 +qwert +qwerty12 +qwertyui +rabbit1 +rachelle +racoon +rambo +randy1 +ravens +redman +redskins +reggae +reggie +renee +renegade +rescue +revolution +richard1 +richards +richmond +riley +ripper +robby +roberts +rock +rocket1 +rockie +rockon +roger1 +rogers +roland +rommel +rookie +rootbeer +rosie +rufus +rusty +ruthless +sabbath +sabina +safety +saint +samiam +sammie +sammy +samsam +sandi +sanjose +saphire +sarah1 +saskia +sassy +saturday +science +scooby +scoobydoo +scooter1 +scorpion +scotty +scouts +search +september +server +seven7 +sexy +shaggy +shanny +shaolin +shasta +shayne +shelly +sherry +shirley +shorty +shotgun +sidney +simba1 +sinatra +sirius +skate +skipper1 +skyler +slayer +sleepy +slider +smile1 +smitty +smoke +snakes +snapper +snoop +solomon +sophia +space +sparks +spartan +spike1 +sponge +spurs +squash +stargate +starlight +stars +steph1 +steve1 +stevens +stewart +stone +stranger +stretch +strong +studio +stumpy +sucker +suckme +sultan +summit +sunfire +sunset +super +superstar +surfing +susan1 +sutton +sweden +sweetpea +sweety +swordfish +tabatha +tacobell +taiwan +tamtam +tanner +target +tasha +tattoo +tequila +terry1 +texas +thankyou +theend +thompson +thrasher +tiger2 +timber +timothy +tinkerbell +topcat +topher +toshiba +tototo +travis +treasure +trees +tricky +trish +triton +trombone +trouble +trucker +turbo +twins +tyler1 +ultimate +unique +united +ursula +vacation +valley +vampire +vanessa +venice +venus +vermont +vicki +vicky +victor1 +vincent1 +violet +violin +virgil +virginia +vision +volley +voodoo +vortex +waiting +wanker +warner +water1 +wayne1 +webster +weezer +wendy1 +western +white +whitney +whocares +wildcat +william1 +wilma +window +winniethepooh +wolfgang +wolverine +wonder +xxxxxxxx +yamaha +yankee +yogibear +yolanda +yomama +yvette +zachary +zebras +zxcvbn +00000000 +121212 +1234qwer +131313 +13579 +90210 +99999999 +ABC123 +action +amelie +anaconda +apollo13 +artist +asshole +benoit +bernard +bernie +bigbird +blizzard +bluesky +bonjour +caesar +cardinal +carolina +cesar +chandler +chapman +charlie1 +chevy +chiquita +chocolat +coco +cougars +courtney +dolphins +dominic +donkey +dusty +eminem +energy +fearless +forest +forever +glenn +guinness +hotdog +indian +jared +jimbo +johnson +jojo +josie +kristin +lloyd +lorraine +lynn +maxime +memory +mimi +mirror +nebraska +nemesis +network +nigel +oatmeal +patton +pedro +planet +players +portland +praise +psalms +qwaszx +raiders +rambo1 +rancid +shawn +shelley +softball +speedo +sports +ssssss +steele +steph +stephani +sunday +tiffany +tigre +toronto +trixie +undead +valentin +velvet +viking +walker +watson +young +babygirl +pretty +hottie +teamo +987654321 +naruto +spongebob +daniela +princesa +christ +blessed +single +qazwsx +pokemon +iloveyou1 +iloveyou2 +fuckyou1 +hahaha +poop +blessing +blahblah +blink182 +123qwe +trinity +passw0rd +google +looking +spirit +iloveyou! +qwerty1 +onelove +mylove +222222 +ilovegod +football1 +loving +emmanuel +1q2w3e4r +red123 +blabla +112233 +hallo +spiderman +simpsons +monster +november +brooklyn +poopoo +darkness +159753 +pineapple +chester +1qaz2wsx +drowssap +monkey12 +wordpass +q1w2e3r4 +coolness +11235813 +something +alexandra +estrella +miguel +iloveme +sayang +princess1 +555555 +999999 +alejandro +brittany +alejandra +tequiero +antonio +987654 +00000 +fernando +corazon +cristina +kisses +myspace +rebelde +babygurl +alyssa +mahalkita +gabriela +pictures +hellokitty +babygirl1 +angelica +mahalko +mariana +eduardo +andres +ronaldo +inuyasha +adriana +celtic +samsung +angelo +456789 +sebastian +karina +hotmail +0123456789 +barcelona +cameron +slipknot +cutiepie +50cent +bonita +maganda +babyboy +natalie +cuteako +javier +789456123 +123654 +bowwow +portugal +777777 +volleyball +january +cristian +bianca +chrisbrown +101010 +sweet +panget +benfica +love123 +lollipop +camila +qwertyuiop +harrypotter +ihateyou +christine +lorena +andreea +charmed +rafael +brianna +aaliyah +johncena +lovelove +gangsta +333333 +hiphop +mybaby +sergio +metallica +myspace1 +babyblue +badboy +fernanda +westlife +sasuke +steaua +roberto +slideshow +asdfghjkl +santiago +jayson +5201314 +jerome +gandako +gatita +babyko +246810 +sweetheart +chivas +alberto +valeria +nicole1 +12345678910 +leonardo +jayjay +liliana +sexygirl +232323 +amores +anthony1 +bitch1 +fatima +miamor +lover +lalala +252525 +skittles +colombia +159357 +manutd +123456a +britney +katrina +christina +pasaway +mahal +tatiana +cantik +0123456 +teiubesc +147258369 +natalia +francisco +amorcito +paola +angelito +manchester +mommy1 +147258 +amigos +marlon +linkinpark +147852 +diego +444444 +iverson +andrei +justine +frankie +pimpin +fashion +bestfriend +england +hermosa +456123 +102030 +sporting +hearts +potter +iloveu2 +number1 +212121 +truelove +jayden +savannah +hottie1 +ganda +scotland +ilovehim +shakira +estrellita +brandon1 +sweets +familia +love12 +omarion +monkeys +loverboy +elijah +ronnie +mamita +999999999 +broken +rodrigo +westside +mauricio +amigas +preciosa +shopping +flores +isabella +martinez +elaine +friendster +cheche +gracie +connor +valentina +darling +santos +joanne +fuckyou2 +pebbles +sunshine1 +gangster +gloria +darkangel +bettyboop +jessica1 +cheyenne +dustin +iubire +a123456 +purple1 +bestfriends +inlove +batista +karla +chacha +marian +sexyme +pogiako +jordan1 +010203 +daddy1 +daddysgirl +billabong +pinky +erika +skater +nenita +tigger1 +gatito +lokita +maldita +buttercup +bambam +glitter +123789 +sister +zacefron +tokiohotel +loveya +lovebug +bubblegum +marissa +cecilia +lollypop +nicolas +puppies +ariana +chubby +sexybitch +roxana +mememe +susana +baller +hotstuff +carter +babylove +angelina +playgirl +sweet16 +012345 +bhebhe +marcos +loveme1 +milagros +lilmama +beyonce +lovely1 +catdog +armando +margarita +151515 +loves +202020 +gerard +undertaker +amistad +capricorn +delfin +cheerleader +password2 +PASSWORD +lizzie +matthew1 +enrique +badgirl +141414 +dancing +cuteme +amelia +skyline +angeles +janine +carlitos +justme +legolas +michelle1 +cinderella +jesuschrist +ilovejesus +tazmania +tekiero +thebest +princesita +lucky7 +jesucristo +buddy1 +regina +myself +lipgloss +jazmin +rosita +chichi +pangit +mierda +741852963 +hernandez +arturo +silvia +melvin +celeste +pussycat +gorgeous +honeyko +mylife +babyboo +loveu +lupita +panthers +hollywood +alfredo +musica +hawaii +sparkle +kristina +sexymama +crazy +scarface +098765 +hayden +micheal +242424 +0987654321 +marisol +jeremiah +mhine +isaiah +lolipop +butterfly1 +xbox360 +madalina +anamaria +yourmom +jasmine1 +bubbles1 +beatriz +diamonds +friendship +sweetness +desiree +741852 +hannah1 +bananas +julius +leanne +marie1 +lover1 +twinkle +february +bebita +87654321 +twilight +imissyou +pollito +ashlee +cookie1 +147852369 +beckham +simone +nursing +torres +damian +123123123 +joshua1 +babyface +dinamo +mommy +juliana +cassandra +redsox +gundam +0000 +ou812 +dave +golf +molson +Monday +newpass +thx1138 +1 +Internet +coke +foobar +abc +fish +fred +help +ncc1701d +newuser +none +pat +dog +duck +duke +floyd +guest +joe +kingfish +micro +sam +telecom +test1 +7777 +absolut +babylon5 +backup +bill +bird33 +deliver +fire +flip +galileo +gopher +hansolo +jane +jim +mom +passwd +phil +phish +porsche911 +rain +red +sergei +training +truck +video +volvo +007 +1969 +5683 +Bond007 +Friday +Hendrix +October +Taurus +aaa +alexandr +catalog +challenge +clipper +coltrane +cyrano +dan +dawn +dean +deutsch +dilbert +e-mail +export +ford +fountain +fox +frog +gabriell +garlic +goforit +grateful +hoops +lady +ledzep +lee +mailman +mantra +market +mazda1 +metallic +ncc1701e +nesbitt +open +pete +quest +republic +research +supra +tara +testing +xanadu +xxxx +zaphod +zeus +0007 +1022 +10sne1 +1973 +1978 +2000 +2222 +3bears +Broadway +Fisher +Jeanne +Killer +Knight +Master +Pepper +Sierra +Tennis +abacab +abcd +ace +acropolis +amy +anders +avenir +basil +bass +beer +ben +bliss +blowfish +boss +bridges +buck +bugsy +bull +cannondale +canon +catnip +chip +civil +content +cook +cordelia +crack1 +cyber +daisie +dark1 +database +deadhead +denali +depeche +dickens +emmitt +entropy +farout +farside +feedback +fidel +firenze +fish1 +fletch +fool +fozzie +fun +gargoyle +gasman +gold +graphic +hell +image +intern +intrepid +jeff +jkl123 +joel +johanna1 +kidder +kim +king +kirk +kris +lambda +leon +logical +lorrie +major +mariner +mark1 +max +media +merlot +midway +mine +mmouse +moon +mopar +mortimer +nermal +nina +olsen +opera +overkill +pacers +packer +picard +polar +polo +primus +prometheus +public +radio +rastafarian +reptile +rob +robotech +rodeo +rolex +rouge +roy +ruby +salasana +scarecrow +scout +scuba1 +sergey +skibum +skunk +sound +starter +sting1 +sunbird +tbird +teflon +temporal +terminal +the +thejudge +time +toby +today +tokyo +tree +trout +vader +val +valhalla +windsurf +wolf +wolf1 +xcountry +yoda +yukon +1213 +1214 +1225 +1313 +1818 +1975 +1977 +1991 +1kitty +2001 +2020 +2112 +2kids +333 +4444 +5050 +57chevy +7dwarfs +Animals +Ariel +Bismillah +Booboo +Boston +Carol +Computer +Creative +Curtis +Denise +Eagles +Esther +Fishing +Freddy +Gandalf +Golden +Goober +Hacker +Harley +Henry +Hershey +Jackson +Jersey +Joanna +Johnson +Katie +Kitten +Liberty +Lindsay +Lizard +Madeline +Margaret +Maxwell +Money +Monster +Pamela +Peaches +Peter +Phoenix +Piglet +Pookie +Rabbit +Raiders +Random +Russell +Sammy +Saturn +Skeeter +Smokey +Sparky +Speedy +Sterling +Theresa +Thunder +Vincent +Willow +Winnie +Wolverine +aaaa +aardvark +abbott +acura +admin +admin1 +adrock +aerobics +agent +airwolf +ali +alien +allegro +allstate +altamira +altima1 +andrew! +ann +anne +anneli +aptiva +arrow +asdf;lkj +assmunch +baraka +barnyard +bart +bartman +beasty +beavis1 +bebe +belgium +beowulf +beryl +best +bharat +bichon +bigal +biker +bilbo +bills +bimmer +biochem +birdy +blinds +blitz +bluejean +bogey +bogus +boulder +bourbon +boxer +brain +branch +britain +broker +bucks +buffett +bugs +bulls +burns +buzz +c00per +calgary +camay +carl +cat +cement +cessna +chad +chainsaw +chameleon +chang +chess +chinook +chouette +chronos +cicero +circuit +cirque +cirrus +clapton +clarkson +class +claudel +cleo +cliff +clock +color +comet +concept +concorde +coolbean +corky +cornflake +corwin +cows +crescent +cross +crowley +cthulhu +cunt +current +cutlass +daedalus +dagger1 +daily +dale +dana +daytek +dead +decker +dharma +dillweed +dipper +disco +dixon +doitnow +doors +dork +doug +dutch +effie +ella +elsie +engage +eric1 +ernie1 +escort1 +excel +faculty +fairview +faust +fenris +finance +first +fishhead +flanders +fleurs +flute +flyboy +flyer +franka +frederic +free +front242 +frontier +fugazi +funtime +gaby +gaelic +gambler +gammaphi +garfunkel +garth +gary +gateway2 +gator1 +gibbons +gigi +gilgamesh +goat +godiva +goethe +gofish +good +gramps +gravis +gray +greed +greg +greg1 +greta +gretzky +guido +gumby +h2opolo +hamid +hank +hawkeye1 +health1 +hello8 +help123 +helper +homerj +hoosier +hope +huang +hugo +hydrogen +ib6ub9 +insight +instructor +integral +iomega +iris +izzy +jazz +jean +jeepster +jetta1 +joanie +josee +joy +julia2 +jumbo +jump +justice4 +kalamazoo +kali +kat +kate +kerala +kids +kiwi +kleenex +kombat +lamer +laser +laserjet +lassie1 +leblanc +legal +leo +life +lions +liz +logger +logos +loislane +loki +longer +lori +lost +lotus +lou +macha +macross +madoka +makeitso +mallard +marc +math +mattingly +mechanic +meister +mercer +merde +merrill +michal +michou +mickel +minou +mobydick +modem +mojo +montana3 +montrose +motor +mowgli +mulder1 +muscle +neil +neutrino +newaccount +nicklaus +nightshade +nightwing +nike +none1 +nopass +nouveau +novell +oaxaca +obiwan +obsession +orville +otter +ozzy +packrat +paint +papa +paradigm +pass +pavel +peterk +phialpha +phishy +piano1 +pianoman +pianos +pipeline +plato +play +poetic +print +printing +provider +qqq111 +quebec +qwer +racer +racerx +radar +rafiki +raleigh +rasta1 +redcloud +redfish +redwing +redwood +reed +rene +reznor +rhino +ripple +rita +robocop +robotics +roche +roni +rossignol +rugger +safety1 +saigon +satori +saturn5 +schnapps +scotch +scuba +secret3 +seeker +services +sex +shanghai +shazam +shelter +sigmachi +signal +signature +simsim +skydive +slick +smegma +smiths +smurfy +snow +sober1 +sonics +sony +spazz +sphynx +spock +spoon +spot +sprocket +starbuck +steel +stephi +sting +stocks +storage +strat +strato +stud +student2 +susanna +swanson +swim +switzer +system5 +t-bone +talon +tarheel +tata +tazdevil +tester +testtest +thisisit +thorne +tightend +tim +tom +tool +total +toucan +transfer +transit +transport +trapper +trash +trophy +tucson +turbo2 +unity +upsilon +vedder +vette +vikram +virago +visual +volcano +walden +waldo +walleye +webmaster +wedge +whale1 +whit +whoville +wibble +will +wombat1 +word +world +x-files +xxx123 +zack +zepplin +zoltan +zoomer +123go +21122112 +5555 +911 +FuckYou +Fuckyou +Gizmo +Hello +Michel +Qwerty +Windows +angus +aspen +ass +bird +booster +byteme +cats +changeit +christia +christoph +classroom +cloclo +corrado +dasha +fiction +french1 +fubar +gator +gilles +gocougs +hilbert +hola +home +judy +koko +lulu +mac +macintosh +mailer +mars +meow +ne1469 +niki +paul +politics +pomme +property +ruth +sales +salut +scrooge +skidoo +spain +surf +sylvie +symbol +forum +rotimi +god +saved +2580 +1998 +xxx +1928 +777 +info +a +netware +sun +tech +doom +mmm +one +ppp +1911 +1948 +1996 +5252 +Champs +Tuesday +bach +crow +don +draft +hal9000 +herzog +huey +jethrotull +jussi +mail +miki +nicarao +snowski +1316 +1412 +1430 +1952 +1953 +1955 +1956 +1960 +1964 +1qw23e +22 +2200 +2252 +3010 +3112 +4788 +6262 +Alpha +Bastard +Beavis +Cardinal +Celtics +Cougar +Darkman +Figaro +Fortune +Geronimo +Hammer +Homer +Janet +Mellon +Merlot +Metallic +Montreal +Newton +Paladin +Peanuts +Service +Vernon +Waterloo +Webster +aki123 +aqua +aylmer +beta +bozo +car +chat +chinacat +cora +courier +dogbert +eieio +elina1 +fly +funguy +fuzz +ggeorge +glider1 +gone +hawk +heikki +histoire +hugh +if6was9 +ingvar +jan +jedi +jimi +juhani +khan +lima +midvale +neko +nesbit +nexus6 +nisse +notta1 +pam +park +pole +pope +pyro +ram +reliant +rex +rush +seoul +skip +stan +sue +suzy +tab +testi +thelorax +tika +tnt +toto1 +tre +wind +x-men +xyz +zxc +369 +Abcdef +Asdfgh +Changeme +NCC1701 +Zxcvbnm +demo +doom2 +e +good-luck +homebrew +m1911a1 +nat +ne1410s +ne14a69 +zhongguo +sample123 +0852 +basf +OU812 +!@#$% +informix +majordomo +news +temp +trek +!@#$%^ +!@#$%^&* +Pentium +Raistlin +adi +bmw +law +m +new +opus +plus +visa +www +y +zzz +1332 +1950 +3141 +3533 +4055 +4854 +6301 +Bonzo +ChangeMe +Front242 +Gretel +Michel1 +Noriko +Sidekick +Sverige +Swoosh +Woodrow +aa +ayelet +barn +betacam +biz +boat +cuda +doc +hal +hallowell +haro +hosehead +i +ilmari +irmeli +j1l2t3 +jer +kcin +kerrya +kissa2 +leaf +lissabon +mart +matti1 +mech +morecats +paagal +performa +prof +ratio +ship +slip +stivers +tapani +targas +test2 +test3 +tula +unix +user1 +xanth +!@#$%^& +1701d +@#$%^& +Qwert +allo +dirk +go +newcourt +nite +notused +sss diff --git a/resources/celerity/map.php b/resources/celerity/map.php new file mode 100644 index 0000000000..0501153b27 --- /dev/null +++ b/resources/celerity/map.php @@ -0,0 +1,2191 @@ + + array( + 'core.pkg.css' => 'a74869c3', + 'core.pkg.js' => 'c7854cc5', + 'darkconsole.pkg.js' => 'ca8671ce', + 'differential.pkg.css' => '5a65a762', + 'differential.pkg.js' => '322ea941', + 'diffusion.pkg.css' => '3783278d', + 'diffusion.pkg.js' => '7b51e80a', + 'javelin.pkg.js' => '6d430a66', + 'maniphest.pkg.css' => '6944ad50', + 'maniphest.pkg.js' => '1e8f11af', + 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', + 'rsrc/css/aphront/aphront-notes.css' => '6acadd3f', + 'rsrc/css/aphront/calendar-view.css' => '166af03d', + 'rsrc/css/aphront/context-bar.css' => '1c3b0529', + 'rsrc/css/aphront/dark-console.css' => '6378ef3d', + 'rsrc/css/aphront/dialog-view.css' => 'dd9db96c', + 'rsrc/css/aphront/error-view.css' => '16cd9949', + 'rsrc/css/aphront/lightbox-attachment.css' => '686f8885', + 'rsrc/css/aphront/list-filter-view.css' => 'ef989c67', + 'rsrc/css/aphront/multi-column.css' => '05bbd016', + 'rsrc/css/aphront/notification.css' => '6901121e', + 'rsrc/css/aphront/pager-view.css' => '2e3539af', + 'rsrc/css/aphront/panel-view.css' => '5846dfa2', + 'rsrc/css/aphront/phabricator-nav-view.css' => 'd0d4a509', + 'rsrc/css/aphront/request-failure-view.css' => 'da14df31', + 'rsrc/css/aphront/table-view.css' => '92a719ca', + 'rsrc/css/aphront/tokenizer.css' => 'd888465e', + 'rsrc/css/aphront/tooltip.css' => '9c90229d', + 'rsrc/css/aphront/transaction.css' => 'ce491938', + 'rsrc/css/aphront/two-column.css' => '16ab3ad2', + 'rsrc/css/aphront/typeahead.css' => '00c9a200', + 'rsrc/css/application/auth/auth.css' => '1e655982', + 'rsrc/css/application/base/main-menu-view.css' => 'd4021e15', + 'rsrc/css/application/base/notification-menu.css' => 'fc9a363c', + 'rsrc/css/application/base/phabricator-application-launch-view.css' => '55ba7571', + 'rsrc/css/application/base/standard-page-view.css' => '517cdfb1', + 'rsrc/css/application/chatlog/chatlog.css' => '0cd2bc78', + 'rsrc/css/application/config/config-options.css' => '7fedf08b', + 'rsrc/css/application/config/config-template.css' => '25d446d6', + 'rsrc/css/application/config/setup-issue.css' => '1bb81c53', + 'rsrc/css/application/conpherence/menu.css' => '561348ac', + 'rsrc/css/application/conpherence/message-pane.css' => '2aedca89', + 'rsrc/css/application/conpherence/notification.css' => 'f9ba9914', + 'rsrc/css/application/conpherence/update.css' => '1099a660', + 'rsrc/css/application/conpherence/widget-pane.css' => '87b12e0c', + 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', + 'rsrc/css/application/countdown/timer.css' => '86b7b0a0', + 'rsrc/css/application/diff/inline-comment-summary.css' => '14a91639', + 'rsrc/css/application/differential/add-comment.css' => 'c478bcaa', + 'rsrc/css/application/differential/changeset-view.css' => '82431767', + 'rsrc/css/application/differential/core.css' => '8135cb0c', + 'rsrc/css/application/differential/local-commits-view.css' => '19649019', + 'rsrc/css/application/differential/results-table.css' => '239924f9', + 'rsrc/css/application/differential/revision-comment-list.css' => 'bc291c47', + 'rsrc/css/application/differential/revision-comment.css' => '48186045', + 'rsrc/css/application/differential/revision-history.css' => 'f37aee8f', + 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33', + 'rsrc/css/application/differential/table-of-contents.css' => '19566f76', + 'rsrc/css/application/diffusion/commit-view.css' => '92d1e8f9', + 'rsrc/css/application/diffusion/diffusion-icons.css' => '384a0f7d', + 'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661', + 'rsrc/css/application/directory/phabricator-jump-nav.css' => 'f0c5e726', + 'rsrc/css/application/feed/feed.css' => '0d17c209', + 'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad', + 'rsrc/css/application/flag/flag.css' => '5337623f', + 'rsrc/css/application/herald/herald-test.css' => '2b7d0f54', + 'rsrc/css/application/herald/herald.css' => '59d48f01', + 'rsrc/css/application/legalpad/legalpad-document.css' => 'cd275275', + 'rsrc/css/application/maniphest/batch-editor.css' => '78444bc1', + 'rsrc/css/application/maniphest/report.css' => '6fc16517', + 'rsrc/css/application/maniphest/task-edit.css' => '8e23031b', + 'rsrc/css/application/maniphest/task-summary.css' => '0cabd9a6', + 'rsrc/css/application/objectselector/object-selector.css' => '029a133d', + 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', + 'rsrc/css/application/paste/paste.css' => 'aa1767d1', + 'rsrc/css/application/people/people-profile.css' => 'd0bababe', + 'rsrc/css/application/phame/phame.css' => '450826e1', + 'rsrc/css/application/pholio/pholio-edit.css' => 'b9e59b6d', + 'rsrc/css/application/pholio/pholio-inline-comments.css' => '52be33f0', + 'rsrc/css/application/pholio/pholio.css' => 'd23ace50', + 'rsrc/css/application/phortune/phortune-credit-card-form.css' => 'b25b4beb', + 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', + 'rsrc/css/application/phriction/phriction-document-css.css' => 'b0309d8e', + 'rsrc/css/application/policy/policy-edit.css' => '05cca26a', + 'rsrc/css/application/policy/policy.css' => '957ea14c', + 'rsrc/css/application/ponder/comments.css' => '6cdccea7', + 'rsrc/css/application/ponder/feed.css' => 'e62615b6', + 'rsrc/css/application/ponder/post.css' => 'ebab8a70', + 'rsrc/css/application/ponder/vote.css' => '8ed6ed8b', + 'rsrc/css/application/profile/profile-view.css' => '3a7e04ca', + 'rsrc/css/application/projects/phabricator-object-list-view.css' => '1a1ea560', + 'rsrc/css/application/projects/project-tag.css' => '095c9404', + 'rsrc/css/application/releeph/releeph-branch.css' => 'b8821d2d', + 'rsrc/css/application/releeph/releeph-colors.css' => '2d2d6aa8', + 'rsrc/css/application/releeph/releeph-core.css' => '140b959d', + 'rsrc/css/application/releeph/releeph-intents.css' => '7364ac97', + 'rsrc/css/application/releeph/releeph-preview-branch.css' => '0e383ca3', + 'rsrc/css/application/releeph/releeph-project.css' => 'ee1f9f57', + 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', + 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae', + 'rsrc/css/application/releeph/releeph-status.css' => 'a20631d9', + 'rsrc/css/application/search/search-results.css' => 'f240504c', + 'rsrc/css/application/settings/settings.css' => 'ea8f5915', + 'rsrc/css/application/slowvote/slowvote.css' => '266df6a1', + 'rsrc/css/application/tokens/tokens.css' => 'fb286311', + 'rsrc/css/application/uiexample/example.css' => '4741b891', + 'rsrc/css/core/core.css' => 'da26ddb2', + 'rsrc/css/core/remarkup.css' => 'ca7f2265', + 'rsrc/css/core/syntax.css' => '3c18c1cb', + 'rsrc/css/core/z-index.css' => '1bbbd4f1', + 'rsrc/css/diviner/diviner-shared.css' => 'be90f718', + 'rsrc/css/layout/phabricator-action-header-view.css' => 'cc654b91', + 'rsrc/css/layout/phabricator-action-list-view.css' => '81383e25', + 'rsrc/css/layout/phabricator-crumbs-view.css' => '2d9db584', + 'rsrc/css/layout/phabricator-filetree-view.css' => 'a8c86ace', + 'rsrc/css/layout/phabricator-hovercard-view.css' => '67c12b16', + 'rsrc/css/layout/phabricator-side-menu-view.css' => '503699d0', + 'rsrc/css/layout/phabricator-source-code-view.css' => '62a99814', + 'rsrc/css/layout/phabricator-timeline-view.css' => 'f4f846c4', + 'rsrc/css/phui/phui-box.css' => '21da4d8c', + 'rsrc/css/phui/phui-button.css' => '8106a67a', + 'rsrc/css/phui/phui-document.css' => '143b2ac8', + 'rsrc/css/phui/phui-feed-story.css' => '3a59c2cf', + 'rsrc/css/phui/phui-form-view.css' => '4b2019ff', + 'rsrc/css/phui/phui-form.css' => 'b78ec020', + 'rsrc/css/phui/phui-header-view.css' => '472a6003', + 'rsrc/css/phui/phui-icon.css' => '29e83226', + 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', + 'rsrc/css/phui/phui-list.css' => '2edb76cf', + 'rsrc/css/phui/phui-object-box.css' => '4f916b80', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'eb579d6c', + 'rsrc/css/phui/phui-pinboard-view.css' => '53c5fca0', + 'rsrc/css/phui/phui-property-list-view.css' => 'dbf53b12', + 'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b', + 'rsrc/css/phui/phui-spacing.css' => '042804d6', + 'rsrc/css/phui/phui-status.css' => '2f562399', + 'rsrc/css/phui/phui-tag-view.css' => '295d81c4', + 'rsrc/css/phui/phui-text.css' => '23e9b4b7', + 'rsrc/css/phui/phui-workboard-view.css' => 'bf70dd2e', + 'rsrc/css/phui/phui-workpanel-view.css' => '6f8527f6', + 'rsrc/css/sprite-actions.css' => '4557baf8', + 'rsrc/css/sprite-apps-large.css' => '0a453d4b', + 'rsrc/css/sprite-apps-xlarge.css' => 'db66c878', + 'rsrc/css/sprite-apps.css' => 'c3857e4e', + 'rsrc/css/sprite-conpherence.css' => '084b1f13', + 'rsrc/css/sprite-docs.css' => '7ea4927e', + 'rsrc/css/sprite-gradient.css' => 'a10def53', + 'rsrc/css/sprite-icons.css' => 'ce73ef3e', + 'rsrc/css/sprite-login.css' => 'fa00ebdc', + 'rsrc/css/sprite-main-header.css' => 'ba949d0d', + 'rsrc/css/sprite-menu.css' => 'fbf248ea', + 'rsrc/css/sprite-minicons.css' => 'eb009037', + 'rsrc/css/sprite-payments.css' => '68ec786d', + 'rsrc/css/sprite-projects.css' => '8109f6e3', + 'rsrc/css/sprite-status.css' => '767312f9', + 'rsrc/css/sprite-tokens.css' => '9540e337', + 'rsrc/custom/css/phabricator-welcome-page.css' => 'c370f13b', + 'rsrc/custom/image/blender_logo.png' => '3667b481', + 'rsrc/externals/javelin/core/Event.js' => '79473b62', + 'rsrc/externals/javelin/core/Stratcom.js' => 'c293f7b9', + 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => 'e27df27b', + 'rsrc/externals/javelin/core/__tests__/install.js' => '1dd4d6db', + 'rsrc/externals/javelin/core/__tests__/stratcom.js' => 'da194d4b', + 'rsrc/externals/javelin/core/__tests__/util.js' => 'd3b157a9', + 'rsrc/externals/javelin/core/init.js' => 'b88ab49e', + 'rsrc/externals/javelin/core/install.js' => '52a92793', + 'rsrc/externals/javelin/core/util.js' => '7501647b', + 'rsrc/externals/javelin/docs/Base.js' => '3b9ca7eb', + 'rsrc/externals/javelin/docs/onload.js' => '69948972', + 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a', + 'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba', + 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => '63f9ad59', + 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => 'ba86e2fd', + 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '96474586', + 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '4c33dff1', + 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'bd3c1838', + 'rsrc/externals/javelin/ext/view/HTMLView.js' => '957caa12', + 'rsrc/externals/javelin/ext/view/View.js' => '4641579a', + 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => '0c33c1a0', + 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '2fa810fc', + 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '77461fd6', + 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'ca704f2b', + 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb', + 'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'bda69c40', + 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', + 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '5426001c', + 'rsrc/externals/javelin/lib/Cookie.js' => '6b3dcf44', + 'rsrc/externals/javelin/lib/DOM.js' => '03be94fb', + 'rsrc/externals/javelin/lib/History.js' => 'c60f4327', + 'rsrc/externals/javelin/lib/JSON.js' => '08e56a4e', + 'rsrc/externals/javelin/lib/Mask.js' => 'b9f26029', + 'rsrc/externals/javelin/lib/Request.js' => '23f9bb8d', + 'rsrc/externals/javelin/lib/Resource.js' => '356de121', + 'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862', + 'rsrc/externals/javelin/lib/Vector.js' => '403a3dce', + 'rsrc/externals/javelin/lib/Workflow.js' => 'd16edeae', + 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', + 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', + 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074', + 'rsrc/externals/javelin/lib/__tests__/URI.js' => 'ece3ddb3', + 'rsrc/externals/javelin/lib/__tests__/behavior.js' => 'c1d75ee6', + 'rsrc/externals/javelin/lib/behavior.js' => '8a3ed18b', + 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'e9e18227', + 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'c22f4c01', + 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '5f850b5c', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => 'dbd9cd11', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '1f595fb0', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => 'e9b95df3', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'f4412299', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => 'c2b8bf64', + 'rsrc/externals/raphael/g.raphael.js' => '40dde778', + 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', + 'rsrc/externals/raphael/raphael.js' => '51ee6b43', + 'rsrc/image/actions/edit.png' => '2fc41442', + 'rsrc/image/apple-touch-icon.png' => '8458dda7', + 'rsrc/image/avatar.png' => '62c5f933', + 'rsrc/image/checker_dark.png' => 'd8e65881', + 'rsrc/image/checker_light.png' => 'a0155918', + 'rsrc/image/credit_cards.png' => '72b8ede8', + 'rsrc/image/darkload.gif' => '1ffd3ec6', + 'rsrc/image/divot.png' => '94dded62', + 'rsrc/image/grippy_texture.png' => 'aca81e2f', + 'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c', + 'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0', + 'rsrc/image/icon/fatcow/bullet_black.png' => 'ff190031', + 'rsrc/image/icon/fatcow/bullet_orange.png' => 'e273e5bb', + 'rsrc/image/icon/fatcow/bullet_red.png' => 'c0b75434', + 'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275', + 'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60', + 'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d', + 'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee', + 'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783', + 'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a', + 'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66', + 'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2', + 'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522', + 'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f', + 'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4', + 'rsrc/image/icon/fatcow/folder.png' => '95a435af', + 'rsrc/image/icon/fatcow/folder_go.png' => '001cbc94', + 'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a', + 'rsrc/image/icon/fatcow/link.png' => '7afd4d5e', + 'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8', + 'rsrc/image/icon/fatcow/page_white_link.png' => 'a90023c7', + 'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c', + 'rsrc/image/icon/fatcow/page_white_text.png' => '1e1f79c3', + 'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f', + 'rsrc/image/icon/fatcow/source/email.png' => '9bab3239', + 'rsrc/image/icon/fatcow/source/fax.png' => '04195e68', + 'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264', + 'rsrc/image/icon/fatcow/source/tablet.png' => '49396799', + 'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d', + 'rsrc/image/icon/fatcow/thumbnails/default160x120.png' => 'f2e8a2eb', + 'rsrc/image/icon/fatcow/thumbnails/default60x45.png' => '0118abed', + 'rsrc/image/icon/fatcow/thumbnails/image160x120.png' => '79bb556a', + 'rsrc/image/icon/fatcow/thumbnails/image60x45.png' => 'c5e1685e', + 'rsrc/image/icon/fatcow/thumbnails/pdf160x120.png' => 'ac9edbf5', + 'rsrc/image/icon/fatcow/thumbnails/pdf60x45.png' => 'c0db4143', + 'rsrc/image/icon/fatcow/thumbnails/zip160x120.png' => '75f9cd0f', + 'rsrc/image/icon/fatcow/thumbnails/zip60x45.png' => 'af11bf3e', + 'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8', + 'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e', + 'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b', + 'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3', + 'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0', + 'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21', + 'rsrc/image/icon/subscribe.png' => 'd03ed5a5', + 'rsrc/image/icon/tango/attachment.png' => 'ecc8022e', + 'rsrc/image/icon/tango/edit.png' => '929a1363', + 'rsrc/image/icon/tango/go-down.png' => '96d95e43', + 'rsrc/image/icon/tango/log.png' => 'b08cc63a', + 'rsrc/image/icon/tango/upload.png' => '7bbb7984', + 'rsrc/image/icon/unsubscribe.png' => '25725013', + 'rsrc/image/lightblue-header.png' => '5c168b6d', + 'rsrc/image/loading.gif' => '75d384cc', + 'rsrc/image/loading/boating_24.gif' => '5c90f086', + 'rsrc/image/loading/compass_24.gif' => 'b36b4f46', + 'rsrc/image/loading/loading_24.gif' => '26bc9adc', + 'rsrc/image/loading/loading_48.gif' => '6a4994c7', + 'rsrc/image/loading/loading_d48.gif' => 'cdcbe900', + 'rsrc/image/loading/loading_w24.gif' => '7662fa2b', + 'rsrc/image/main_texture.png' => '29a2c5ad', + 'rsrc/image/menu_texture.png' => '5a17580d', + 'rsrc/image/people/harding.png' => '45aa614e', + 'rsrc/image/people/jefferson.png' => 'afca0e53', + 'rsrc/image/people/lincoln.png' => '9369126d', + 'rsrc/image/people/mckinley.png' => 'fb8f16ce', + 'rsrc/image/people/taft.png' => 'd7bc402c', + 'rsrc/image/people/washington.png' => '40dd301c', + 'rsrc/image/phrequent_active.png' => 'a466a8ed', + 'rsrc/image/phrequent_inactive.png' => 'bfc15a69', + 'rsrc/image/search-white.png' => '64cc0d45', + 'rsrc/image/search.png' => '82625a7e', + 'rsrc/image/sprite-actions-X2.png' => '7dfd5652', + 'rsrc/image/sprite-actions.png' => '7b370d72', + 'rsrc/image/sprite-apps-X2.png' => '8d56a056', + 'rsrc/image/sprite-apps-large-X2.png' => '0e676838', + 'rsrc/image/sprite-apps-large.png' => 'b0aa7d64', + 'rsrc/image/sprite-apps-xlarge.png' => 'a751a580', + 'rsrc/image/sprite-apps.png' => '35052ada', + 'rsrc/image/sprite-conpherence-X2.png' => '5ed80fb0', + 'rsrc/image/sprite-conpherence.png' => '7146f76f', + 'rsrc/image/sprite-docs-X2.png' => '520858fa', + 'rsrc/image/sprite-docs.png' => '4636297f', + 'rsrc/image/sprite-gradient.png' => '4ece0b62', + 'rsrc/image/sprite-icons-X2.png' => '2e9bb665', + 'rsrc/image/sprite-icons.png' => 'edbe6b0d', + 'rsrc/image/sprite-login-X2.png' => 'd2132242', + 'rsrc/image/sprite-login.png' => '7f878f1d', + 'rsrc/image/sprite-main-header.png' => 'bca643fc', + 'rsrc/image/sprite-menu-X2.png' => '4e38aacb', + 'rsrc/image/sprite-menu.png' => '8d4da28c', + 'rsrc/image/sprite-minicons-X2.png' => 'd986b16b', + 'rsrc/image/sprite-minicons.png' => '272644ea', + 'rsrc/image/sprite-payments.png' => 'aedcb4a6', + 'rsrc/image/sprite-projects-X2.png' => 'd30e8ca4', + 'rsrc/image/sprite-projects.png' => '9e372fae', + 'rsrc/image/sprite-status-X2.png' => '2997265d', + 'rsrc/image/sprite-status.png' => '94fdbdd8', + 'rsrc/image/sprite-tokens-X2.png' => '1e5d2641', + 'rsrc/image/sprite-tokens.png' => 'a6cf4488', + 'rsrc/image/texture/card-gradient.png' => '815f26e8', + 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8', + 'rsrc/image/texture/dark-menu.png' => '7e22296e', + 'rsrc/image/texture/grip.png' => '719404f3', + 'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe', + 'rsrc/image/texture/phlnx-bg.png' => '8d819209', + 'rsrc/image/texture/pholio-background.gif' => 'ba29239c', + 'rsrc/image/texture/table_header.png' => '5c433037', + 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', + 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', + 'rsrc/js/application/aphlict/Aphlict.js' => '493665ee', + 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '2a2dba85', + 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '845731b8', + 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', + 'rsrc/js/application/config/behavior-reorder-fields.js' => '69bb5094', + 'rsrc/js/application/conpherence/behavior-menu.js' => '872bc8ff', + 'rsrc/js/application/conpherence/behavior-pontificate.js' => '53f6f2dd', + 'rsrc/js/application/conpherence/behavior-widget-pane.js' => 'd8ef8659', + 'rsrc/js/application/countdown/timer.js' => '8454ce4f', + 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746', + 'rsrc/js/application/differential/behavior-accept-with-errors.js' => 'e12c760a', + 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '4ba4c13d', + 'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79', + 'rsrc/js/application/differential/behavior-comment-preview.js' => '127f2018', + 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', + 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '5f004630', + 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '93f43142', + 'rsrc/js/application/differential/behavior-keyboard-nav.js' => 'da3e88f9', + 'rsrc/js/application/differential/behavior-populate.js' => 'ce0c217a', + 'rsrc/js/application/differential/behavior-show-all-comments.js' => '784b8218', + 'rsrc/js/application/differential/behavior-show-field-details.js' => '441f2137', + 'rsrc/js/application/differential/behavior-show-more.js' => 'dd7e8ef5', + 'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb', + 'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d', + 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'be81801d', + 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'eae2f65d', + 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '85ba3cf4', + 'rsrc/js/application/diffusion/behavior-jump-to.js' => '9db3d160', + 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', + 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => '3c5310da', + 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'fd27d99a', + 'rsrc/js/application/files/behavior-icon-composer.js' => 'ea38f732', + 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '6ec125a0', + 'rsrc/js/application/harbormaster/behavior-reorder-steps.js' => 'b21125a5', + 'rsrc/js/application/herald/HeraldRuleEditor.js' => '4366c8cc', + 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', + 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', + 'rsrc/js/application/maniphest/behavior-batch-editor.js' => '391457d7', + 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ead554ec', + 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'cdcbe8a4', + 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'cf76cfd5', + 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '84845b5b', + 'rsrc/js/application/maniphest/behavior-transaction-controls.js' => '75e50c72', + 'rsrc/js/application/maniphest/behavior-transaction-expand.js' => '2f2e18aa', + 'rsrc/js/application/maniphest/behavior-transaction-preview.js' => 'f8248bc5', + 'rsrc/js/application/owners/OwnersPathEditor.js' => '46efd18e', + 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3', + 'rsrc/js/application/passphrase/phame-credential-control.js' => '1e1c8a59', + 'rsrc/js/application/phame/phame-post-preview.js' => '61d927ec', + 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '1e1e8bb0', + 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => '28497740', + 'rsrc/js/application/phortune/behavior-balanced-payment-form.js' => '3b3e1664', + 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '1693a296', + 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'b3e5ee60', + 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', + 'rsrc/js/application/policy/behavior-policy-control.js' => 'c01153ea', + 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '263aeb8c', + 'rsrc/js/application/ponder/behavior-votebox.js' => '327dbe61', + 'rsrc/js/application/projects/behavior-project-boards.js' => '1b9facd8', + 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', + 'rsrc/js/application/releeph/releeph-preview-branch.js' => '9eb2cedb', + 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'fe7fc914', + 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'cd9e7094', + 'rsrc/js/application/repository/repository-crossreference.js' => '8ab282be', + 'rsrc/js/application/search/behavior-reorder-queries.js' => '34397f68', + 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => 'a51fdb2e', + 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9084a36f', + 'rsrc/js/application/transactions/behavior-transaction-list.js' => '0dcf1716', + 'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807', + 'rsrc/js/application/uiexample/ReactorButtonExample.js' => '44524435', + 'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '7ba325ee', + 'rsrc/js/application/uiexample/ReactorFocusExample.js' => '82f568cd', + 'rsrc/js/application/uiexample/ReactorInputExample.js' => 'd6ca6b1c', + 'rsrc/js/application/uiexample/ReactorMouseoverExample.js' => '4e37e4de', + 'rsrc/js/application/uiexample/ReactorRadioExample.js' => '858f9728', + 'rsrc/js/application/uiexample/ReactorSelectExample.js' => '189e4fe3', + 'rsrc/js/application/uiexample/ReactorSendClassExample.js' => 'bf97561d', + 'rsrc/js/application/uiexample/ReactorSendPropertiesExample.js' => '551add57', + 'rsrc/js/application/uiexample/busy-example.js' => 'fbbce3bf', + 'rsrc/js/application/uiexample/gesture-example.js' => 'f42bb8c6', + 'rsrc/js/application/uiexample/notification-example.js' => 'c51a6616', + 'rsrc/js/core/Busy.js' => '6453c869', + 'rsrc/js/core/DragAndDropFileUpload.js' => 'ae6abfba', + 'rsrc/js/core/DraggableList.js' => '1681c4d4', + 'rsrc/js/core/DropdownMenu.js' => 'fb342e18', + 'rsrc/js/core/DropdownMenuItem.js' => '0f386ef4', + 'rsrc/js/core/FileUpload.js' => '96713558', + 'rsrc/js/core/Hovercard.js' => '4f344388', + 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', + 'rsrc/js/core/KeyboardShortcutManager.js' => 'ad7a69ca', + 'rsrc/js/core/MultirowRowManager.js' => 'e7076916', + 'rsrc/js/core/Notification.js' => '95944043', + 'rsrc/js/core/Prefab.js' => '9eaf0bfa', + 'rsrc/js/core/ShapedRequest.js' => 'dfa181a4', + 'rsrc/js/core/TextAreaUtils.js' => 'b3ec3cfc', + 'rsrc/js/core/ToolTip.js' => '0a81ea29', + 'rsrc/js/core/behavior-active-nav.js' => 'c81bc98f', + 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', + 'rsrc/js/core/behavior-autofocus.js' => '7319e029', + 'rsrc/js/core/behavior-crop.js' => 'b98fc918', + 'rsrc/js/core/behavior-dark-console.js' => 'e9fdb5e5', + 'rsrc/js/core/behavior-device.js' => '03d6ed07', + 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '94d146cb', + 'rsrc/js/core/behavior-error-log.js' => 'a5d7cf86', + 'rsrc/js/core/behavior-fancy-datepicker.js' => '5d584426', + 'rsrc/js/core/behavior-file-tree.js' => 'c8728c70', + 'rsrc/js/core/behavior-form.js' => '27d4da3f', + 'rsrc/js/core/behavior-gesture.js' => 'fe2e0ba4', + 'rsrc/js/core/behavior-global-drag-and-drop.js' => '828a2eed', + 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', + 'rsrc/js/core/behavior-hovercard.js' => '9c808199', + 'rsrc/js/core/behavior-keyboard-pager.js' => 'b657bdf8', + 'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6', + 'rsrc/js/core/behavior-konami.js' => '5bc2cb21', + 'rsrc/js/core/behavior-lightbox-attachments.js' => '3aa45ad9', + 'rsrc/js/core/behavior-line-linker.js' => 'bc778103', + 'rsrc/js/core/behavior-more.js' => '9b9197be', + 'rsrc/js/core/behavior-object-selector.js' => 'b4eef37b', + 'rsrc/js/core/behavior-oncopy.js' => 'dab9253e', + 'rsrc/js/core/behavior-phabricator-nav.js' => 'b5842a5e', + 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'c021950a', + 'rsrc/js/core/behavior-refresh-csrf.js' => 'c4b31646', + 'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45', + 'rsrc/js/core/behavior-reveal-content.js' => '8f24abfc', + 'rsrc/js/core/behavior-search-typeahead.js' => 'f6b56f7a', + 'rsrc/js/core/behavior-select-on-click.js' => '0e34ca02', + 'rsrc/js/core/behavior-toggle-class.js' => 'a82a7769', + 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', + 'rsrc/js/core/behavior-tooltip.js' => 'e5dd1c6d', + 'rsrc/js/core/behavior-watch-anchor.js' => '06e05112', + 'rsrc/js/core/behavior-workflow.js' => '82947dda', + 'rsrc/js/core/phtize.js' => 'd254d646', + 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => 'a3e2244e', + 'rsrc/swf/aphlict.swf' => 'abac967d', + ), + 'symbols' => + array( + 'aphront-bars' => '231ac33c', + 'aphront-calendar-view-css' => '166af03d', + 'aphront-contextbar-view-css' => '1c3b0529', + 'aphront-dark-console-css' => '6378ef3d', + 'aphront-dialog-view-css' => 'dd9db96c', + 'aphront-error-view-css' => '16cd9949', + 'aphront-list-filter-view-css' => 'ef989c67', + 'aphront-multi-column-view-css' => '05bbd016', + 'aphront-notes' => '6acadd3f', + 'aphront-pager-view-css' => '2e3539af', + 'aphront-panel-view-css' => '5846dfa2', + 'aphront-request-failure-view-css' => 'da14df31', + 'aphront-table-view-css' => '92a719ca', + 'aphront-tokenizer-control-css' => 'd888465e', + 'aphront-tooltip-css' => '9c90229d', + 'aphront-two-column-view-css' => '16ab3ad2', + 'aphront-typeahead-control-css' => '00c9a200', + 'auth-css' => '1e655982', + 'config-options-css' => '7fedf08b', + 'conpherence-menu-css' => '561348ac', + 'conpherence-message-pane-css' => '2aedca89', + 'conpherence-notification-css' => 'f9ba9914', + 'conpherence-update-css' => '1099a660', + 'conpherence-widget-pane-css' => '87b12e0c', + 'differential-changeset-view-css' => '82431767', + 'differential-core-view-css' => '8135cb0c', + 'differential-inline-comment-editor' => 'f2441746', + 'differential-local-commits-view-css' => '19649019', + 'differential-results-table-css' => '239924f9', + 'differential-revision-add-comment-css' => 'c478bcaa', + 'differential-revision-comment-css' => '48186045', + 'differential-revision-comment-list-css' => 'bc291c47', + 'differential-revision-history-css' => 'f37aee8f', + 'differential-revision-list-css' => 'f3c47d33', + 'differential-table-of-contents-css' => '19566f76', + 'diffusion-commit-view-css' => '92d1e8f9', + 'diffusion-icons-css' => '384a0f7d', + 'diffusion-source-css' => '66fdf661', + 'diviner-shared-css' => 'be90f718', + 'global-drag-and-drop-css' => '697324ad', + 'herald-css' => '59d48f01', + 'herald-rule-editor' => '4366c8cc', + 'herald-test-css' => '2b7d0f54', + 'inline-comment-summary-css' => '14a91639', + 'javelin-aphlict' => '493665ee', + 'javelin-behavior' => '8a3ed18b', + 'javelin-behavior-aphlict-dropdown' => '2a2dba85', + 'javelin-behavior-aphlict-listen' => '845731b8', + 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', + 'javelin-behavior-aphront-crop' => 'b98fc918', + 'javelin-behavior-aphront-drag-and-drop-textarea' => '94d146cb', + 'javelin-behavior-aphront-form-disable-on-submit' => '27d4da3f', + 'javelin-behavior-aphront-more' => '9b9197be', + 'javelin-behavior-audio-source' => '59b251eb', + 'javelin-behavior-audit-preview' => 'be81801d', + 'javelin-behavior-balanced-payment-form' => '3b3e1664', + 'javelin-behavior-config-reorder-fields' => '69bb5094', + 'javelin-behavior-conpherence-menu' => '872bc8ff', + 'javelin-behavior-conpherence-pontificate' => '53f6f2dd', + 'javelin-behavior-conpherence-widget-pane' => 'd8ef8659', + 'javelin-behavior-countdown-timer' => '8454ce4f', + 'javelin-behavior-dark-console' => 'e9fdb5e5', + 'javelin-behavior-device' => '03d6ed07', + 'javelin-behavior-differential-accept-with-errors' => 'e12c760a', + 'javelin-behavior-differential-add-reviewers-and-ccs' => '4ba4c13d', + 'javelin-behavior-differential-comment-jump' => '71755c79', + 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', + 'javelin-behavior-differential-dropdown-menus' => '5f004630', + 'javelin-behavior-differential-edit-inline-comments' => '93f43142', + 'javelin-behavior-differential-feedback-preview' => '127f2018', + 'javelin-behavior-differential-keyboard-navigation' => 'da3e88f9', + 'javelin-behavior-differential-populate' => 'ce0c217a', + 'javelin-behavior-differential-show-all-comments' => '784b8218', + 'javelin-behavior-differential-show-field-details' => '441f2137', + 'javelin-behavior-differential-show-more' => 'dd7e8ef5', + 'javelin-behavior-differential-toggle-files' => 'ca3f91eb', + 'javelin-behavior-differential-user-select' => 'a8d8459d', + 'javelin-behavior-diffusion-commit-branches' => 'eae2f65d', + 'javelin-behavior-diffusion-commit-graph' => '85ba3cf4', + 'javelin-behavior-diffusion-jump-to' => '9db3d160', + 'javelin-behavior-diffusion-pull-lastmodified' => '3c5310da', + 'javelin-behavior-doorkeeper-tag' => 'fd27d99a', + 'javelin-behavior-error-log' => 'a5d7cf86', + 'javelin-behavior-fancy-datepicker' => '5d584426', + 'javelin-behavior-global-drag-and-drop' => '828a2eed', + 'javelin-behavior-harbormaster-reorder-steps' => 'b21125a5', + 'javelin-behavior-herald-rule-editor' => '7ebaeed3', + 'javelin-behavior-history-install' => '7ee2b591', + 'javelin-behavior-icon-composer' => 'ea38f732', + 'javelin-behavior-konami' => '5bc2cb21', + 'javelin-behavior-launch-icon-composer' => '6ec125a0', + 'javelin-behavior-lightbox-attachments' => '3aa45ad9', + 'javelin-behavior-line-chart' => 'cdcbe8a4', + 'javelin-behavior-load-blame' => '42126667', + 'javelin-behavior-maniphest-batch-editor' => '391457d7', + 'javelin-behavior-maniphest-batch-selector' => 'ead554ec', + 'javelin-behavior-maniphest-list-editor' => 'cf76cfd5', + 'javelin-behavior-maniphest-subpriority-editor' => '84845b5b', + 'javelin-behavior-maniphest-transaction-controls' => '75e50c72', + 'javelin-behavior-maniphest-transaction-expand' => '2f2e18aa', + 'javelin-behavior-maniphest-transaction-preview' => 'f8248bc5', + 'javelin-behavior-owners-path-editor' => '7a68dda3', + 'javelin-behavior-passphrase-credential-control' => '1e1c8a59', + 'javelin-behavior-persona-login' => '9414ff18', + 'javelin-behavior-phabricator-active-nav' => 'c81bc98f', + 'javelin-behavior-phabricator-autofocus' => '7319e029', + 'javelin-behavior-phabricator-busy-example' => 'fbbce3bf', + 'javelin-behavior-phabricator-file-tree' => 'c8728c70', + 'javelin-behavior-phabricator-gesture' => 'fe2e0ba4', + 'javelin-behavior-phabricator-gesture-example' => 'f42bb8c6', + 'javelin-behavior-phabricator-hovercards' => '9c808199', + 'javelin-behavior-phabricator-keyboard-pager' => 'b657bdf8', + 'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6', + 'javelin-behavior-phabricator-line-linker' => 'bc778103', + 'javelin-behavior-phabricator-nav' => 'b5842a5e', + 'javelin-behavior-phabricator-notification-example' => 'c51a6616', + 'javelin-behavior-phabricator-object-selector' => 'b4eef37b', + 'javelin-behavior-phabricator-oncopy' => 'dab9253e', + 'javelin-behavior-phabricator-remarkup-assist' => 'c021950a', + 'javelin-behavior-phabricator-reveal-content' => '8f24abfc', + 'javelin-behavior-phabricator-search-typeahead' => 'f6b56f7a', + 'javelin-behavior-phabricator-tooltips' => 'e5dd1c6d', + 'javelin-behavior-phabricator-transaction-comment-form' => '9084a36f', + 'javelin-behavior-phabricator-transaction-list' => '0dcf1716', + 'javelin-behavior-phabricator-watch-anchor' => '06e05112', + 'javelin-behavior-phame-post-preview' => '61d927ec', + 'javelin-behavior-pholio-mock-edit' => '1e1e8bb0', + 'javelin-behavior-pholio-mock-view' => '28497740', + 'javelin-behavior-phui-object-box-tabs' => 'a3e2244e', + 'javelin-behavior-policy-control' => 'c01153ea', + 'javelin-behavior-policy-rule-editor' => '263aeb8c', + 'javelin-behavior-ponder-votebox' => '327dbe61', + 'javelin-behavior-project-boards' => '1b9facd8', + 'javelin-behavior-project-create' => '065227cc', + 'javelin-behavior-refresh-csrf' => 'c4b31646', + 'javelin-behavior-releeph-preview-branch' => '9eb2cedb', + 'javelin-behavior-releeph-request-state-change' => 'fe7fc914', + 'javelin-behavior-releeph-request-typeahead' => 'cd9e7094', + 'javelin-behavior-remarkup-preview' => 'f7379f45', + 'javelin-behavior-repository-crossreference' => '8ab282be', + 'javelin-behavior-search-reorder-queries' => '34397f68', + 'javelin-behavior-select-on-click' => '0e34ca02', + 'javelin-behavior-slowvote-embed' => 'a51fdb2e', + 'javelin-behavior-stripe-payment-form' => '1693a296', + 'javelin-behavior-test-payment-form' => 'b3e5ee60', + 'javelin-behavior-toggle-class' => 'a82a7769', + 'javelin-behavior-view-placeholder' => '2fa810fc', + 'javelin-behavior-workflow' => '82947dda', + 'javelin-color' => '7e41274a', + 'javelin-cookie' => '6b3dcf44', + 'javelin-dom' => '03be94fb', + 'javelin-dynval' => '63f9ad59', + 'javelin-event' => '79473b62', + 'javelin-fx' => '54b612ba', + 'javelin-history' => 'c60f4327', + 'javelin-install' => '52a92793', + 'javelin-json' => '08e56a4e', + 'javelin-magical-init' => 'b88ab49e', + 'javelin-mask' => 'b9f26029', + 'javelin-reactor' => 'ba86e2fd', + 'javelin-reactor-dom' => 'bd3c1838', + 'javelin-reactor-node-calmer' => '4c33dff1', + 'javelin-reactornode' => '96474586', + 'javelin-request' => '23f9bb8d', + 'javelin-resource' => '356de121', + 'javelin-stratcom' => 'c293f7b9', + 'javelin-tokenizer' => 'e9e18227', + 'javelin-typeahead' => 'c22f4c01', + 'javelin-typeahead-composite-source' => 'dbd9cd11', + 'javelin-typeahead-normalizer' => '5f850b5c', + 'javelin-typeahead-ondemand-source' => '1f595fb0', + 'javelin-typeahead-preloaded-source' => 'e9b95df3', + 'javelin-typeahead-source' => 'f4412299', + 'javelin-typeahead-static-source' => 'c2b8bf64', + 'javelin-uri' => 'd9a9b862', + 'javelin-util' => '7501647b', + 'javelin-vector' => '403a3dce', + 'javelin-view' => '4641579a', + 'javelin-view-html' => '957caa12', + 'javelin-view-interpreter' => '0c33c1a0', + 'javelin-view-renderer' => '77461fd6', + 'javelin-view-visitor' => 'ca704f2b', + 'javelin-workflow' => 'd16edeae', + 'legalpad-document-css' => 'cd275275', + 'lightbox-attachment-css' => '686f8885', + 'maniphest-batch-editor' => '78444bc1', + 'maniphest-report-css' => '6fc16517', + 'maniphest-task-edit-css' => '8e23031b', + 'maniphest-task-summary-css' => '0cabd9a6', + 'multirow-row-manager' => 'e7076916', + 'owners-path-editor' => '46efd18e', + 'owners-path-editor-css' => '2f00933b', + 'paste-css' => 'aa1767d1', + 'path-typeahead' => 'f7fc67ec', + 'people-profile-css' => 'd0bababe', + 'phabricator-action-header-view-css' => 'cc654b91', + 'phabricator-action-list-view-css' => '81383e25', + 'phabricator-application-launch-view-css' => '55ba7571', + 'phabricator-busy' => '6453c869', + 'phabricator-chatlog-css' => '0cd2bc78', + 'phabricator-content-source-view-css' => '4b8b05d4', + 'phabricator-core-css' => 'da26ddb2', + 'phabricator-countdown-css' => '86b7b0a0', + 'phabricator-crumbs-view-css' => '2d9db584', + 'phabricator-drag-and-drop-file-upload' => 'ae6abfba', + 'phabricator-draggable-list' => '1681c4d4', + 'phabricator-dropdown-menu' => 'fb342e18', + 'phabricator-fatal-config-template-css' => '25d446d6', + 'phabricator-feed-css' => '0d17c209', + 'phabricator-file-upload' => '96713558', + 'phabricator-filetree-view-css' => 'a8c86ace', + 'phabricator-flag-css' => '5337623f', + 'phabricator-hovercard' => '4f344388', + 'phabricator-hovercard-view-css' => '67c12b16', + 'phabricator-jump-nav' => 'f0c5e726', + 'phabricator-keyboard-shortcut' => '1ae869f2', + 'phabricator-keyboard-shortcut-manager' => 'ad7a69ca', + 'phabricator-main-menu-view' => 'd4021e15', + 'phabricator-menu-item' => '0f386ef4', + 'phabricator-nav-view-css' => 'd0d4a509', + 'phabricator-notification' => '95944043', + 'phabricator-notification-css' => '6901121e', + 'phabricator-notification-menu-css' => 'fc9a363c', + 'phabricator-object-list-view-css' => '1a1ea560', + 'phabricator-object-selector-css' => '029a133d', + 'phabricator-phtize' => 'd254d646', + 'phabricator-prefab' => '9eaf0bfa', + 'phabricator-profile-css' => '3a7e04ca', + 'phabricator-project-tag-css' => '095c9404', + 'phabricator-remarkup-css' => 'ca7f2265', + 'phabricator-search-results-css' => 'f240504c', + 'phabricator-settings-css' => 'ea8f5915', + 'phabricator-shaped-request' => 'dfa181a4', + 'phabricator-side-menu-view-css' => '503699d0', + 'phabricator-slowvote-css' => '266df6a1', + 'phabricator-source-code-view-css' => '62a99814', + 'phabricator-standard-page-view' => '517cdfb1', + 'phabricator-textareautils' => 'b3ec3cfc', + 'phabricator-timeline-view-css' => 'f4f846c4', + 'phabricator-tooltip' => '0a81ea29', + 'phabricator-transaction-view-css' => 'ce491938', + 'phabricator-ui-example-css' => '4741b891', + 'phabricator-uiexample-javelin-view' => 'd4a14807', + 'phabricator-uiexample-reactor-button' => '44524435', + 'phabricator-uiexample-reactor-checkbox' => '7ba325ee', + 'phabricator-uiexample-reactor-focus' => '82f568cd', + 'phabricator-uiexample-reactor-input' => 'd6ca6b1c', + 'phabricator-uiexample-reactor-mouseover' => '4e37e4de', + 'phabricator-uiexample-reactor-radio' => '858f9728', + 'phabricator-uiexample-reactor-select' => '189e4fe3', + 'phabricator-uiexample-reactor-sendclass' => 'bf97561d', + 'phabricator-uiexample-reactor-sendproperties' => '551add57', + 'phabricator-welcome-page' => 'c370f13b', + 'phabricator-zindex-css' => '1bbbd4f1', + 'phame-css' => '450826e1', + 'pholio-css' => 'd23ace50', + 'pholio-edit-css' => 'b9e59b6d', + 'pholio-inline-comments-css' => '52be33f0', + 'phortune-credit-card-form' => '2290aeef', + 'phortune-credit-card-form-css' => 'b25b4beb', + 'phrequent-css' => 'ffc185ad', + 'phriction-document-css' => 'b0309d8e', + 'phui-box-css' => '21da4d8c', + 'phui-button-css' => '8106a67a', + 'phui-document-view-css' => '143b2ac8', + 'phui-feed-story-css' => '3a59c2cf', + 'phui-form-css' => 'b78ec020', + 'phui-form-view-css' => '4b2019ff', + 'phui-header-view-css' => '472a6003', + 'phui-icon-view-css' => '29e83226', + 'phui-info-panel-css' => '27ea50a1', + 'phui-list-view-css' => '2edb76cf', + 'phui-object-box-css' => '4f916b80', + 'phui-object-item-list-view-css' => 'eb579d6c', + 'phui-pinboard-view-css' => '53c5fca0', + 'phui-property-list-view-css' => 'dbf53b12', + 'phui-remarkup-preview-css' => '19ad512b', + 'phui-spacing-css' => '042804d6', + 'phui-status-list-view-css' => '2f562399', + 'phui-tag-view-css' => '295d81c4', + 'phui-text-css' => '23e9b4b7', + 'phui-workboard-view-css' => 'bf70dd2e', + 'phui-workpanel-view-css' => '6f8527f6', + 'policy-css' => '957ea14c', + 'policy-edit-css' => '05cca26a', + 'ponder-comment-table-css' => '6cdccea7', + 'ponder-feed-view-css' => 'e62615b6', + 'ponder-post-css' => 'ebab8a70', + 'ponder-vote-css' => '8ed6ed8b', + 'raphael-core' => '51ee6b43', + 'raphael-g' => '40dde778', + 'raphael-g-line' => '40da039e', + 'releeph-branch' => 'b8821d2d', + 'releeph-colors' => '2d2d6aa8', + 'releeph-core' => '140b959d', + 'releeph-intents' => '7364ac97', + 'releeph-preview-branch' => '0e383ca3', + 'releeph-project' => 'ee1f9f57', + 'releeph-request-differential-create-dialog' => '8d8b92cd', + 'releeph-request-typeahead-css' => '667a48ae', + 'releeph-status' => 'a20631d9', + 'setup-issue-css' => '1bb81c53', + 'sprite-actions-css' => '4557baf8', + 'sprite-apps-css' => 'c3857e4e', + 'sprite-apps-large-css' => '0a453d4b', + 'sprite-apps-xlarge-css' => 'db66c878', + 'sprite-conpherence-css' => '084b1f13', + 'sprite-docs-css' => '7ea4927e', + 'sprite-gradient-css' => 'a10def53', + 'sprite-icons-css' => 'ce73ef3e', + 'sprite-login-css' => 'fa00ebdc', + 'sprite-main-header-css' => 'ba949d0d', + 'sprite-menu-css' => 'fbf248ea', + 'sprite-minicons-css' => 'eb009037', + 'sprite-payments-css' => '68ec786d', + 'sprite-projects-css' => '8109f6e3', + 'sprite-status-css' => '767312f9', + 'sprite-tokens-css' => '9540e337', + 'syntax-highlighting-css' => '3c18c1cb', + 'tokens-css' => 'fb286311', + ), + 'requires' => + array( + '029a133d' => + array( + 0 => 'aphront-dialog-view-css', + ), + '03be94fb' => + array( + 0 => 'javelin-magical-init', + 1 => 'javelin-install', + 2 => 'javelin-util', + 3 => 'javelin-vector', + 4 => 'javelin-stratcom', + ), + '03d6ed07' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-install', + ), + '065227cc' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-workflow', + ), + '06e05112' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + ), + '08e56a4e' => + array( + 0 => 'javelin-install', + ), + '0a81ea29' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + ), + '0c33c1a0' => + array( + 0 => 'javelin-view', + 1 => 'javelin-install', + 2 => 'javelin-dom', + ), + '0dcf1716' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + 4 => 'javelin-fx', + ), + '0e34ca02' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '0f386ef4' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + ), + '127f2018' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-request', + 4 => 'javelin-util', + 5 => 'phabricator-shaped-request', + ), + '1681c4d4' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-util', + 4 => 'javelin-vector', + 5 => 'javelin-magical-init', + ), + '1693a296' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'phortune-credit-card-form', + ), + '189e4fe3' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + '1ae869f2' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'phabricator-keyboard-shortcut-manager', + ), + '1b9facd8' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-stratcom', + 4 => 'javelin-workflow', + 5 => 'phabricator-draggable-list', + ), + '1e1c8a59' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-workflow', + 4 => 'javelin-util', + 5 => 'javelin-uri', + ), + '1e1e8bb0' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-workflow', + 4 => 'phabricator-phtize', + 5 => 'phabricator-drag-and-drop-file-upload', + 6 => 'phabricator-draggable-list', + ), + '1f595fb0' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-request', + 3 => 'javelin-typeahead-source', + ), + '2290aeef' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-json', + 3 => 'javelin-workflow', + 4 => 'javelin-util', + ), + '23f9bb8d' => + array( + 0 => 'javelin-install', + 1 => 'javelin-stratcom', + 2 => 'javelin-util', + 3 => 'javelin-behavior', + 4 => 'javelin-json', + 5 => 'javelin-dom', + 6 => 'javelin-resource', + ), + '263aeb8c' => + array( + 0 => 'javelin-behavior', + 1 => 'multirow-row-manager', + 2 => 'javelin-dom', + 3 => 'javelin-util', + 4 => 'phabricator-prefab', + 5 => 'javelin-tokenizer', + 6 => 'javelin-typeahead', + 7 => 'javelin-typeahead-preloaded-source', + 8 => 'javelin-json', + ), + '27d4da3f' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '2a2dba85' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-request', + 2 => 'javelin-stratcom', + 3 => 'javelin-vector', + 4 => 'javelin-dom', + 5 => 'javelin-uri', + 6 => 'javelin-behavior-device', + ), + '2f2e18aa' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-workflow', + 3 => 'javelin-stratcom', + ), + '2fa810fc' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-view-renderer', + 3 => 'javelin-install', + ), + '327dbe61' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-stratcom', + 4 => 'javelin-request', + ), + '34397f68' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + 4 => 'phabricator-draggable-list', + ), + '356de121' => + array( + 0 => 'javelin-util', + 1 => 'javelin-uri', + 2 => 'javelin-install', + ), + '391457d7' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-prefab', + 4 => 'multirow-row-manager', + 5 => 'javelin-json', + ), + '3aa45ad9' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-mask', + 4 => 'javelin-util', + 5 => 'phabricator-busy', + ), + '3b3e1664' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'phortune-credit-card-form', + ), + '3c5310da' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-request', + ), + '403a3dce' => + array( + 0 => 'javelin-install', + 1 => 'javelin-event', + ), + '4366c8cc' => + array( + 0 => 'multirow-row-manager', + 1 => 'javelin-install', + 2 => 'javelin-typeahead', + 3 => 'javelin-util', + 4 => 'javelin-dom', + 5 => 'javelin-tokenizer', + 6 => 'javelin-typeahead-preloaded-source', + 7 => 'javelin-stratcom', + 8 => 'javelin-json', + 9 => 'phabricator-prefab', + ), + '441f2137' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '4641579a' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + ), + '46efd18e' => + array( + 0 => 'multirow-row-manager', + 1 => 'javelin-install', + 2 => 'path-typeahead', + 3 => 'javelin-dom', + 4 => 'javelin-util', + 5 => 'phabricator-prefab', + ), + '493665ee' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + ), + '4ba4c13d' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'phabricator-prefab', + ), + '4c33dff1' => + array( + 0 => 'javelin-install', + 1 => 'javelin-reactor', + 2 => 'javelin-util', + ), + '4e37e4de' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + '4f344388' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + 3 => 'javelin-request', + 4 => 'javelin-uri', + ), + '52a92793' => + array( + 0 => 'javelin-util', + 1 => 'javelin-magical-init', + ), + '53f6f2dd' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-workflow', + 4 => 'javelin-stratcom', + ), + '54b612ba' => + array( + 0 => 'javelin-color', + 1 => 'javelin-install', + 2 => 'javelin-util', + ), + '551add57' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + '59b251eb' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-vector', + 3 => 'javelin-dom', + ), + '5bc2cb21' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + ), + '5d584426' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-stratcom', + 4 => 'javelin-vector', + ), + '5f004630' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-stratcom', + 4 => 'phabricator-dropdown-menu', + 5 => 'phabricator-menu-item', + 6 => 'phabricator-phtize', + ), + '5f850b5c' => + array( + 0 => 'javelin-install', + ), + '61d927ec' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-shaped-request', + ), + '63f9ad59' => + array( + 0 => 'javelin-install', + 1 => 'javelin-reactornode', + 2 => 'javelin-util', + 3 => 'javelin-reactor', + ), + '6453c869' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-fx', + ), + '69bb5094' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-json', + 4 => 'phabricator-draggable-list', + ), + '6b3dcf44' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + ), + '6ec125a0' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-workflow', + ), + '71755c79' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '7319e029' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), + '75e50c72' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'phabricator-prefab', + ), + '77461fd6' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + ), + '784b8218' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '79473b62' => + array( + 0 => 'javelin-install', + ), + '7a68dda3' => + array( + 0 => 'owners-path-editor', + 1 => 'javelin-behavior', + ), + '7ba325ee' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + '7e41274a' => + array( + 0 => 'javelin-install', + ), + '7ebaeed3' => + array( + 0 => 'herald-rule-editor', + 1 => 'javelin-behavior', + ), + '7ee2b591' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-history', + ), + '828a2eed' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-uri', + 3 => 'javelin-mask', + 4 => 'phabricator-drag-and-drop-file-upload', + ), + '82947dda' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + ), + '82f568cd' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + '8454ce4f' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), + '845731b8' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-aphlict', + 2 => 'javelin-stratcom', + 3 => 'javelin-request', + 4 => 'javelin-uri', + 5 => 'javelin-dom', + 6 => 'javelin-json', + 7 => 'phabricator-notification', + ), + '84845b5b' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-workflow', + 4 => 'phabricator-draggable-list', + ), + '858f9728' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + '85ba3cf4' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + ), + '872bc8ff' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-stratcom', + 4 => 'javelin-workflow', + 5 => 'javelin-behavior-device', + 6 => 'javelin-history', + 7 => 'javelin-vector', + ), + '8a3ed18b' => + array( + 0 => 'javelin-magical-init', + 1 => 'javelin-util', + ), + '8ab282be' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-uri', + ), + '8f24abfc' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '9084a36f' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-fx', + 4 => 'javelin-request', + 5 => 'phabricator-shaped-request', + ), + '93f43142' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-util', + 4 => 'javelin-vector', + 5 => 'differential-inline-comment-editor', + ), + '9414ff18' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-resource', + 2 => 'javelin-stratcom', + 3 => 'javelin-workflow', + 4 => 'javelin-util', + ), + '94d146cb' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'phabricator-drag-and-drop-file-upload', + 3 => 'phabricator-textareautils', + ), + '957caa12' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-view-visitor', + 3 => 'javelin-util', + ), + '9b9197be' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + '9c808199' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-behavior-device', + 2 => 'javelin-stratcom', + 3 => 'javelin-vector', + 4 => 'phabricator-hovercard', + ), + '9db3d160' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-vector', + 2 => 'javelin-dom', + ), + '9eaf0bfa' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-typeahead', + 4 => 'javelin-tokenizer', + 5 => 'javelin-typeahead-preloaded-source', + 6 => 'javelin-typeahead-ondemand-source', + 7 => 'javelin-dom', + 8 => 'javelin-stratcom', + 9 => 'javelin-util', + ), + '9eb2cedb' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-uri', + 3 => 'javelin-request', + ), + 'a3e2244e' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + 'a51fdb2e' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-request', + 2 => 'javelin-stratcom', + 3 => 'javelin-dom', + ), + 'a5d7cf86' => + array( + 0 => 'javelin-dom', + ), + 'a82a7769' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + 'a8d8459d' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + ), + 'ad7a69ca' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + 3 => 'javelin-dom', + 4 => 'javelin-vector', + ), + 'ae6abfba' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-request', + 3 => 'javelin-dom', + 4 => 'javelin-uri', + 5 => 'phabricator-file-upload', + ), + 'b21125a5' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + 4 => 'phabricator-draggable-list', + ), + 'b3a4b884' => + array( + 0 => 'javelin-behavior', + 1 => 'phabricator-prefab', + ), + 'b3e5ee60' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'phortune-credit-card-form', + ), + 'b3ec3cfc' => + array( + 0 => 'javelin-install', + ), + 'b4eef37b' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-request', + 3 => 'javelin-util', + ), + 'b5842a5e' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-behavior-device', + 2 => 'javelin-stratcom', + 3 => 'javelin-dom', + 4 => 'javelin-magical-init', + 5 => 'javelin-vector', + 6 => 'javelin-request', + 7 => 'javelin-util', + ), + 'b657bdf8' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-uri', + 2 => 'phabricator-keyboard-shortcut', + ), + 'b98fc918' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + 3 => 'javelin-magical-init', + ), + 'b9f26029' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + ), + 'ba86e2fd' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + ), + 'bc778103' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-history', + ), + 'bd3c1838' => + array( + 0 => 'javelin-dom', + 1 => 'javelin-dynval', + 2 => 'javelin-reactor', + 3 => 'javelin-reactornode', + 4 => 'javelin-install', + 5 => 'javelin-util', + ), + 'be81801d' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-shaped-request', + ), + 'bf97561d' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-reactor-dom', + ), + 'c01153ea' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-dropdown-menu', + 4 => 'phabricator-menu-item', + 5 => 'javelin-workflow', + ), + 'c021950a' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'phabricator-phtize', + 4 => 'phabricator-textareautils', + 5 => 'javelin-workflow', + 6 => 'javelin-vector', + ), + 'c22f4c01' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + 3 => 'javelin-util', + ), + 'c293f7b9' => + array( + 0 => 'javelin-install', + 1 => 'javelin-event', + 2 => 'javelin-util', + 3 => 'javelin-magical-init', + ), + 'c2b8bf64' => + array( + 0 => 'javelin-install', + 1 => 'javelin-typeahead-source', + ), + 'c4b31646' => + array( + 0 => 'javelin-request', + 1 => 'javelin-behavior', + 2 => 'javelin-dom', + 3 => 'phabricator-busy', + ), + 'c51a6616' => + array( + 0 => 'phabricator-notification', + 1 => 'javelin-stratcom', + 2 => 'javelin-behavior', + ), + 'c60f4327' => + array( + 0 => 'javelin-stratcom', + 1 => 'javelin-install', + 2 => 'javelin-uri', + 3 => 'javelin-util', + ), + 'c81bc98f' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-vector', + 3 => 'javelin-dom', + 4 => 'javelin-uri', + ), + 'c8728c70' => + array( + 0 => 'javelin-behavior', + 1 => 'phabricator-keyboard-shortcut', + 2 => 'javelin-stratcom', + ), + 'ca3f91eb' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'phabricator-phtize', + ), + 'ca704f2b' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + ), + 'cd9e7094' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-typeahead', + 3 => 'javelin-typeahead-ondemand-source', + 4 => 'javelin-dom', + ), + 'cdcbe8a4' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + ), + 'ce0c217a' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-workflow', + 2 => 'javelin-util', + 3 => 'javelin-dom', + 4 => 'javelin-stratcom', + 5 => 'javelin-behavior-device', + 6 => 'javelin-vector', + 7 => 'phabricator-tooltip', + ), + 'cf76cfd5' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-workflow', + 4 => 'javelin-fx', + 5 => 'javelin-util', + ), + 'd16edeae' => + array( + 0 => 'javelin-stratcom', + 1 => 'javelin-request', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-install', + 5 => 'javelin-util', + 6 => 'javelin-mask', + 7 => 'javelin-uri', + ), + 'd254d646' => + array( + 0 => 'javelin-util', + ), + 'd4a14807' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-view', + ), + 'd6ca6b1c' => + array( + 0 => 'javelin-install', + 1 => 'javelin-reactor-dom', + 2 => 'javelin-view-html', + 3 => 'javelin-view-interpreter', + 4 => 'javelin-view-renderer', + ), + 'd75709e6' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-workflow', + 2 => 'javelin-json', + 3 => 'javelin-dom', + 4 => 'phabricator-keyboard-shortcut', + ), + 'd888465e' => + array( + 0 => 'aphront-typeahead-control-css', + ), + 'd8ef8659' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-workflow', + 4 => 'javelin-util', + 5 => 'phabricator-notification', + 6 => 'javelin-behavior-device', + 7 => 'phabricator-dropdown-menu', + 8 => 'phabricator-menu-item', + ), + 'd9a9b862' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + ), + 'da3e88f9' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'phabricator-keyboard-shortcut', + ), + 'dab9253e' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), + 'dbd9cd11' => + array( + 0 => 'javelin-install', + 1 => 'javelin-typeahead-source', + 2 => 'javelin-util', + ), + 'dd7e8ef5' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-workflow', + 3 => 'javelin-util', + 4 => 'javelin-stratcom', + ), + 'dfa181a4' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-request', + ), + 'e12c760a' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), + 'e1ff79b1' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), + 'e5dd1c6d' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-behavior-device', + 2 => 'javelin-stratcom', + 3 => 'phabricator-tooltip', + ), + 'e7076916' => + array( + 0 => 'javelin-install', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'javelin-util', + ), + 'e9b95df3' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-request', + 3 => 'javelin-typeahead-source', + ), + 'e9e18227' => + array( + 0 => 'javelin-dom', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + 3 => 'javelin-install', + ), + 'e9fdb5e5' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-util', + 3 => 'javelin-dom', + 4 => 'javelin-request', + 5 => 'phabricator-keyboard-shortcut', + ), + 'ea38f732' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + ), + 'ead554ec' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-util', + ), + 'eae2f65d' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-request', + ), + 'f2441746' => + array( + 0 => 'javelin-dom', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + 3 => 'javelin-install', + 4 => 'javelin-request', + 5 => 'javelin-workflow', + ), + 'f42bb8c6' => + array( + 0 => 'javelin-stratcom', + 1 => 'javelin-behavior', + 2 => 'javelin-vector', + 3 => 'javelin-dom', + ), + 'f4412299' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-typeahead-normalizer', + ), + 'f6b56f7a' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-typeahead-ondemand-source', + 2 => 'javelin-typeahead', + 3 => 'javelin-dom', + 4 => 'javelin-uri', + 5 => 'javelin-util', + 6 => 'javelin-stratcom', + ), + 'f7379f45' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-shaped-request', + ), + 'f7fc67ec' => + array( + 0 => 'javelin-install', + 1 => 'javelin-typeahead', + 2 => 'javelin-dom', + 3 => 'javelin-request', + 4 => 'javelin-typeahead-ondemand-source', + 5 => 'javelin-util', + ), + 'f8248bc5' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-json', + 4 => 'javelin-stratcom', + 5 => 'phabricator-shaped-request', + ), + 'fb342e18' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-stratcom', + 5 => 'phabricator-menu-item', + ), + 'fbbce3bf' => + array( + 0 => 'phabricator-busy', + 1 => 'javelin-behavior', + ), + 'fd27d99a' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-json', + 3 => 'javelin-workflow', + 4 => 'javelin-magical-init', + ), + 'fe2e0ba4' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-behavior-device', + 2 => 'javelin-stratcom', + 3 => 'javelin-vector', + 4 => 'javelin-dom', + 5 => 'javelin-magical-init', + ), + 'fe7fc914' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-request', + 4 => 'phabricator-keyboard-shortcut', + 5 => 'phabricator-notification', + ), + 28497740 => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + 3 => 'javelin-dom', + 4 => 'javelin-vector', + 5 => 'javelin-magical-init', + 6 => 'javelin-request', + 7 => 'javelin-history', + 8 => 'javelin-workflow', + 9 => 'javelin-mask', + 10 => 'javelin-behavior-device', + 11 => 'phabricator-keyboard-shortcut', + ), + 42126667 => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-request', + ), + 44524435 => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-dynval', + 4 => 'javelin-reactor-dom', + ), + 95944043 => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-util', + 4 => 'phabricator-notification-css', + ), + 96474586 => + array( + 0 => 'javelin-install', + 1 => 'javelin-reactor', + 2 => 'javelin-util', + 3 => 'javelin-reactor-node-calmer', + ), + 96713558 => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'phabricator-notification', + ), + ), + 'packages' => + array( + 'core.pkg.css' => + array( + 0 => 'phabricator-core-css', + 1 => 'phabricator-zindex-css', + 2 => 'phui-button-css', + 3 => 'phabricator-standard-page-view', + 4 => 'aphront-dialog-view-css', + 5 => 'phui-form-view-css', + 6 => 'aphront-panel-view-css', + 7 => 'aphront-table-view-css', + 8 => 'aphront-tokenizer-control-css', + 9 => 'aphront-typeahead-control-css', + 10 => 'aphront-list-filter-view-css', + 11 => 'phabricator-jump-nav', + 12 => 'phabricator-remarkup-css', + 13 => 'syntax-highlighting-css', + 14 => 'aphront-pager-view-css', + 15 => 'phabricator-transaction-view-css', + 16 => 'aphront-tooltip-css', + 17 => 'phabricator-flag-css', + 18 => 'aphront-error-view-css', + 19 => 'sprite-icons-css', + 20 => 'sprite-gradient-css', + 21 => 'sprite-menu-css', + 22 => 'sprite-apps-large-css', + 23 => 'sprite-status-css', + 24 => 'phabricator-main-menu-view', + 25 => 'phabricator-notification-css', + 26 => 'phabricator-notification-menu-css', + 27 => 'lightbox-attachment-css', + 28 => 'phui-header-view-css', + 29 => 'phabricator-filetree-view-css', + 30 => 'phabricator-nav-view-css', + 31 => 'phabricator-side-menu-view-css', + 32 => 'phabricator-crumbs-view-css', + 33 => 'phui-object-item-list-view-css', + 34 => 'global-drag-and-drop-css', + 35 => 'phui-spacing-css', + 36 => 'phui-form-css', + 37 => 'phui-icon-view-css', + 38 => 'phabricator-application-launch-view-css', + 39 => 'phabricator-action-list-view-css', + 40 => 'phui-property-list-view-css', + 41 => 'phui-tag-view-css', + 42 => 'phui-list-view-css', + ), + 'core.pkg.js' => + array( + 0 => 'javelin-behavior-aphront-basic-tokenizer', + 1 => 'javelin-behavior-workflow', + 2 => 'javelin-behavior-aphront-form-disable-on-submit', + 3 => 'phabricator-keyboard-shortcut-manager', + 4 => 'phabricator-keyboard-shortcut', + 5 => 'javelin-behavior-phabricator-keyboard-shortcuts', + 6 => 'javelin-behavior-refresh-csrf', + 7 => 'javelin-behavior-phabricator-watch-anchor', + 8 => 'javelin-behavior-phabricator-autofocus', + 9 => 'phabricator-menu-item', + 10 => 'phabricator-dropdown-menu', + 11 => 'phabricator-phtize', + 12 => 'javelin-behavior-phabricator-oncopy', + 13 => 'phabricator-tooltip', + 14 => 'javelin-behavior-phabricator-tooltips', + 15 => 'phabricator-prefab', + 16 => 'javelin-behavior-device', + 17 => 'javelin-behavior-toggle-class', + 18 => 'javelin-behavior-lightbox-attachments', + 19 => 'phabricator-busy', + 20 => 'javelin-aphlict', + 21 => 'phabricator-notification', + 22 => 'javelin-behavior-aphlict-listen', + 23 => 'javelin-behavior-phabricator-search-typeahead', + 24 => 'javelin-behavior-konami', + 25 => 'javelin-behavior-aphlict-dropdown', + 26 => 'javelin-behavior-history-install', + 27 => 'javelin-behavior-phabricator-gesture', + 28 => 'javelin-behavior-phabricator-active-nav', + 29 => 'javelin-behavior-phabricator-nav', + 30 => 'javelin-behavior-phabricator-remarkup-assist', + 31 => 'phabricator-textareautils', + 32 => 'phabricator-file-upload', + 33 => 'javelin-behavior-global-drag-and-drop', + 34 => 'javelin-behavior-phabricator-reveal-content', + 35 => 'phabricator-hovercard', + 36 => 'javelin-behavior-phabricator-hovercards', + 37 => 'javelin-color', + 38 => 'javelin-fx', + ), + 'darkconsole.pkg.js' => + array( + 0 => 'javelin-behavior-dark-console', + 1 => 'javelin-behavior-error-log', + ), + 'differential.pkg.css' => + array( + 0 => 'differential-core-view-css', + 1 => 'differential-changeset-view-css', + 2 => 'differential-results-table-css', + 3 => 'differential-revision-history-css', + 4 => 'differential-revision-list-css', + 5 => 'differential-table-of-contents-css', + 6 => 'differential-revision-comment-css', + 7 => 'differential-revision-add-comment-css', + 8 => 'differential-revision-comment-list-css', + 9 => 'phabricator-object-selector-css', + 10 => 'phabricator-content-source-view-css', + 11 => 'differential-local-commits-view-css', + 12 => 'inline-comment-summary-css', + ), + 'differential.pkg.js' => + array( + 0 => 'phabricator-drag-and-drop-file-upload', + 1 => 'phabricator-shaped-request', + 2 => 'javelin-behavior-differential-feedback-preview', + 3 => 'javelin-behavior-differential-edit-inline-comments', + 4 => 'javelin-behavior-differential-populate', + 5 => 'javelin-behavior-differential-show-more', + 6 => 'javelin-behavior-differential-diff-radios', + 7 => 'javelin-behavior-differential-accept-with-errors', + 8 => 'javelin-behavior-differential-comment-jump', + 9 => 'javelin-behavior-differential-add-reviewers-and-ccs', + 10 => 'javelin-behavior-differential-keyboard-navigation', + 11 => 'javelin-behavior-aphront-drag-and-drop-textarea', + 12 => 'javelin-behavior-phabricator-object-selector', + 13 => 'javelin-behavior-repository-crossreference', + 14 => 'javelin-behavior-load-blame', + 15 => 'differential-inline-comment-editor', + 16 => 'javelin-behavior-differential-dropdown-menus', + 17 => 'javelin-behavior-differential-toggle-files', + 18 => 'javelin-behavior-differential-user-select', + ), + 'diffusion.pkg.css' => + array( + 0 => 'diffusion-commit-view-css', + 1 => 'diffusion-icons-css', + ), + 'diffusion.pkg.js' => + array( + 0 => 'javelin-behavior-diffusion-pull-lastmodified', + 1 => 'javelin-behavior-diffusion-commit-graph', + 2 => 'javelin-behavior-audit-preview', + ), + 'javelin.pkg.js' => + array( + 0 => 'javelin-util', + 1 => 'javelin-install', + 2 => 'javelin-event', + 3 => 'javelin-stratcom', + 4 => 'javelin-behavior', + 5 => 'javelin-resource', + 6 => 'javelin-request', + 7 => 'javelin-vector', + 8 => 'javelin-dom', + 9 => 'javelin-json', + 10 => 'javelin-uri', + 11 => 'javelin-workflow', + 12 => 'javelin-mask', + 13 => 'javelin-typeahead', + 14 => 'javelin-typeahead-normalizer', + 15 => 'javelin-typeahead-source', + 16 => 'javelin-typeahead-preloaded-source', + 17 => 'javelin-typeahead-ondemand-source', + 18 => 'javelin-tokenizer', + 19 => 'javelin-history', + ), + 'maniphest.pkg.css' => + array( + 0 => 'maniphest-task-summary-css', + 1 => 'phabricator-project-tag-css', + ), + 'maniphest.pkg.js' => + array( + 0 => 'javelin-behavior-maniphest-batch-selector', + 1 => 'javelin-behavior-maniphest-transaction-controls', + 2 => 'javelin-behavior-maniphest-transaction-preview', + 3 => 'javelin-behavior-maniphest-transaction-expand', + 4 => 'javelin-behavior-maniphest-subpriority-editor', + ), + ), +); diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php new file mode 100644 index 0000000000..08abd1a186 --- /dev/null +++ b/resources/celerity/packages.php @@ -0,0 +1,180 @@ + array( + 'javelin-util', + 'javelin-install', + 'javelin-event', + 'javelin-stratcom', + 'javelin-behavior', + 'javelin-resource', + 'javelin-request', + 'javelin-vector', + 'javelin-dom', + 'javelin-json', + 'javelin-uri', + 'javelin-workflow', + 'javelin-mask', + 'javelin-typeahead', + 'javelin-typeahead-normalizer', + 'javelin-typeahead-source', + 'javelin-typeahead-preloaded-source', + 'javelin-typeahead-ondemand-source', + 'javelin-tokenizer', + 'javelin-history', + ), + 'core.pkg.js' => array( + 'javelin-behavior-aphront-basic-tokenizer', + 'javelin-behavior-workflow', + 'javelin-behavior-aphront-form-disable-on-submit', + 'phabricator-keyboard-shortcut-manager', + 'phabricator-keyboard-shortcut', + 'javelin-behavior-phabricator-keyboard-shortcuts', + 'javelin-behavior-refresh-csrf', + 'javelin-behavior-phabricator-watch-anchor', + 'javelin-behavior-phabricator-autofocus', + 'phabricator-menu-item', + 'phabricator-dropdown-menu', + 'phabricator-phtize', + 'javelin-behavior-phabricator-oncopy', + 'phabricator-tooltip', + 'javelin-behavior-phabricator-tooltips', + 'phabricator-prefab', + 'javelin-behavior-device', + 'javelin-behavior-toggle-class', + 'javelin-behavior-lightbox-attachments', + 'phabricator-busy', + 'javelin-aphlict', + 'phabricator-notification', + 'javelin-behavior-aphlict-listen', + 'javelin-behavior-phabricator-search-typeahead', + 'javelin-behavior-konami', + 'javelin-behavior-aphlict-dropdown', + 'javelin-behavior-history-install', + 'javelin-behavior-phabricator-gesture', + + 'javelin-behavior-phabricator-active-nav', + 'javelin-behavior-phabricator-nav', + 'javelin-behavior-phabricator-remarkup-assist', + 'phabricator-textareautils', + 'phabricator-file-upload', + 'javelin-behavior-global-drag-and-drop', + 'javelin-behavior-phabricator-reveal-content', + 'phabricator-hovercard', + 'javelin-behavior-phabricator-hovercards', + 'javelin-color', + 'javelin-fx', + ), + 'core.pkg.css' => array( + 'phabricator-core-css', + 'phabricator-zindex-css', + 'phui-button-css', + 'phabricator-standard-page-view', + 'aphront-dialog-view-css', + 'phui-form-view-css', + 'aphront-panel-view-css', + 'aphront-table-view-css', + 'aphront-tokenizer-control-css', + 'aphront-typeahead-control-css', + 'aphront-list-filter-view-css', + + 'phabricator-jump-nav', + + 'phabricator-remarkup-css', + 'syntax-highlighting-css', + 'aphront-pager-view-css', + 'phabricator-transaction-view-css', + 'aphront-tooltip-css', + 'phabricator-flag-css', + 'aphront-error-view-css', + + 'sprite-icons-css', + 'sprite-gradient-css', + 'sprite-menu-css', + 'sprite-apps-large-css', + 'sprite-status-css', + + 'phabricator-main-menu-view', + 'phabricator-notification-css', + 'phabricator-notification-menu-css', + 'lightbox-attachment-css', + 'phui-header-view-css', + 'phabricator-filetree-view-css', + 'phabricator-nav-view-css', + 'phabricator-side-menu-view-css', + 'phabricator-crumbs-view-css', + 'phui-object-item-list-view-css', + 'global-drag-and-drop-css', + 'phui-spacing-css', + 'phui-form-css', + 'phui-icon-view-css', + + 'phabricator-application-launch-view-css', + 'phabricator-action-list-view-css', + 'phui-property-list-view-css', + 'phui-tag-view-css', + 'phui-list-view-css', + ), + 'differential.pkg.css' => array( + 'differential-core-view-css', + 'differential-changeset-view-css', + 'differential-results-table-css', + 'differential-revision-history-css', + 'differential-revision-list-css', + 'differential-table-of-contents-css', + 'differential-revision-comment-css', + 'differential-revision-add-comment-css', + 'differential-revision-comment-list-css', + 'phabricator-object-selector-css', + 'phabricator-content-source-view-css', + 'differential-local-commits-view-css', + 'inline-comment-summary-css', + ), + 'differential.pkg.js' => array( + 'phabricator-drag-and-drop-file-upload', + 'phabricator-shaped-request', + + 'javelin-behavior-differential-feedback-preview', + 'javelin-behavior-differential-edit-inline-comments', + 'javelin-behavior-differential-populate', + 'javelin-behavior-differential-show-more', + 'javelin-behavior-differential-diff-radios', + 'javelin-behavior-differential-accept-with-errors', + 'javelin-behavior-differential-comment-jump', + 'javelin-behavior-differential-add-reviewers-and-ccs', + 'javelin-behavior-differential-keyboard-navigation', + 'javelin-behavior-aphront-drag-and-drop-textarea', + 'javelin-behavior-phabricator-object-selector', + 'javelin-behavior-repository-crossreference', + 'javelin-behavior-load-blame', + + 'differential-inline-comment-editor', + 'javelin-behavior-differential-dropdown-menus', + 'javelin-behavior-differential-toggle-files', + 'javelin-behavior-differential-user-select', + ), + 'diffusion.pkg.css' => array( + 'diffusion-commit-view-css', + 'diffusion-icons-css', + ), + 'diffusion.pkg.js' => array( + 'javelin-behavior-diffusion-pull-lastmodified', + 'javelin-behavior-diffusion-commit-graph', + 'javelin-behavior-audit-preview', + ), + 'maniphest.pkg.css' => array( + 'maniphest-task-summary-css', + 'phabricator-project-tag-css', + ), + 'maniphest.pkg.js' => array( + 'javelin-behavior-maniphest-batch-selector', + 'javelin-behavior-maniphest-transaction-controls', + 'javelin-behavior-maniphest-transaction-preview', + 'javelin-behavior-maniphest-transaction-expand', + 'javelin-behavior-maniphest-subpriority-editor', + ), + 'darkconsole.pkg.js' => array( + 'javelin-behavior-dark-console', + 'javelin-behavior-error-log', + ), +); diff --git a/resources/sprite/apps_blue_2x/authentication.png b/resources/sprite/apps_blue_2x/authentication.png index ba88acb776..37fb4743d2 100644 Binary files a/resources/sprite/apps_blue_2x/authentication.png and b/resources/sprite/apps_blue_2x/authentication.png differ diff --git a/resources/sprite/apps_blue_2x/fancyhome.png b/resources/sprite/apps_blue_2x/fancyhome.png new file mode 100644 index 0000000000..8d3e67194b Binary files /dev/null and b/resources/sprite/apps_blue_2x/fancyhome.png differ diff --git a/resources/sprite/apps_blue_2x/harbormaster.png b/resources/sprite/apps_blue_2x/harbormaster.png index 6fd75277b1..716116bbfb 100644 Binary files a/resources/sprite/apps_blue_2x/harbormaster.png and b/resources/sprite/apps_blue_2x/harbormaster.png differ diff --git a/resources/sprite/apps_blue_2x/info-sm.png b/resources/sprite/apps_blue_2x/info-sm.png new file mode 100644 index 0000000000..a4afe27f39 Binary files /dev/null and b/resources/sprite/apps_blue_2x/info-sm.png differ diff --git a/resources/sprite/apps_blue_2x/logout-sm.png b/resources/sprite/apps_blue_2x/logout-sm.png new file mode 100644 index 0000000000..07b12ac84c Binary files /dev/null and b/resources/sprite/apps_blue_2x/logout-sm.png differ diff --git a/resources/sprite/apps_blue_2x/new-sm.png b/resources/sprite/apps_blue_2x/new-sm.png new file mode 100644 index 0000000000..6dfcd53acf Binary files /dev/null and b/resources/sprite/apps_blue_2x/new-sm.png differ diff --git a/resources/sprite/apps_blue_2x/passphrase.png b/resources/sprite/apps_blue_2x/passphrase.png new file mode 100644 index 0000000000..fc13b1c7f2 Binary files /dev/null and b/resources/sprite/apps_blue_2x/passphrase.png differ diff --git a/resources/sprite/apps_blue_2x/phortune.png b/resources/sprite/apps_blue_2x/phortune.png index 66e19a2d3b..1fa2eee715 100644 Binary files a/resources/sprite/apps_blue_2x/phortune.png and b/resources/sprite/apps_blue_2x/phortune.png differ diff --git a/resources/sprite/apps_blue_2x/phragment.png b/resources/sprite/apps_blue_2x/phragment.png new file mode 100644 index 0000000000..d514b62e96 Binary files /dev/null and b/resources/sprite/apps_blue_2x/phragment.png differ diff --git a/resources/sprite/apps_blue_2x/settings-sm.png b/resources/sprite/apps_blue_2x/settings-sm.png new file mode 100644 index 0000000000..544bebf018 Binary files /dev/null and b/resources/sprite/apps_blue_2x/settings-sm.png differ diff --git a/resources/sprite/apps_blue_4x/authentication.png b/resources/sprite/apps_blue_4x/authentication.png index 6ba71068b7..4054d0e26a 100644 Binary files a/resources/sprite/apps_blue_4x/authentication.png and b/resources/sprite/apps_blue_4x/authentication.png differ diff --git a/resources/sprite/apps_blue_4x/fancyhome.png b/resources/sprite/apps_blue_4x/fancyhome.png new file mode 100644 index 0000000000..7a466de22a Binary files /dev/null and b/resources/sprite/apps_blue_4x/fancyhome.png differ diff --git a/resources/sprite/apps_blue_4x/info-sm.png b/resources/sprite/apps_blue_4x/info-sm.png new file mode 100644 index 0000000000..dbe735330d Binary files /dev/null and b/resources/sprite/apps_blue_4x/info-sm.png differ diff --git a/resources/sprite/apps_blue_4x/logout-sm.png b/resources/sprite/apps_blue_4x/logout-sm.png new file mode 100644 index 0000000000..c05e466f37 Binary files /dev/null and b/resources/sprite/apps_blue_4x/logout-sm.png differ diff --git a/resources/sprite/apps_blue_4x/new-sm.png b/resources/sprite/apps_blue_4x/new-sm.png new file mode 100644 index 0000000000..806740eccd Binary files /dev/null and b/resources/sprite/apps_blue_4x/new-sm.png differ diff --git a/resources/sprite/apps_blue_4x/passphrase.png b/resources/sprite/apps_blue_4x/passphrase.png new file mode 100644 index 0000000000..31d260b8af Binary files /dev/null and b/resources/sprite/apps_blue_4x/passphrase.png differ diff --git a/resources/sprite/apps_blue_4x/phragment.png b/resources/sprite/apps_blue_4x/phragment.png new file mode 100644 index 0000000000..c1ace08897 Binary files /dev/null and b/resources/sprite/apps_blue_4x/phragment.png differ diff --git a/resources/sprite/apps_blue_4x/settings-sm.png b/resources/sprite/apps_blue_4x/settings-sm.png new file mode 100644 index 0000000000..6f4027c0f8 Binary files /dev/null and b/resources/sprite/apps_blue_4x/settings-sm.png differ diff --git a/resources/sprite/apps_dark_1x/authentication.png b/resources/sprite/apps_dark_1x/authentication.png index ae66f339e8..d6310087a1 100644 Binary files a/resources/sprite/apps_dark_1x/authentication.png and b/resources/sprite/apps_dark_1x/authentication.png differ diff --git a/resources/sprite/apps_dark_1x/fancyhome.png b/resources/sprite/apps_dark_1x/fancyhome.png new file mode 100644 index 0000000000..5d8aec9bf0 Binary files /dev/null and b/resources/sprite/apps_dark_1x/fancyhome.png differ diff --git a/resources/sprite/apps_dark_1x/info-sm.png b/resources/sprite/apps_dark_1x/info-sm.png new file mode 100644 index 0000000000..f395952dc7 Binary files /dev/null and b/resources/sprite/apps_dark_1x/info-sm.png differ diff --git a/resources/sprite/apps_dark_1x/logout-sm.png b/resources/sprite/apps_dark_1x/logout-sm.png new file mode 100644 index 0000000000..dcf8f8588f Binary files /dev/null and b/resources/sprite/apps_dark_1x/logout-sm.png differ diff --git a/resources/sprite/apps_dark_1x/new-sm.png b/resources/sprite/apps_dark_1x/new-sm.png new file mode 100644 index 0000000000..780a92a2e3 Binary files /dev/null and b/resources/sprite/apps_dark_1x/new-sm.png differ diff --git a/resources/sprite/apps_dark_1x/passphrase.png b/resources/sprite/apps_dark_1x/passphrase.png new file mode 100644 index 0000000000..1b279d8adb Binary files /dev/null and b/resources/sprite/apps_dark_1x/passphrase.png differ diff --git a/resources/sprite/apps_dark_1x/phragment.png b/resources/sprite/apps_dark_1x/phragment.png new file mode 100644 index 0000000000..9db2586546 Binary files /dev/null and b/resources/sprite/apps_dark_1x/phragment.png differ diff --git a/resources/sprite/apps_dark_1x/settings-sm.png b/resources/sprite/apps_dark_1x/settings-sm.png new file mode 100644 index 0000000000..6b36cf082a Binary files /dev/null and b/resources/sprite/apps_dark_1x/settings-sm.png differ diff --git a/resources/sprite/apps_dark_2x/authentication.png b/resources/sprite/apps_dark_2x/authentication.png index 42f3c8ba55..a5189917a0 100644 Binary files a/resources/sprite/apps_dark_2x/authentication.png and b/resources/sprite/apps_dark_2x/authentication.png differ diff --git a/resources/sprite/apps_dark_2x/fancyhome.png b/resources/sprite/apps_dark_2x/fancyhome.png new file mode 100644 index 0000000000..06bc8662bf Binary files /dev/null and b/resources/sprite/apps_dark_2x/fancyhome.png differ diff --git a/resources/sprite/apps_dark_2x/info-sm.png b/resources/sprite/apps_dark_2x/info-sm.png new file mode 100644 index 0000000000..501d9f6b4c Binary files /dev/null and b/resources/sprite/apps_dark_2x/info-sm.png differ diff --git a/resources/sprite/apps_dark_2x/logout-sm.png b/resources/sprite/apps_dark_2x/logout-sm.png new file mode 100644 index 0000000000..421f52b87b Binary files /dev/null and b/resources/sprite/apps_dark_2x/logout-sm.png differ diff --git a/resources/sprite/apps_dark_2x/new-sm.png b/resources/sprite/apps_dark_2x/new-sm.png new file mode 100644 index 0000000000..d17a5b4dd0 Binary files /dev/null and b/resources/sprite/apps_dark_2x/new-sm.png differ diff --git a/resources/sprite/apps_dark_2x/passphrase.png b/resources/sprite/apps_dark_2x/passphrase.png new file mode 100644 index 0000000000..556f0b46ae Binary files /dev/null and b/resources/sprite/apps_dark_2x/passphrase.png differ diff --git a/resources/sprite/apps_dark_2x/phortune.png b/resources/sprite/apps_dark_2x/phortune.png index 43c853e171..ce49cd9f2a 100644 Binary files a/resources/sprite/apps_dark_2x/phortune.png and b/resources/sprite/apps_dark_2x/phortune.png differ diff --git a/resources/sprite/apps_dark_2x/phragment.png b/resources/sprite/apps_dark_2x/phragment.png new file mode 100644 index 0000000000..44a03efbae Binary files /dev/null and b/resources/sprite/apps_dark_2x/phragment.png differ diff --git a/resources/sprite/apps_dark_2x/settings-sm.png b/resources/sprite/apps_dark_2x/settings-sm.png new file mode 100644 index 0000000000..7f6e745202 Binary files /dev/null and b/resources/sprite/apps_dark_2x/settings-sm.png differ diff --git a/resources/sprite/apps_dark_4x/authentication.png b/resources/sprite/apps_dark_4x/authentication.png index 4bfbbd0c68..5cfd6af1fc 100644 Binary files a/resources/sprite/apps_dark_4x/authentication.png and b/resources/sprite/apps_dark_4x/authentication.png differ diff --git a/resources/sprite/apps_dark_4x/fancyhome.png b/resources/sprite/apps_dark_4x/fancyhome.png new file mode 100644 index 0000000000..6538c839f0 Binary files /dev/null and b/resources/sprite/apps_dark_4x/fancyhome.png differ diff --git a/resources/sprite/apps_dark_4x/info-sm.png b/resources/sprite/apps_dark_4x/info-sm.png new file mode 100644 index 0000000000..311296994b Binary files /dev/null and b/resources/sprite/apps_dark_4x/info-sm.png differ diff --git a/resources/sprite/apps_dark_4x/logout-sm.png b/resources/sprite/apps_dark_4x/logout-sm.png new file mode 100644 index 0000000000..741ad23a9d Binary files /dev/null and b/resources/sprite/apps_dark_4x/logout-sm.png differ diff --git a/resources/sprite/apps_dark_4x/new-sm.png b/resources/sprite/apps_dark_4x/new-sm.png new file mode 100644 index 0000000000..9b9ec1e895 Binary files /dev/null and b/resources/sprite/apps_dark_4x/new-sm.png differ diff --git a/resources/sprite/apps_dark_4x/passphrase.png b/resources/sprite/apps_dark_4x/passphrase.png new file mode 100644 index 0000000000..d1c7470457 Binary files /dev/null and b/resources/sprite/apps_dark_4x/passphrase.png differ diff --git a/resources/sprite/apps_dark_4x/phragment.png b/resources/sprite/apps_dark_4x/phragment.png new file mode 100644 index 0000000000..75f7eb5a7b Binary files /dev/null and b/resources/sprite/apps_dark_4x/phragment.png differ diff --git a/resources/sprite/apps_dark_4x/settings-sm.png b/resources/sprite/apps_dark_4x/settings-sm.png new file mode 100644 index 0000000000..27fa47b63e Binary files /dev/null and b/resources/sprite/apps_dark_4x/settings-sm.png differ diff --git a/resources/sprite/apps_lb_2x/authentication.png b/resources/sprite/apps_lb_2x/authentication.png index 1052da99c8..77a0f0a85e 100644 Binary files a/resources/sprite/apps_lb_2x/authentication.png and b/resources/sprite/apps_lb_2x/authentication.png differ diff --git a/resources/sprite/apps_lb_2x/fancyhome.png b/resources/sprite/apps_lb_2x/fancyhome.png new file mode 100644 index 0000000000..b10ea5bde0 Binary files /dev/null and b/resources/sprite/apps_lb_2x/fancyhome.png differ diff --git a/resources/sprite/apps_lb_2x/info-sm.png b/resources/sprite/apps_lb_2x/info-sm.png new file mode 100644 index 0000000000..a9fe5f2a49 Binary files /dev/null and b/resources/sprite/apps_lb_2x/info-sm.png differ diff --git a/resources/sprite/apps_lb_2x/logout-sm.png b/resources/sprite/apps_lb_2x/logout-sm.png new file mode 100644 index 0000000000..f17b61f0f9 Binary files /dev/null and b/resources/sprite/apps_lb_2x/logout-sm.png differ diff --git a/resources/sprite/apps_lb_2x/new-sm.png b/resources/sprite/apps_lb_2x/new-sm.png new file mode 100644 index 0000000000..fa7eb8a888 Binary files /dev/null and b/resources/sprite/apps_lb_2x/new-sm.png differ diff --git a/resources/sprite/apps_lb_2x/passphrase.png b/resources/sprite/apps_lb_2x/passphrase.png new file mode 100644 index 0000000000..dfa8da803f Binary files /dev/null and b/resources/sprite/apps_lb_2x/passphrase.png differ diff --git a/resources/sprite/apps_lb_2x/phragment.png b/resources/sprite/apps_lb_2x/phragment.png new file mode 100644 index 0000000000..920c8fba4f Binary files /dev/null and b/resources/sprite/apps_lb_2x/phragment.png differ diff --git a/resources/sprite/apps_lb_2x/settings-sm.png b/resources/sprite/apps_lb_2x/settings-sm.png new file mode 100644 index 0000000000..430e5e41c5 Binary files /dev/null and b/resources/sprite/apps_lb_2x/settings-sm.png differ diff --git a/resources/sprite/apps_lb_4x/authentication.png b/resources/sprite/apps_lb_4x/authentication.png index e80002109a..acfe1a2780 100644 Binary files a/resources/sprite/apps_lb_4x/authentication.png and b/resources/sprite/apps_lb_4x/authentication.png differ diff --git a/resources/sprite/apps_lb_4x/fancyhome.png b/resources/sprite/apps_lb_4x/fancyhome.png new file mode 100644 index 0000000000..db29ffae76 Binary files /dev/null and b/resources/sprite/apps_lb_4x/fancyhome.png differ diff --git a/resources/sprite/apps_lb_4x/info-sm.png b/resources/sprite/apps_lb_4x/info-sm.png new file mode 100644 index 0000000000..9e34284a0e Binary files /dev/null and b/resources/sprite/apps_lb_4x/info-sm.png differ diff --git a/resources/sprite/apps_lb_4x/logout-sm.png b/resources/sprite/apps_lb_4x/logout-sm.png new file mode 100644 index 0000000000..467bdc5bb4 Binary files /dev/null and b/resources/sprite/apps_lb_4x/logout-sm.png differ diff --git a/resources/sprite/apps_lb_4x/new-sm.png b/resources/sprite/apps_lb_4x/new-sm.png new file mode 100644 index 0000000000..f7b26d8a90 Binary files /dev/null and b/resources/sprite/apps_lb_4x/new-sm.png differ diff --git a/resources/sprite/apps_lb_4x/passphrase.png b/resources/sprite/apps_lb_4x/passphrase.png new file mode 100644 index 0000000000..20a0121efb Binary files /dev/null and b/resources/sprite/apps_lb_4x/passphrase.png differ diff --git a/resources/sprite/apps_lb_4x/phragment.png b/resources/sprite/apps_lb_4x/phragment.png new file mode 100644 index 0000000000..8be7807337 Binary files /dev/null and b/resources/sprite/apps_lb_4x/phragment.png differ diff --git a/resources/sprite/apps_lb_4x/settings-sm.png b/resources/sprite/apps_lb_4x/settings-sm.png new file mode 100644 index 0000000000..64821e6c3d Binary files /dev/null and b/resources/sprite/apps_lb_4x/settings-sm.png differ diff --git a/resources/sprite/apps_white_1x/authentication.png b/resources/sprite/apps_white_1x/authentication.png index 17e11d0b5b..aaa18ad532 100644 Binary files a/resources/sprite/apps_white_1x/authentication.png and b/resources/sprite/apps_white_1x/authentication.png differ diff --git a/resources/sprite/apps_white_1x/fancyhome.png b/resources/sprite/apps_white_1x/fancyhome.png new file mode 100644 index 0000000000..c7a2988175 Binary files /dev/null and b/resources/sprite/apps_white_1x/fancyhome.png differ diff --git a/resources/sprite/apps_white_1x/info-sm.png b/resources/sprite/apps_white_1x/info-sm.png new file mode 100644 index 0000000000..d116b0ae85 Binary files /dev/null and b/resources/sprite/apps_white_1x/info-sm.png differ diff --git a/resources/sprite/apps_white_1x/logout-sm.png b/resources/sprite/apps_white_1x/logout-sm.png new file mode 100644 index 0000000000..81301a6baa Binary files /dev/null and b/resources/sprite/apps_white_1x/logout-sm.png differ diff --git a/resources/sprite/apps_white_1x/new-sm.png b/resources/sprite/apps_white_1x/new-sm.png new file mode 100644 index 0000000000..105f43bec6 Binary files /dev/null and b/resources/sprite/apps_white_1x/new-sm.png differ diff --git a/resources/sprite/apps_white_1x/passphrase.png b/resources/sprite/apps_white_1x/passphrase.png new file mode 100644 index 0000000000..17e11d0b5b Binary files /dev/null and b/resources/sprite/apps_white_1x/passphrase.png differ diff --git a/resources/sprite/apps_white_1x/phragment.png b/resources/sprite/apps_white_1x/phragment.png new file mode 100644 index 0000000000..039589d592 Binary files /dev/null and b/resources/sprite/apps_white_1x/phragment.png differ diff --git a/resources/sprite/apps_white_1x/settings-sm.png b/resources/sprite/apps_white_1x/settings-sm.png new file mode 100644 index 0000000000..c06688d1df Binary files /dev/null and b/resources/sprite/apps_white_1x/settings-sm.png differ diff --git a/resources/sprite/apps_white_2x/authentication.png b/resources/sprite/apps_white_2x/authentication.png index 17d3332948..2ab63b298a 100644 Binary files a/resources/sprite/apps_white_2x/authentication.png and b/resources/sprite/apps_white_2x/authentication.png differ diff --git a/resources/sprite/apps_white_2x/fancyhome.png b/resources/sprite/apps_white_2x/fancyhome.png new file mode 100644 index 0000000000..fcba9f5976 Binary files /dev/null and b/resources/sprite/apps_white_2x/fancyhome.png differ diff --git a/resources/sprite/apps_white_2x/info-sm.png b/resources/sprite/apps_white_2x/info-sm.png new file mode 100644 index 0000000000..a2220be4fc Binary files /dev/null and b/resources/sprite/apps_white_2x/info-sm.png differ diff --git a/resources/sprite/apps_white_2x/logout-sm.png b/resources/sprite/apps_white_2x/logout-sm.png new file mode 100644 index 0000000000..8ded2edcc6 Binary files /dev/null and b/resources/sprite/apps_white_2x/logout-sm.png differ diff --git a/resources/sprite/apps_white_2x/new-sm.png b/resources/sprite/apps_white_2x/new-sm.png new file mode 100644 index 0000000000..f0de03133b Binary files /dev/null and b/resources/sprite/apps_white_2x/new-sm.png differ diff --git a/resources/sprite/apps_white_2x/passphrase.png b/resources/sprite/apps_white_2x/passphrase.png new file mode 100644 index 0000000000..36214ad86b Binary files /dev/null and b/resources/sprite/apps_white_2x/passphrase.png differ diff --git a/resources/sprite/apps_white_2x/phragment.png b/resources/sprite/apps_white_2x/phragment.png new file mode 100644 index 0000000000..26c0696ffc Binary files /dev/null and b/resources/sprite/apps_white_2x/phragment.png differ diff --git a/resources/sprite/apps_white_2x/settings-sm.png b/resources/sprite/apps_white_2x/settings-sm.png new file mode 100644 index 0000000000..1d22e0bac5 Binary files /dev/null and b/resources/sprite/apps_white_2x/settings-sm.png differ diff --git a/resources/sprite/apps_white_4x/authentication.png b/resources/sprite/apps_white_4x/authentication.png index 6f227dd2df..72e36e6b17 100644 Binary files a/resources/sprite/apps_white_4x/authentication.png and b/resources/sprite/apps_white_4x/authentication.png differ diff --git a/resources/sprite/apps_white_4x/fancyhome.png b/resources/sprite/apps_white_4x/fancyhome.png new file mode 100644 index 0000000000..9057f51e73 Binary files /dev/null and b/resources/sprite/apps_white_4x/fancyhome.png differ diff --git a/resources/sprite/apps_white_4x/info-sm.png b/resources/sprite/apps_white_4x/info-sm.png new file mode 100644 index 0000000000..9171be0c70 Binary files /dev/null and b/resources/sprite/apps_white_4x/info-sm.png differ diff --git a/resources/sprite/apps_white_4x/logout-sm.png b/resources/sprite/apps_white_4x/logout-sm.png new file mode 100644 index 0000000000..46e63770f4 Binary files /dev/null and b/resources/sprite/apps_white_4x/logout-sm.png differ diff --git a/resources/sprite/apps_white_4x/new-sm.png b/resources/sprite/apps_white_4x/new-sm.png new file mode 100644 index 0000000000..9cc9cfe047 Binary files /dev/null and b/resources/sprite/apps_white_4x/new-sm.png differ diff --git a/resources/sprite/apps_white_4x/passphrase.png b/resources/sprite/apps_white_4x/passphrase.png new file mode 100644 index 0000000000..367c510ab9 Binary files /dev/null and b/resources/sprite/apps_white_4x/passphrase.png differ diff --git a/resources/sprite/apps_white_4x/phragment.png b/resources/sprite/apps_white_4x/phragment.png new file mode 100644 index 0000000000..56355a361a Binary files /dev/null and b/resources/sprite/apps_white_4x/phragment.png differ diff --git a/resources/sprite/apps_white_4x/settings-sm.png b/resources/sprite/apps_white_4x/settings-sm.png new file mode 100644 index 0000000000..1210807630 Binary files /dev/null and b/resources/sprite/apps_white_4x/settings-sm.png differ diff --git a/resources/sprite/icons_1x/backward.png b/resources/sprite/icons_1x/backward.png new file mode 100644 index 0000000000..6aa24779c8 Binary files /dev/null and b/resources/sprite/icons_1x/backward.png differ diff --git a/resources/sprite/icons_1x/forward.png b/resources/sprite/icons_1x/forward.png new file mode 100644 index 0000000000..2df0ea372b Binary files /dev/null and b/resources/sprite/icons_1x/forward.png differ diff --git a/resources/sprite/icons_1x/octocat.png b/resources/sprite/icons_1x/octocat.png new file mode 100644 index 0000000000..246ce8910d Binary files /dev/null and b/resources/sprite/icons_1x/octocat.png differ diff --git a/resources/sprite/icons_1x/ok.png b/resources/sprite/icons_1x/ok.png new file mode 100644 index 0000000000..12da87d50e Binary files /dev/null and b/resources/sprite/icons_1x/ok.png differ diff --git a/resources/sprite/icons_1x/pause.png b/resources/sprite/icons_1x/pause.png new file mode 100644 index 0000000000..46226b69d7 Binary files /dev/null and b/resources/sprite/icons_1x/pause.png differ diff --git a/resources/sprite/icons_1x/play.png b/resources/sprite/icons_1x/play.png new file mode 100644 index 0000000000..230cb5437a Binary files /dev/null and b/resources/sprite/icons_1x/play.png differ diff --git a/resources/sprite/icons_1x/popout.png b/resources/sprite/icons_1x/popout.png new file mode 100644 index 0000000000..291d65fcb0 Binary files /dev/null and b/resources/sprite/icons_1x/popout.png differ diff --git a/resources/sprite/icons_1x/stop.png b/resources/sprite/icons_1x/stop.png new file mode 100644 index 0000000000..ac8e2f3df7 Binary files /dev/null and b/resources/sprite/icons_1x/stop.png differ diff --git a/resources/sprite/icons_2x/backward.png b/resources/sprite/icons_2x/backward.png new file mode 100644 index 0000000000..13bd9b89bd Binary files /dev/null and b/resources/sprite/icons_2x/backward.png differ diff --git a/resources/sprite/icons_2x/forward.png b/resources/sprite/icons_2x/forward.png new file mode 100644 index 0000000000..b859577d30 Binary files /dev/null and b/resources/sprite/icons_2x/forward.png differ diff --git a/resources/sprite/icons_2x/octocat.png b/resources/sprite/icons_2x/octocat.png new file mode 100644 index 0000000000..ef0dc78a9f Binary files /dev/null and b/resources/sprite/icons_2x/octocat.png differ diff --git a/resources/sprite/icons_2x/ok.png b/resources/sprite/icons_2x/ok.png new file mode 100644 index 0000000000..d6a322e41d Binary files /dev/null and b/resources/sprite/icons_2x/ok.png differ diff --git a/resources/sprite/icons_2x/pause.png b/resources/sprite/icons_2x/pause.png new file mode 100644 index 0000000000..5be384ff06 Binary files /dev/null and b/resources/sprite/icons_2x/pause.png differ diff --git a/resources/sprite/icons_2x/play.png b/resources/sprite/icons_2x/play.png new file mode 100644 index 0000000000..0466557b46 Binary files /dev/null and b/resources/sprite/icons_2x/play.png differ diff --git a/resources/sprite/icons_2x/popout.png b/resources/sprite/icons_2x/popout.png new file mode 100644 index 0000000000..a989326251 Binary files /dev/null and b/resources/sprite/icons_2x/popout.png differ diff --git a/resources/sprite/icons_2x/stop.png b/resources/sprite/icons_2x/stop.png new file mode 100644 index 0000000000..ac11bac651 Binary files /dev/null and b/resources/sprite/icons_2x/stop.png differ diff --git a/resources/sprite/icons_grey_1x/backward.png b/resources/sprite/icons_grey_1x/backward.png new file mode 100644 index 0000000000..507fc74eac Binary files /dev/null and b/resources/sprite/icons_grey_1x/backward.png differ diff --git a/resources/sprite/icons_grey_1x/forward.png b/resources/sprite/icons_grey_1x/forward.png new file mode 100644 index 0000000000..0b1c38f151 Binary files /dev/null and b/resources/sprite/icons_grey_1x/forward.png differ diff --git a/resources/sprite/icons_grey_1x/octocat.png b/resources/sprite/icons_grey_1x/octocat.png new file mode 100644 index 0000000000..26b221f024 Binary files /dev/null and b/resources/sprite/icons_grey_1x/octocat.png differ diff --git a/resources/sprite/icons_grey_1x/ok.png b/resources/sprite/icons_grey_1x/ok.png new file mode 100644 index 0000000000..65f9a7fc51 Binary files /dev/null and b/resources/sprite/icons_grey_1x/ok.png differ diff --git a/resources/sprite/icons_grey_1x/pause.png b/resources/sprite/icons_grey_1x/pause.png new file mode 100644 index 0000000000..63d5e577c7 Binary files /dev/null and b/resources/sprite/icons_grey_1x/pause.png differ diff --git a/resources/sprite/icons_grey_1x/play.png b/resources/sprite/icons_grey_1x/play.png new file mode 100644 index 0000000000..3908a17485 Binary files /dev/null and b/resources/sprite/icons_grey_1x/play.png differ diff --git a/resources/sprite/icons_grey_1x/popout.png b/resources/sprite/icons_grey_1x/popout.png new file mode 100644 index 0000000000..2c193ad532 Binary files /dev/null and b/resources/sprite/icons_grey_1x/popout.png differ diff --git a/resources/sprite/icons_grey_1x/stop.png b/resources/sprite/icons_grey_1x/stop.png new file mode 100644 index 0000000000..cfff46118d Binary files /dev/null and b/resources/sprite/icons_grey_1x/stop.png differ diff --git a/resources/sprite/icons_grey_2x/backward.png b/resources/sprite/icons_grey_2x/backward.png new file mode 100644 index 0000000000..2d067dd335 Binary files /dev/null and b/resources/sprite/icons_grey_2x/backward.png differ diff --git a/resources/sprite/icons_grey_2x/forward.png b/resources/sprite/icons_grey_2x/forward.png new file mode 100644 index 0000000000..dc3f0d83f8 Binary files /dev/null and b/resources/sprite/icons_grey_2x/forward.png differ diff --git a/resources/sprite/icons_grey_2x/octocat.png b/resources/sprite/icons_grey_2x/octocat.png new file mode 100644 index 0000000000..77f9f030ec Binary files /dev/null and b/resources/sprite/icons_grey_2x/octocat.png differ diff --git a/resources/sprite/icons_grey_2x/ok.png b/resources/sprite/icons_grey_2x/ok.png new file mode 100644 index 0000000000..5b6799bf7d Binary files /dev/null and b/resources/sprite/icons_grey_2x/ok.png differ diff --git a/resources/sprite/icons_grey_2x/pause.png b/resources/sprite/icons_grey_2x/pause.png new file mode 100644 index 0000000000..c690df9580 Binary files /dev/null and b/resources/sprite/icons_grey_2x/pause.png differ diff --git a/resources/sprite/icons_grey_2x/play.png b/resources/sprite/icons_grey_2x/play.png new file mode 100644 index 0000000000..ff62746f78 Binary files /dev/null and b/resources/sprite/icons_grey_2x/play.png differ diff --git a/resources/sprite/icons_grey_2x/popout.png b/resources/sprite/icons_grey_2x/popout.png new file mode 100644 index 0000000000..47b096eb1d Binary files /dev/null and b/resources/sprite/icons_grey_2x/popout.png differ diff --git a/resources/sprite/icons_grey_2x/stop.png b/resources/sprite/icons_grey_2x/stop.png new file mode 100644 index 0000000000..5fb3bdc7ea Binary files /dev/null and b/resources/sprite/icons_grey_2x/stop.png differ diff --git a/resources/sprite/icons_white_1x/backward.png b/resources/sprite/icons_white_1x/backward.png new file mode 100644 index 0000000000..4723174fb2 Binary files /dev/null and b/resources/sprite/icons_white_1x/backward.png differ diff --git a/resources/sprite/icons_white_1x/forward.png b/resources/sprite/icons_white_1x/forward.png new file mode 100644 index 0000000000..2a781b0b77 Binary files /dev/null and b/resources/sprite/icons_white_1x/forward.png differ diff --git a/resources/sprite/icons_white_1x/octocat.png b/resources/sprite/icons_white_1x/octocat.png new file mode 100644 index 0000000000..6a1ae839b7 Binary files /dev/null and b/resources/sprite/icons_white_1x/octocat.png differ diff --git a/resources/sprite/icons_white_1x/ok.png b/resources/sprite/icons_white_1x/ok.png new file mode 100644 index 0000000000..8d6ba06d9d Binary files /dev/null and b/resources/sprite/icons_white_1x/ok.png differ diff --git a/resources/sprite/icons_white_1x/pause.png b/resources/sprite/icons_white_1x/pause.png new file mode 100644 index 0000000000..ea80ec20ce Binary files /dev/null and b/resources/sprite/icons_white_1x/pause.png differ diff --git a/resources/sprite/icons_white_1x/play.png b/resources/sprite/icons_white_1x/play.png new file mode 100644 index 0000000000..8efb237259 Binary files /dev/null and b/resources/sprite/icons_white_1x/play.png differ diff --git a/resources/sprite/icons_white_1x/popout.png b/resources/sprite/icons_white_1x/popout.png new file mode 100644 index 0000000000..2f74a79948 Binary files /dev/null and b/resources/sprite/icons_white_1x/popout.png differ diff --git a/resources/sprite/icons_white_1x/stop.png b/resources/sprite/icons_white_1x/stop.png new file mode 100644 index 0000000000..8f2421f8f8 Binary files /dev/null and b/resources/sprite/icons_white_1x/stop.png differ diff --git a/resources/sprite/icons_white_2x/backward.png b/resources/sprite/icons_white_2x/backward.png new file mode 100644 index 0000000000..b51a4d886e Binary files /dev/null and b/resources/sprite/icons_white_2x/backward.png differ diff --git a/resources/sprite/icons_white_2x/forward.png b/resources/sprite/icons_white_2x/forward.png new file mode 100644 index 0000000000..eadf213700 Binary files /dev/null and b/resources/sprite/icons_white_2x/forward.png differ diff --git a/resources/sprite/icons_white_2x/octocat.png b/resources/sprite/icons_white_2x/octocat.png new file mode 100644 index 0000000000..e6848fb1e4 Binary files /dev/null and b/resources/sprite/icons_white_2x/octocat.png differ diff --git a/resources/sprite/icons_white_2x/ok.png b/resources/sprite/icons_white_2x/ok.png new file mode 100644 index 0000000000..fcf2a2036c Binary files /dev/null and b/resources/sprite/icons_white_2x/ok.png differ diff --git a/resources/sprite/icons_white_2x/pause.png b/resources/sprite/icons_white_2x/pause.png new file mode 100644 index 0000000000..684d6f7534 Binary files /dev/null and b/resources/sprite/icons_white_2x/pause.png differ diff --git a/resources/sprite/icons_white_2x/play.png b/resources/sprite/icons_white_2x/play.png new file mode 100644 index 0000000000..76ea0bb70f Binary files /dev/null and b/resources/sprite/icons_white_2x/play.png differ diff --git a/resources/sprite/icons_white_2x/popout.png b/resources/sprite/icons_white_2x/popout.png new file mode 100644 index 0000000000..a9003c8b59 Binary files /dev/null and b/resources/sprite/icons_white_2x/popout.png differ diff --git a/resources/sprite/icons_white_2x/stop.png b/resources/sprite/icons_white_2x/stop.png new file mode 100644 index 0000000000..9989a730c3 Binary files /dev/null and b/resources/sprite/icons_white_2x/stop.png differ diff --git a/resources/sprite/main_header/applebloom.png b/resources/sprite/main_header/applebloom.png new file mode 100644 index 0000000000..29721ef8fd Binary files /dev/null and b/resources/sprite/main_header/applebloom.png differ diff --git a/resources/sprite/main_header/blue.png b/resources/sprite/main_header/blue.png new file mode 100644 index 0000000000..7183340432 Binary files /dev/null and b/resources/sprite/main_header/blue.png differ diff --git a/resources/sprite/main_header/dark.png b/resources/sprite/main_header/dark.png new file mode 100644 index 0000000000..f676a0d1b8 Binary files /dev/null and b/resources/sprite/main_header/dark.png differ diff --git a/resources/sprite/main_header/fluttershy.png b/resources/sprite/main_header/fluttershy.png new file mode 100644 index 0000000000..796158d39b Binary files /dev/null and b/resources/sprite/main_header/fluttershy.png differ diff --git a/resources/sprite/main_header/green.png b/resources/sprite/main_header/green.png new file mode 100644 index 0000000000..6c84d1deaa Binary files /dev/null and b/resources/sprite/main_header/green.png differ diff --git a/resources/sprite/main_header/nightmaremoon.png b/resources/sprite/main_header/nightmaremoon.png new file mode 100644 index 0000000000..2e98550795 Binary files /dev/null and b/resources/sprite/main_header/nightmaremoon.png differ diff --git a/resources/sprite/main_header/red.png b/resources/sprite/main_header/red.png new file mode 100644 index 0000000000..2620221ec6 Binary files /dev/null and b/resources/sprite/main_header/red.png differ diff --git a/resources/sprite/main_header/scootaloo.png b/resources/sprite/main_header/scootaloo.png new file mode 100644 index 0000000000..32e724ec5e Binary files /dev/null and b/resources/sprite/main_header/scootaloo.png differ diff --git a/resources/sprite/main_header/yellow.png b/resources/sprite/main_header/yellow.png new file mode 100644 index 0000000000..a8a4d67f0b Binary files /dev/null and b/resources/sprite/main_header/yellow.png differ diff --git a/resources/sprite/manifest/apps-large.json b/resources/sprite/manifest/apps-large.json index 05ed437792..55b8b4126b 100644 --- a/resources/sprite/manifest/apps-large.json +++ b/resources/sprite/manifest/apps-large.json @@ -64,22 +64,22 @@ "apps-authentication-blue-large" : { "name" : "apps-authentication-blue-large", "rule" : ".apps-authentication-blue-large, .phabricator-crumb-view:hover .apps-authentication-dark-large", - "hash" : "9a8389120be19de5b0fba2814736d2c4" + "hash" : "3d5e35b539e11d69635d04651c973adf" }, "apps-authentication-dark-large" : { "name" : "apps-authentication-dark-large", "rule" : ".apps-authentication-dark-large", - "hash" : "478a7a1ae296ccf988ccce89bc7de25d" + "hash" : "2321ca8222a5368f80717c5424f3fd17" }, "apps-authentication-light-large" : { "name" : "apps-authentication-light-large", "rule" : ".apps-authentication-light-large", - "hash" : "034256f1056033424b936e94debaa9ad" + "hash" : "8ec1778a94a9b17d22dec23e466077b3" }, "apps-authentication-white-large" : { "name" : "apps-authentication-white-large", "rule" : ".apps-authentication-white-large", - "hash" : "29a681a8825dbeb805480bdddb91c737" + "hash" : "9ccbff0a913a20892846b0df6a502bbe" }, "apps-calendar-blue-large" : { "name" : "apps-calendar-blue-large", @@ -301,6 +301,26 @@ "rule" : ".apps-fact-white-large", "hash" : "d24e0b88ecfb05cb80de311eb5cc2e15" }, + "apps-fancyhome-blue-large" : { + "name" : "apps-fancyhome-blue-large", + "rule" : ".apps-fancyhome-blue-large, .phabricator-crumb-view:hover .apps-fancyhome-dark-large", + "hash" : "280c997ba6ada59c571a3d510d783872" + }, + "apps-fancyhome-dark-large" : { + "name" : "apps-fancyhome-dark-large", + "rule" : ".apps-fancyhome-dark-large", + "hash" : "8bcb67304d092f8c29e0298c677e56df" + }, + "apps-fancyhome-light-large" : { + "name" : "apps-fancyhome-light-large", + "rule" : ".apps-fancyhome-light-large", + "hash" : "c385141a3f3c655c1798e7df0d8d6a38" + }, + "apps-fancyhome-white-large" : { + "name" : "apps-fancyhome-white-large", + "rule" : ".apps-fancyhome-white-large", + "hash" : "aee0b67863ce48c7f03ca87042108c78" + }, "apps-feed-blue-large" : { "name" : "apps-feed-blue-large", "rule" : ".apps-feed-blue-large, .phabricator-crumb-view:hover .apps-feed-dark-large", @@ -364,7 +384,7 @@ "apps-harbormaster-blue-large" : { "name" : "apps-harbormaster-blue-large", "rule" : ".apps-harbormaster-blue-large, .phabricator-crumb-view:hover .apps-harbormaster-dark-large", - "hash" : "c4c81a696f9a4e1242324a1e420ab35b" + "hash" : "6912f9730452b2272439f7a77b4d0469" }, "apps-harbormaster-dark-large" : { "name" : "apps-harbormaster-dark-large", @@ -441,6 +461,26 @@ "rule" : ".apps-home-white-large", "hash" : "709053e29f8364081c00724c1f9634aa" }, + "apps-info-sm-blue-large" : { + "name" : "apps-info-sm-blue-large", + "rule" : ".apps-info-sm-blue-large, .phabricator-crumb-view:hover .apps-info-sm-dark-large", + "hash" : "2975fdc99dbeb3ef28b5a2558bcfaf1d" + }, + "apps-info-sm-dark-large" : { + "name" : "apps-info-sm-dark-large", + "rule" : ".apps-info-sm-dark-large", + "hash" : "d5be94bb802d03064944c3d17771e76c" + }, + "apps-info-sm-light-large" : { + "name" : "apps-info-sm-light-large", + "rule" : ".apps-info-sm-light-large", + "hash" : "48ed57d0e856604c0053ea35a711427f" + }, + "apps-info-sm-white-large" : { + "name" : "apps-info-sm-white-large", + "rule" : ".apps-info-sm-white-large", + "hash" : "a8738137dc48d8d81b7648118d9ac6f8" + }, "apps-legalpad-blue-large" : { "name" : "apps-legalpad-blue-large", "rule" : ".apps-legalpad-blue-large, .phabricator-crumb-view:hover .apps-legalpad-dark-large", @@ -481,6 +521,26 @@ "rule" : ".apps-logo-white-large", "hash" : "323fa19b10af04bd7a368b65591b7be1" }, + "apps-logout-sm-blue-large" : { + "name" : "apps-logout-sm-blue-large", + "rule" : ".apps-logout-sm-blue-large, .phabricator-crumb-view:hover .apps-logout-sm-dark-large", + "hash" : "5137785c3b164ff73aa3cc1dbe95b9df" + }, + "apps-logout-sm-dark-large" : { + "name" : "apps-logout-sm-dark-large", + "rule" : ".apps-logout-sm-dark-large", + "hash" : "9f6177afadb8680891cb2580a0385fdf" + }, + "apps-logout-sm-light-large" : { + "name" : "apps-logout-sm-light-large", + "rule" : ".apps-logout-sm-light-large", + "hash" : "8e1323b1155e335e916e773a4ce3ecda" + }, + "apps-logout-sm-white-large" : { + "name" : "apps-logout-sm-white-large", + "rule" : ".apps-logout-sm-white-large", + "hash" : "c4b9ca54ac63d6313c7dad772083fd2e" + }, "apps-macro-blue-large" : { "name" : "apps-macro-blue-large", "rule" : ".apps-macro-blue-large, .phabricator-crumb-view:hover .apps-macro-dark-large", @@ -576,6 +636,26 @@ "rule" : ".apps-new-light-large", "hash" : "c93739e145aec183aaa1fea4e37aa3aa" }, + "apps-new-sm-blue-large" : { + "name" : "apps-new-sm-blue-large", + "rule" : ".apps-new-sm-blue-large, .phabricator-crumb-view:hover .apps-new-sm-dark-large", + "hash" : "b0c78705fbbc134e3ac076c49a3c8ba5" + }, + "apps-new-sm-dark-large" : { + "name" : "apps-new-sm-dark-large", + "rule" : ".apps-new-sm-dark-large", + "hash" : "cf0fed65bbdf380cdcaed9c1fcb9b24b" + }, + "apps-new-sm-light-large" : { + "name" : "apps-new-sm-light-large", + "rule" : ".apps-new-sm-light-large", + "hash" : "b0c2796af2de1ae23ec8ce88c8dae4c6" + }, + "apps-new-sm-white-large" : { + "name" : "apps-new-sm-white-large", + "rule" : ".apps-new-sm-white-large", + "hash" : "08258672d0957d5daa3f11d1c6d8b51d" + }, "apps-new-white-large" : { "name" : "apps-new-white-large", "rule" : ".apps-new-white-large", @@ -621,6 +701,26 @@ "rule" : ".apps-owners-white-large", "hash" : "a267ce517c928ce5861fe7ac85116af7" }, + "apps-passphrase-blue-large" : { + "name" : "apps-passphrase-blue-large", + "rule" : ".apps-passphrase-blue-large, .phabricator-crumb-view:hover .apps-passphrase-dark-large", + "hash" : "bb5ff2b25e5f7df50ec77ed4a5d38ea6" + }, + "apps-passphrase-dark-large" : { + "name" : "apps-passphrase-dark-large", + "rule" : ".apps-passphrase-dark-large", + "hash" : "8f5dea9e2e329a1d5001bbc8e739899c" + }, + "apps-passphrase-light-large" : { + "name" : "apps-passphrase-light-large", + "rule" : ".apps-passphrase-light-large", + "hash" : "b036bcd1cff87d5733fee676ae1d45c0" + }, + "apps-passphrase-white-large" : { + "name" : "apps-passphrase-white-large", + "rule" : ".apps-passphrase-white-large", + "hash" : "7c16e3be0fec2b107da1a770026e1155" + }, "apps-paste-blue-large" : { "name" : "apps-paste-blue-large", "rule" : ".apps-paste-blue-large, .phabricator-crumb-view:hover .apps-paste-dark-large", @@ -764,12 +864,12 @@ "apps-phortune-blue-large" : { "name" : "apps-phortune-blue-large", "rule" : ".apps-phortune-blue-large, .phabricator-crumb-view:hover .apps-phortune-dark-large", - "hash" : "78c71081f4f3c8f5075041f5e6c802a1" + "hash" : "99363e792c5ec614d6979a73c42a0506" }, "apps-phortune-dark-large" : { "name" : "apps-phortune-dark-large", "rule" : ".apps-phortune-dark-large", - "hash" : "7e8a5ad784e4353309c8ae496dff9141" + "hash" : "64c5f757d1182df3cb529628cf4d46bf" }, "apps-phortune-light-large" : { "name" : "apps-phortune-light-large", @@ -801,6 +901,26 @@ "rule" : ".apps-phpast-white-large", "hash" : "69ffddb6e09d2f0fb9dd286bec30ed85" }, + "apps-phragment-blue-large" : { + "name" : "apps-phragment-blue-large", + "rule" : ".apps-phragment-blue-large, .phabricator-crumb-view:hover .apps-phragment-dark-large", + "hash" : "d41b01892ba6b3a68bc09aa996077126" + }, + "apps-phragment-dark-large" : { + "name" : "apps-phragment-dark-large", + "rule" : ".apps-phragment-dark-large", + "hash" : "35a854a1cd62fb6223811d31d1076dfd" + }, + "apps-phragment-light-large" : { + "name" : "apps-phragment-light-large", + "rule" : ".apps-phragment-light-large", + "hash" : "26eb4af0e4792cd5c7bdf480afc520c5" + }, + "apps-phragment-white-large" : { + "name" : "apps-phragment-white-large", + "rule" : ".apps-phragment-white-large", + "hash" : "6ef73910f67c20ed29491b6192cfb263" + }, "apps-phrequent-blue-large" : { "name" : "apps-phrequent-blue-large", "rule" : ".apps-phrequent-blue-large, .phabricator-crumb-view:hover .apps-phrequent-dark-large", @@ -996,6 +1116,26 @@ "rule" : ".apps-settings-light-large", "hash" : "a754e26164dac005b5e230f99b94851a" }, + "apps-settings-sm-blue-large" : { + "name" : "apps-settings-sm-blue-large", + "rule" : ".apps-settings-sm-blue-large, .phabricator-crumb-view:hover .apps-settings-sm-dark-large", + "hash" : "0f892f588d6aebd475d870c0b368afee" + }, + "apps-settings-sm-dark-large" : { + "name" : "apps-settings-sm-dark-large", + "rule" : ".apps-settings-sm-dark-large", + "hash" : "4a1a8b2628b26c1d3f48254a335c00af" + }, + "apps-settings-sm-light-large" : { + "name" : "apps-settings-sm-light-large", + "rule" : ".apps-settings-sm-light-large", + "hash" : "14770a40409ef7218dbbbf684026f658" + }, + "apps-settings-sm-white-large" : { + "name" : "apps-settings-sm-white-large", + "rule" : ".apps-settings-sm-white-large", + "hash" : "7e1343d9f0f6a86dc7cbb514143d0232" + }, "apps-settings-white-large" : { "name" : "apps-settings-white-large", "rule" : ".apps-settings-white-large", diff --git a/resources/sprite/manifest/apps.json b/resources/sprite/manifest/apps.json index 9427a63caa..bd7d1a5427 100644 --- a/resources/sprite/manifest/apps.json +++ b/resources/sprite/manifest/apps.json @@ -8,7 +8,7 @@ }, "apps-adventure-white" : { "name" : "apps-adventure-white", - "rule" : ".apps-adventure-white", + "rule" : ".apps-adventure-white, .phui-list-item-href:hover .apps-adventure-dark", "hash" : "bd4dd76bc727638a757f392c8b1ffdeb" }, "apps-application-dark" : { @@ -18,7 +18,7 @@ }, "apps-application-white" : { "name" : "apps-application-white", - "rule" : ".apps-application-white", + "rule" : ".apps-application-white, .phui-list-item-href:hover .apps-application-dark", "hash" : "efe96bec30b97147c5eb4ffa818cac5a" }, "apps-audit-dark" : { @@ -28,18 +28,18 @@ }, "apps-audit-white" : { "name" : "apps-audit-white", - "rule" : ".apps-audit-white", + "rule" : ".apps-audit-white, .phui-list-item-href:hover .apps-audit-dark", "hash" : "8b14beee5adc284b3804d0bb99e0498f" }, "apps-authentication-dark" : { "name" : "apps-authentication-dark", "rule" : ".apps-authentication-dark", - "hash" : "5ae0f7e5d45df5da12ecc447f44bc7d9" + "hash" : "8616a1c5aeeeac1ce87376f2f0757439" }, "apps-authentication-white" : { "name" : "apps-authentication-white", - "rule" : ".apps-authentication-white", - "hash" : "567b1e8f11974b0817250b933b9dd3de" + "rule" : ".apps-authentication-white, .phui-list-item-href:hover .apps-authentication-dark", + "hash" : "9478fbaeee9e5cc44c4a5b483645caed" }, "apps-calendar-dark" : { "name" : "apps-calendar-dark", @@ -48,7 +48,7 @@ }, "apps-calendar-white" : { "name" : "apps-calendar-white", - "rule" : ".apps-calendar-white", + "rule" : ".apps-calendar-white, .phui-list-item-href:hover .apps-calendar-dark", "hash" : "d1a6ba42bc0b5e8b595e5f3a5ecc4c07" }, "apps-chatlog-dark" : { @@ -58,7 +58,7 @@ }, "apps-chatlog-white" : { "name" : "apps-chatlog-white", - "rule" : ".apps-chatlog-white", + "rule" : ".apps-chatlog-white, .phui-list-item-href:hover .apps-chatlog-dark", "hash" : "edff1793764633464cf167e3c6c128f0" }, "apps-conduit-dark" : { @@ -68,7 +68,7 @@ }, "apps-conduit-white" : { "name" : "apps-conduit-white", - "rule" : ".apps-conduit-white", + "rule" : ".apps-conduit-white, .phui-list-item-href:hover .apps-conduit-dark", "hash" : "3023fb959718d9abae4eb8af770c8378" }, "apps-conpherence-dark" : { @@ -78,7 +78,7 @@ }, "apps-conpherence-white" : { "name" : "apps-conpherence-white", - "rule" : ".apps-conpherence-white", + "rule" : ".apps-conpherence-white, .phui-list-item-href:hover .apps-conpherence-dark", "hash" : "d9a2ce4d8928a81b8f02954ea179c75f" }, "apps-countdown-dark" : { @@ -88,7 +88,7 @@ }, "apps-countdown-white" : { "name" : "apps-countdown-white", - "rule" : ".apps-countdown-white", + "rule" : ".apps-countdown-white, .phui-list-item-href:hover .apps-countdown-dark", "hash" : "8d27e6577c033a77ce1d62417845812f" }, "apps-daemon-dark" : { @@ -98,7 +98,7 @@ }, "apps-daemon-white" : { "name" : "apps-daemon-white", - "rule" : ".apps-daemon-white", + "rule" : ".apps-daemon-white, .phui-list-item-href:hover .apps-daemon-dark", "hash" : "c07fc293a9207edc535b2b74e7ae059b" }, "apps-differential-dark" : { @@ -108,7 +108,7 @@ }, "apps-differential-white" : { "name" : "apps-differential-white", - "rule" : ".apps-differential-white", + "rule" : ".apps-differential-white, .phui-list-item-href:hover .apps-differential-dark", "hash" : "feb4e921077420f55a936a2eea300a37" }, "apps-diffusion-dark" : { @@ -118,7 +118,7 @@ }, "apps-diffusion-white" : { "name" : "apps-diffusion-white", - "rule" : ".apps-diffusion-white", + "rule" : ".apps-diffusion-white, .phui-list-item-href:hover .apps-diffusion-dark", "hash" : "f6786cf8d18610621d1baa04d3b55e75" }, "apps-diviner-dark" : { @@ -128,7 +128,7 @@ }, "apps-diviner-white" : { "name" : "apps-diviner-white", - "rule" : ".apps-diviner-white", + "rule" : ".apps-diviner-white, .phui-list-item-href:hover .apps-diviner-dark", "hash" : "1fa0636256aeaea27fd6259aa9cc7003" }, "apps-drydock-dark" : { @@ -138,7 +138,7 @@ }, "apps-drydock-white" : { "name" : "apps-drydock-white", - "rule" : ".apps-drydock-white", + "rule" : ".apps-drydock-white, .phui-list-item-href:hover .apps-drydock-dark", "hash" : "599108df7b845329cb9d8982925cf5eb" }, "apps-fact-dark" : { @@ -148,9 +148,19 @@ }, "apps-fact-white" : { "name" : "apps-fact-white", - "rule" : ".apps-fact-white", + "rule" : ".apps-fact-white, .phui-list-item-href:hover .apps-fact-dark", "hash" : "f387ef4101d9ae0511d975cd25e9b9aa" }, + "apps-fancyhome-dark" : { + "name" : "apps-fancyhome-dark", + "rule" : ".apps-fancyhome-dark", + "hash" : "f30cddee675af2b6b26c11482d2ffee0" + }, + "apps-fancyhome-white" : { + "name" : "apps-fancyhome-white", + "rule" : ".apps-fancyhome-white, .phui-list-item-href:hover .apps-fancyhome-dark", + "hash" : "c15085891df0cf3f10853101f7dea8e1" + }, "apps-feed-dark" : { "name" : "apps-feed-dark", "rule" : ".apps-feed-dark", @@ -158,7 +168,7 @@ }, "apps-feed-white" : { "name" : "apps-feed-white", - "rule" : ".apps-feed-white", + "rule" : ".apps-feed-white, .phui-list-item-href:hover .apps-feed-dark", "hash" : "7540dff74270975a9bf1df4bbc65b728" }, "apps-files-dark" : { @@ -168,7 +178,7 @@ }, "apps-files-white" : { "name" : "apps-files-white", - "rule" : ".apps-files-white", + "rule" : ".apps-files-white, .phui-list-item-href:hover .apps-files-dark", "hash" : "48235273ebb9c6ebdca82dddd64b5eaf" }, "apps-flags-dark" : { @@ -178,7 +188,7 @@ }, "apps-flags-white" : { "name" : "apps-flags-white", - "rule" : ".apps-flags-white", + "rule" : ".apps-flags-white, .phui-list-item-href:hover .apps-flags-dark", "hash" : "9ce2476cff7e5f123a20a9152c79b4b0" }, "apps-harbormaster-dark" : { @@ -188,7 +198,7 @@ }, "apps-harbormaster-white" : { "name" : "apps-harbormaster-white", - "rule" : ".apps-harbormaster-white", + "rule" : ".apps-harbormaster-white, .phui-list-item-href:hover .apps-harbormaster-dark", "hash" : "205912048bf6dba9d8e09835922c13ec" }, "apps-help-dark" : { @@ -198,7 +208,7 @@ }, "apps-help-white" : { "name" : "apps-help-white", - "rule" : ".apps-help-white", + "rule" : ".apps-help-white, .phui-list-item-href:hover .apps-help-dark", "hash" : "a62819577284cfce49e9595760a69071" }, "apps-herald-dark" : { @@ -208,7 +218,7 @@ }, "apps-herald-white" : { "name" : "apps-herald-white", - "rule" : ".apps-herald-white", + "rule" : ".apps-herald-white, .phui-list-item-href:hover .apps-herald-dark", "hash" : "0b962a02801a8ef892ff9dd1f325b09e" }, "apps-home-dark" : { @@ -218,9 +228,19 @@ }, "apps-home-white" : { "name" : "apps-home-white", - "rule" : ".apps-home-white", + "rule" : ".apps-home-white, .phui-list-item-href:hover .apps-home-dark", "hash" : "3b034189ce2507d5acf06a018325524d" }, + "apps-info-sm-dark" : { + "name" : "apps-info-sm-dark", + "rule" : ".apps-info-sm-dark", + "hash" : "17fd77d321b15ba97b1f3ebca0bd7aae" + }, + "apps-info-sm-white" : { + "name" : "apps-info-sm-white", + "rule" : ".apps-info-sm-white, .phui-list-item-href:hover .apps-info-sm-dark", + "hash" : "6c64dfe542b04f8416425367a846405a" + }, "apps-legalpad-dark" : { "name" : "apps-legalpad-dark", "rule" : ".apps-legalpad-dark", @@ -228,7 +248,7 @@ }, "apps-legalpad-white" : { "name" : "apps-legalpad-white", - "rule" : ".apps-legalpad-white", + "rule" : ".apps-legalpad-white, .phui-list-item-href:hover .apps-legalpad-dark", "hash" : "bb0f8fa6ff6a0b4f810923751f9ebd55" }, "apps-logo-dark" : { @@ -238,9 +258,19 @@ }, "apps-logo-white" : { "name" : "apps-logo-white", - "rule" : ".apps-logo-white", + "rule" : ".apps-logo-white, .phui-list-item-href:hover .apps-logo-dark", "hash" : "885b8ab26f2873d326e2173fad44dce7" }, + "apps-logout-sm-dark" : { + "name" : "apps-logout-sm-dark", + "rule" : ".apps-logout-sm-dark", + "hash" : "91aee4598898303c499059bf0eef89df" + }, + "apps-logout-sm-white" : { + "name" : "apps-logout-sm-white", + "rule" : ".apps-logout-sm-white, .phui-list-item-href:hover .apps-logout-sm-dark", + "hash" : "f59a87ceda18a9debe58eb063005183c" + }, "apps-macro-dark" : { "name" : "apps-macro-dark", "rule" : ".apps-macro-dark", @@ -248,7 +278,7 @@ }, "apps-macro-white" : { "name" : "apps-macro-white", - "rule" : ".apps-macro-white", + "rule" : ".apps-macro-white, .phui-list-item-href:hover .apps-macro-dark", "hash" : "81e36ce1cbf39a8c33c5d4f5b01c61dc" }, "apps-mail-dark" : { @@ -258,7 +288,7 @@ }, "apps-mail-white" : { "name" : "apps-mail-white", - "rule" : ".apps-mail-white", + "rule" : ".apps-mail-white, .phui-list-item-href:hover .apps-mail-dark", "hash" : "83a336b8f0cf4fc29e8fad4418b453a5" }, "apps-maniphest-dark" : { @@ -268,7 +298,7 @@ }, "apps-maniphest-white" : { "name" : "apps-maniphest-white", - "rule" : ".apps-maniphest-white", + "rule" : ".apps-maniphest-white, .phui-list-item-href:hover .apps-maniphest-dark", "hash" : "246e1edc77f59f5c5847c5e99bf725f1" }, "apps-metamta-dark" : { @@ -278,7 +308,7 @@ }, "apps-metamta-white" : { "name" : "apps-metamta-white", - "rule" : ".apps-metamta-white", + "rule" : ".apps-metamta-white, .phui-list-item-href:hover .apps-metamta-dark", "hash" : "1664f51de85251cf358b9fad3623d1d5" }, "apps-new-dark" : { @@ -286,9 +316,19 @@ "rule" : ".apps-new-dark", "hash" : "39f2b8d62c4595dc653badf9f11a08d7" }, + "apps-new-sm-dark" : { + "name" : "apps-new-sm-dark", + "rule" : ".apps-new-sm-dark", + "hash" : "05a66a5134ed52c1c92af03aa5340d13" + }, + "apps-new-sm-white" : { + "name" : "apps-new-sm-white", + "rule" : ".apps-new-sm-white, .phui-list-item-href:hover .apps-new-sm-dark", + "hash" : "915ae9ea3fa178c6bb78096e8d7b985c" + }, "apps-new-white" : { "name" : "apps-new-white", - "rule" : ".apps-new-white", + "rule" : ".apps-new-white, .phui-list-item-href:hover .apps-new-dark", "hash" : "dc5ce42c7d60e6ba37b07631b3c3280a" }, "apps-nuance-dark" : { @@ -298,7 +338,7 @@ }, "apps-nuance-white" : { "name" : "apps-nuance-white", - "rule" : ".apps-nuance-white", + "rule" : ".apps-nuance-white, .phui-list-item-href:hover .apps-nuance-dark", "hash" : "368f1241b438492642520a037a1ac294" }, "apps-owners-dark" : { @@ -308,9 +348,19 @@ }, "apps-owners-white" : { "name" : "apps-owners-white", - "rule" : ".apps-owners-white", + "rule" : ".apps-owners-white, .phui-list-item-href:hover .apps-owners-dark", "hash" : "7f409576c8ddab7a513313c301c61683" }, + "apps-passphrase-dark" : { + "name" : "apps-passphrase-dark", + "rule" : ".apps-passphrase-dark", + "hash" : "1ee5fc48415e60795f6f00422f72e170" + }, + "apps-passphrase-white" : { + "name" : "apps-passphrase-white", + "rule" : ".apps-passphrase-white, .phui-list-item-href:hover .apps-passphrase-dark", + "hash" : "a6dc3274fd093b0545937d6a495b391f" + }, "apps-paste-dark" : { "name" : "apps-paste-dark", "rule" : ".apps-paste-dark", @@ -318,7 +368,7 @@ }, "apps-paste-white" : { "name" : "apps-paste-white", - "rule" : ".apps-paste-white", + "rule" : ".apps-paste-white, .phui-list-item-href:hover .apps-paste-dark", "hash" : "ed5ec1ee39cc2a01f568f91c72141bfb" }, "apps-people-dark" : { @@ -328,7 +378,7 @@ }, "apps-people-white" : { "name" : "apps-people-white", - "rule" : ".apps-people-white", + "rule" : ".apps-people-white, .phui-list-item-href:hover .apps-people-dark", "hash" : "64bbb2b31873a206a08a50bd14f80c3e" }, "apps-phage-dark" : { @@ -338,7 +388,7 @@ }, "apps-phage-white" : { "name" : "apps-phage-white", - "rule" : ".apps-phage-white", + "rule" : ".apps-phage-white, .phui-list-item-href:hover .apps-phage-dark", "hash" : "19c313baea3de0f7c6ab13eb5d1931a9" }, "apps-phame-dark" : { @@ -348,7 +398,7 @@ }, "apps-phame-white" : { "name" : "apps-phame-white", - "rule" : ".apps-phame-white", + "rule" : ".apps-phame-white, .phui-list-item-href:hover .apps-phame-dark", "hash" : "3e8c589577b39c5bb6cb416394ca995e" }, "apps-phid-dark" : { @@ -358,7 +408,7 @@ }, "apps-phid-white" : { "name" : "apps-phid-white", - "rule" : ".apps-phid-white", + "rule" : ".apps-phid-white, .phui-list-item-href:hover .apps-phid-dark", "hash" : "35cabe4468dc7ad83ca814d14584da46" }, "apps-phlux-dark" : { @@ -368,7 +418,7 @@ }, "apps-phlux-white" : { "name" : "apps-phlux-white", - "rule" : ".apps-phlux-white", + "rule" : ".apps-phlux-white, .phui-list-item-href:hover .apps-phlux-dark", "hash" : "e45320aa27d6221b513c946a43025e55" }, "apps-pholio-dark" : { @@ -378,17 +428,17 @@ }, "apps-pholio-white" : { "name" : "apps-pholio-white", - "rule" : ".apps-pholio-white", + "rule" : ".apps-pholio-white, .phui-list-item-href:hover .apps-pholio-dark", "hash" : "aafd79e9702c566c91345ba19eb4982b" }, "apps-phortune-dark" : { "name" : "apps-phortune-dark", "rule" : ".apps-phortune-dark", - "hash" : "2ddad4cb503d2b3ae7662717170684ce" + "hash" : "c940ea7f44e7b6c21ddb2fa05bd7a408" }, "apps-phortune-white" : { "name" : "apps-phortune-white", - "rule" : ".apps-phortune-white", + "rule" : ".apps-phortune-white, .phui-list-item-href:hover .apps-phortune-dark", "hash" : "6722013edddd2b302420727a52aec7a0" }, "apps-phpast-dark" : { @@ -398,9 +448,19 @@ }, "apps-phpast-white" : { "name" : "apps-phpast-white", - "rule" : ".apps-phpast-white", + "rule" : ".apps-phpast-white, .phui-list-item-href:hover .apps-phpast-dark", "hash" : "3c72bb3a2701584538d677e9a792837e" }, + "apps-phragment-dark" : { + "name" : "apps-phragment-dark", + "rule" : ".apps-phragment-dark", + "hash" : "436e9896b3ecc51019c5b2bdfba0bebe" + }, + "apps-phragment-white" : { + "name" : "apps-phragment-white", + "rule" : ".apps-phragment-white, .phui-list-item-href:hover .apps-phragment-dark", + "hash" : "58bd0db2496cc31221c56b796d741d36" + }, "apps-phrequent-dark" : { "name" : "apps-phrequent-dark", "rule" : ".apps-phrequent-dark", @@ -408,7 +468,7 @@ }, "apps-phrequent-white" : { "name" : "apps-phrequent-white", - "rule" : ".apps-phrequent-white", + "rule" : ".apps-phrequent-white, .phui-list-item-href:hover .apps-phrequent-dark", "hash" : "3a5ab5724f2256b346987f478fac8ae8" }, "apps-phriction-dark" : { @@ -418,7 +478,7 @@ }, "apps-phriction-white" : { "name" : "apps-phriction-white", - "rule" : ".apps-phriction-white", + "rule" : ".apps-phriction-white, .phui-list-item-href:hover .apps-phriction-dark", "hash" : "a6717397b2e049a1eb77757ae6fcd012" }, "apps-policy-dark" : { @@ -428,7 +488,7 @@ }, "apps-policy-white" : { "name" : "apps-policy-white", - "rule" : ".apps-policy-white", + "rule" : ".apps-policy-white, .phui-list-item-href:hover .apps-policy-dark", "hash" : "53571de9f0025688d71f1f65aff23b00" }, "apps-ponder-dark" : { @@ -438,7 +498,7 @@ }, "apps-ponder-white" : { "name" : "apps-ponder-white", - "rule" : ".apps-ponder-white", + "rule" : ".apps-ponder-white, .phui-list-item-href:hover .apps-ponder-dark", "hash" : "8177095a1bae6421f67395db4bc9fbd1" }, "apps-power-dark" : { @@ -448,7 +508,7 @@ }, "apps-power-white" : { "name" : "apps-power-white", - "rule" : ".apps-power-white", + "rule" : ".apps-power-white, .phui-list-item-href:hover .apps-power-dark", "hash" : "fbc21eb5f7cd1c4b9944335cad012388" }, "apps-projects-dark" : { @@ -458,7 +518,7 @@ }, "apps-projects-white" : { "name" : "apps-projects-white", - "rule" : ".apps-projects-white", + "rule" : ".apps-projects-white, .phui-list-item-href:hover .apps-projects-dark", "hash" : "84063e2f20613b2f36c232a33b633f4b" }, "apps-releeph-dark" : { @@ -468,7 +528,7 @@ }, "apps-releeph-white" : { "name" : "apps-releeph-white", - "rule" : ".apps-releeph-white", + "rule" : ".apps-releeph-white, .phui-list-item-href:hover .apps-releeph-dark", "hash" : "a0b09bcb3ac6f654485dc8b8c100e769" }, "apps-repositories-dark" : { @@ -478,7 +538,7 @@ }, "apps-repositories-white" : { "name" : "apps-repositories-white", - "rule" : ".apps-repositories-white", + "rule" : ".apps-repositories-white, .phui-list-item-href:hover .apps-repositories-dark", "hash" : "848bb97c36e927353a097f2e7312841d" }, "apps-search-dark" : { @@ -488,7 +548,7 @@ }, "apps-search-white" : { "name" : "apps-search-white", - "rule" : ".apps-search-white", + "rule" : ".apps-search-white, .phui-list-item-href:hover .apps-search-dark", "hash" : "97fed01d5d3295baa51cab8e2e6721ab" }, "apps-settings-dark" : { @@ -496,9 +556,19 @@ "rule" : ".apps-settings-dark", "hash" : "a5ebbb89a36998a4f4a42e8ae43eedd2" }, + "apps-settings-sm-dark" : { + "name" : "apps-settings-sm-dark", + "rule" : ".apps-settings-sm-dark", + "hash" : "584388f560e271e86f02a3cf2ffff94c" + }, + "apps-settings-sm-white" : { + "name" : "apps-settings-sm-white", + "rule" : ".apps-settings-sm-white, .phui-list-item-href:hover .apps-settings-sm-dark", + "hash" : "66a979f4618a3c8fa916fa13382c590e" + }, "apps-settings-white" : { "name" : "apps-settings-white", - "rule" : ".apps-settings-white", + "rule" : ".apps-settings-white, .phui-list-item-href:hover .apps-settings-dark", "hash" : "ec11387921da7fef5a3b5e8e160a8565" }, "apps-setup-dark" : { @@ -508,7 +578,7 @@ }, "apps-setup-white" : { "name" : "apps-setup-white", - "rule" : ".apps-setup-white", + "rule" : ".apps-setup-white, .phui-list-item-href:hover .apps-setup-dark", "hash" : "2625facb1900aee48168386418876f4f" }, "apps-slowvote-dark" : { @@ -518,7 +588,7 @@ }, "apps-slowvote-white" : { "name" : "apps-slowvote-white", - "rule" : ".apps-slowvote-white", + "rule" : ".apps-slowvote-white, .phui-list-item-href:hover .apps-slowvote-dark", "hash" : "963b5c1bdad04d944f71a56e05f3160f" }, "apps-token-dark" : { @@ -528,7 +598,7 @@ }, "apps-token-white" : { "name" : "apps-token-white", - "rule" : ".apps-token-white", + "rule" : ".apps-token-white, .phui-list-item-href:hover .apps-token-dark", "hash" : "f9704614e4690af63f8b2fa1bce1be88" }, "apps-uiexamples-dark" : { @@ -538,7 +608,7 @@ }, "apps-uiexamples-white" : { "name" : "apps-uiexamples-white", - "rule" : ".apps-uiexamples-white", + "rule" : ".apps-uiexamples-white, .phui-list-item-href:hover .apps-uiexamples-dark", "hash" : "d1a649462c5fd374059011bb7f705d5b" }, "apps-workphlow-dark" : { @@ -548,7 +618,7 @@ }, "apps-workphlow-white" : { "name" : "apps-workphlow-white", - "rule" : ".apps-workphlow-white", + "rule" : ".apps-workphlow-white, .phui-list-item-href:hover .apps-workphlow-dark", "hash" : "5a5647193d080169b14d631e6fd6a702" }, "apps-xhprof-dark" : { @@ -558,7 +628,7 @@ }, "apps-xhprof-white" : { "name" : "apps-xhprof-white", - "rule" : ".apps-xhprof-white", + "rule" : ".apps-xhprof-white, .phui-list-item-href:hover .apps-xhprof-dark", "hash" : "e2381887939895b4473a2a7d1423cdb6" } }, diff --git a/resources/sprite/manifest/icons.json b/resources/sprite/manifest/icons.json index a4ad4e0021..c405eec7aa 100644 --- a/resources/sprite/manifest/icons.json +++ b/resources/sprite/manifest/icons.json @@ -61,6 +61,21 @@ "rule" : ".icons-attach-white, .device-desktop .phabricator-action-view:hover .icons-attach, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-attach", "hash" : "632fac3ff2d07194557004d20a17d9ba" }, + "icons-backward" : { + "name" : "icons-backward", + "rule" : ".icons-backward", + "hash" : "c420a143edd4c6fa9062bb2901cff9c1" + }, + "icons-backward-grey" : { + "name" : "icons-backward-grey", + "rule" : ".icons-backward-grey", + "hash" : "27d52f4251b240442b4a25b8b1476127" + }, + "icons-backward-white" : { + "name" : "icons-backward-white", + "rule" : ".icons-backward-white, .device-desktop .phabricator-action-view:hover .icons-backward, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-backward", + "hash" : "5bfa565da9d448b9c6558c364a075c92" + }, "icons-blame" : { "name" : "icons-blame", "rule" : ".icons-blame", @@ -481,6 +496,21 @@ "rule" : ".icons-fork-white, .device-desktop .phabricator-action-view:hover .icons-fork, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-fork", "hash" : "1170a86082ad4fce9065b73f4cae8c9e" }, + "icons-forward" : { + "name" : "icons-forward", + "rule" : ".icons-forward", + "hash" : "8d81decaf0970c15a3deb38e707f8ac4" + }, + "icons-forward-grey" : { + "name" : "icons-forward-grey", + "rule" : ".icons-forward-grey", + "hash" : "f7ed3685361986f40f967f1e93131407" + }, + "icons-forward-white" : { + "name" : "icons-forward-white", + "rule" : ".icons-forward-white, .device-desktop .phabricator-action-view:hover .icons-forward, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-forward", + "hash" : "596134836c31699ae53bdee43b8e78a8" + }, "icons-herald" : { "name" : "icons-herald", "rule" : ".icons-herald", @@ -796,6 +826,51 @@ "rule" : ".icons-normal-priority-white, .device-desktop .phabricator-action-view:hover .icons-normal-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-normal-priority", "hash" : "fd9a696de97bde0c45d57e77fa2e7c4a" }, + "icons-octocat" : { + "name" : "icons-octocat", + "rule" : ".icons-octocat", + "hash" : "72ac2c8c30caec1d591cf1073c26cc61" + }, + "icons-octocat-grey" : { + "name" : "icons-octocat-grey", + "rule" : ".icons-octocat-grey", + "hash" : "808474ec2801f155663054bf5e78cba4" + }, + "icons-octocat-white" : { + "name" : "icons-octocat-white", + "rule" : ".icons-octocat-white, .device-desktop .phabricator-action-view:hover .icons-octocat, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-octocat", + "hash" : "bda54b23d423de7b35a964ce9fdea4f2" + }, + "icons-ok" : { + "name" : "icons-ok", + "rule" : ".icons-ok", + "hash" : "6b2617f884e0d2ccb86dcef002d589ba" + }, + "icons-ok-grey" : { + "name" : "icons-ok-grey", + "rule" : ".icons-ok-grey", + "hash" : "9cfda8cc4cb2dbca5f99a24efa5705a3" + }, + "icons-ok-white" : { + "name" : "icons-ok-white", + "rule" : ".icons-ok-white, .device-desktop .phabricator-action-view:hover .icons-ok, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-ok", + "hash" : "cabecea04bc54ca57214fa53f6eb5118" + }, + "icons-pause" : { + "name" : "icons-pause", + "rule" : ".icons-pause", + "hash" : "d9af6b0b4a00e322870b397a08149695" + }, + "icons-pause-grey" : { + "name" : "icons-pause-grey", + "rule" : ".icons-pause-grey", + "hash" : "e85f9238f74f4f5187ce3f9bb5794660" + }, + "icons-pause-white" : { + "name" : "icons-pause-white", + "rule" : ".icons-pause-white, .device-desktop .phabricator-action-view:hover .icons-pause, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-pause", + "hash" : "857928e62d0107c05ee436b7394efe0e" + }, "icons-perflab" : { "name" : "icons-perflab", "rule" : ".icons-perflab", @@ -811,6 +886,36 @@ "rule" : ".icons-perflab-white, .device-desktop .phabricator-action-view:hover .icons-perflab, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-perflab", "hash" : "386f22dd33e0f5e48e294582ca7e1913" }, + "icons-play" : { + "name" : "icons-play", + "rule" : ".icons-play", + "hash" : "f6bbec44291d636aa3236a49e46d161d" + }, + "icons-play-grey" : { + "name" : "icons-play-grey", + "rule" : ".icons-play-grey", + "hash" : "2eb3a9cdc87cd5b6ee914e13e3a8f36a" + }, + "icons-play-white" : { + "name" : "icons-play-white", + "rule" : ".icons-play-white, .device-desktop .phabricator-action-view:hover .icons-play, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-play", + "hash" : "da9a7f0208135139dbaa02d4e27be9c7" + }, + "icons-popout" : { + "name" : "icons-popout", + "rule" : ".icons-popout", + "hash" : "83c413187ec4d5730203494bdb70a6f2" + }, + "icons-popout-grey" : { + "name" : "icons-popout-grey", + "rule" : ".icons-popout-grey", + "hash" : "203aaa1696fe7fdf7f287d5f2908b2ef" + }, + "icons-popout-white" : { + "name" : "icons-popout-white", + "rule" : ".icons-popout-white, .device-desktop .phabricator-action-view:hover .icons-popout, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-popout", + "hash" : "924b3bf8f4768bf647237de4eaa3613e" + }, "icons-preview" : { "name" : "icons-preview", "rule" : ".icons-preview", @@ -916,6 +1021,21 @@ "rule" : ".icons-start-sandcastle-white, .device-desktop .phabricator-action-view:hover .icons-start-sandcastle, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-start-sandcastle", "hash" : "12f3b8ededfbe6599bdce086261cc17a" }, + "icons-stop" : { + "name" : "icons-stop", + "rule" : ".icons-stop", + "hash" : "3a1d43a53d97069562a4634a38ddb271" + }, + "icons-stop-grey" : { + "name" : "icons-stop-grey", + "rule" : ".icons-stop-grey", + "hash" : "f25ee017e941304a87d8f197a5eea0c9" + }, + "icons-stop-white" : { + "name" : "icons-stop-white", + "rule" : ".icons-stop-white, .device-desktop .phabricator-action-view:hover .icons-stop, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-stop", + "hash" : "3f5b089c71f37acbe715b6afedf480de" + }, "icons-tag" : { "name" : "icons-tag", "rule" : ".icons-tag", diff --git a/resources/sprite/manifest/main-header.json b/resources/sprite/manifest/main-header.json new file mode 100644 index 0000000000..1660c55aa2 --- /dev/null +++ b/resources/sprite/manifest/main-header.json @@ -0,0 +1,55 @@ +{ + "version" : 1, + "sprites" : { + "main-header-applebloom" : { + "name" : "main-header-applebloom", + "rule" : ".main-header-applebloom", + "hash" : "64822776b129e724709983db1ac5a712" + }, + "main-header-blue" : { + "name" : "main-header-blue", + "rule" : ".main-header-blue", + "hash" : "b5dd317b7bd35e0592b3f4b66267437c" + }, + "main-header-dark" : { + "name" : "main-header-dark", + "rule" : ".main-header-dark", + "hash" : "817815d84c0c935b4875f6ecc7dbb526" + }, + "main-header-fluttershy" : { + "name" : "main-header-fluttershy", + "rule" : ".main-header-fluttershy", + "hash" : "55d4e9f813cf354693290105cf83cf78" + }, + "main-header-green" : { + "name" : "main-header-green", + "rule" : ".main-header-green", + "hash" : "c230f09e307d167fab3ea0d8c3b33755" + }, + "main-header-nightmaremoon" : { + "name" : "main-header-nightmaremoon", + "rule" : ".main-header-nightmaremoon", + "hash" : "9e7cc7b18d2132d2dd47586ba0cd6400" + }, + "main-header-red" : { + "name" : "main-header-red", + "rule" : ".main-header-red", + "hash" : "3f12cc098afddb8e6c830ef761dcaa61" + }, + "main-header-scootaloo" : { + "name" : "main-header-scootaloo", + "rule" : ".main-header-scootaloo", + "hash" : "8cedc359dccab1bbd49cbc69940f566a" + }, + "main-header-yellow" : { + "name" : "main-header-yellow", + "rule" : ".main-header-yellow", + "hash" : "d920e70a6d2662cfb83e1d7e4b4000fd" + } + }, + "scales" : [ + 1 + ], + "header" : "\/**\n * @provides sprite-main-header-css\n * @generated\n *\/\n\n.sprite-main-header {\n background-image: url(\/rsrc\/image\/sprite-main-header.png);\n background-repeat: repeat-x;\n}\n\n\n", + "type" : "repeat-x" +} diff --git a/resources/sprite/manifest/status.json b/resources/sprite/manifest/status.json index 5a3d8a4210..befc1abae7 100644 --- a/resources/sprite/manifest/status.json +++ b/resources/sprite/manifest/status.json @@ -326,6 +326,16 @@ "rule" : ".status-policy-unknown-white, .dropdown-menu-item:hover .status-policy-unknown", "hash" : "98985bfa005672c4b88feaf88cfa72bc" }, + "status-policy-user" : { + "name" : "status-policy-user", + "rule" : ".status-policy-user", + "hash" : "6c21aa20866d5b86f074bdcf4f487d40" + }, + "status-policy-user-white" : { + "name" : "status-policy-user-white", + "rule" : ".status-policy-user-white", + "hash" : "97a569e973df3f1e4fe012adf216ca40" + }, "status-question" : { "name" : "status-question", "rule" : ".status-question", diff --git a/resources/sprite/status_1x/policy-user-white.png b/resources/sprite/status_1x/policy-user-white.png new file mode 100644 index 0000000000..2b4ae43457 Binary files /dev/null and b/resources/sprite/status_1x/policy-user-white.png differ diff --git a/resources/sprite/status_1x/policy-user.png b/resources/sprite/status_1x/policy-user.png new file mode 100644 index 0000000000..1a33858470 Binary files /dev/null and b/resources/sprite/status_1x/policy-user.png differ diff --git a/resources/sprite/status_2x/policy-user-white.png b/resources/sprite/status_2x/policy-user-white.png new file mode 100644 index 0000000000..913c892356 Binary files /dev/null and b/resources/sprite/status_2x/policy-user-white.png differ diff --git a/resources/sprite/status_2x/policy-user.png b/resources/sprite/status_2x/policy-user.png new file mode 100644 index 0000000000..3d6fcf050b Binary files /dev/null and b/resources/sprite/status_2x/policy-user.png differ diff --git a/resources/sql/autopatches/20140104.harbormastercmd.sql b/resources/sql/autopatches/20140104.harbormastercmd.sql new file mode 100644 index 0000000000..8d4d2f5125 --- /dev/null +++ b/resources/sql/autopatches/20140104.harbormastercmd.sql @@ -0,0 +1,18 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildcommand ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + targetPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + command VARCHAR(128) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_target` (targetPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_build + DROP cancelRequested; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildtarget + ADD targetStatus VARCHAR(64) NOT NULL COLLATE utf8_bin; + +UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildtarget + SET targetStatus = 'target/pending' WHERE targetStatus = ''; diff --git a/resources/sql/autopatches/20140106.macromailkey.1.sql b/resources/sql/autopatches/20140106.macromailkey.1.sql new file mode 100644 index 0000000000..39bdc9aed4 --- /dev/null +++ b/resources/sql/autopatches/20140106.macromailkey.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_file.file_imagemacro + ADD mailKey VARCHAR(20) NOT NULL COLLATE utf8_bin; diff --git a/resources/sql/autopatches/20140106.macromailkey.2.php b/resources/sql/autopatches/20140106.macromailkey.2.php new file mode 100644 index 0000000000..fdca4e4d59 --- /dev/null +++ b/resources/sql/autopatches/20140106.macromailkey.2.php @@ -0,0 +1,23 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $macro) { + $id = $macro->getID(); + + echo "Populating macro {$id}...\n"; + + if (!$macro->getMailKey()) { + queryfx( + $conn_w, + 'UPDATE %T SET mailKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); + } +} + +echo "Done.\n"; diff --git a/resources/sql/autopatches/20140108.ddbpname.1.sql b/resources/sql/autopatches/20140108.ddbpname.1.sql new file mode 100644 index 0000000000..3fe80875db --- /dev/null +++ b/resources/sql/autopatches/20140108.ddbpname.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_blueprint + ADD blueprintName VARCHAR(255) NOT NULL AFTER className; diff --git a/resources/sql/autopatches/20140108.ddbpname.2.php b/resources/sql/autopatches/20140108.ddbpname.2.php new file mode 100644 index 0000000000..ca7e3ef8b2 --- /dev/null +++ b/resources/sql/autopatches/20140108.ddbpname.2.php @@ -0,0 +1,23 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $blueprint) { + $id = $blueprint->getID(); + + echo "Populating blueprint {$id}...\n"; + + if (!strlen($blueprint->getBlueprintName())) { + queryfx( + $conn_w, + 'UPDATE %T SET blueprintName = %s WHERE id = %d', + $table->getTableName(), + pht('Blueprint %s', $id), + $id); + } +} + +echo "Done.\n"; diff --git a/resources/sql/autopatches/20140109.ddxactions.sql b/resources/sql/autopatches/20140109.ddxactions.sql new file mode 100644 index 0000000000..54e410021b --- /dev/null +++ b/resources/sql/autopatches/20140109.ddxactions.sql @@ -0,0 +1,21 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_blueprinttransaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentPHID VARCHAR(64) COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) + +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/autopatches/20140109.projectcolumnsdates.sql b/resources/sql/autopatches/20140109.projectcolumnsdates.sql new file mode 100644 index 0000000000..96dda544e4 --- /dev/null +++ b/resources/sql/autopatches/20140109.projectcolumnsdates.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_project.project_column + ADD dateCreated INT UNSIGNED NOT NULL; + +ALTER TABLE {$NAMESPACE}_project.project_column + ADD dateModified INT UNSIGNED NOT NULL; diff --git a/resources/sql/autopatches/20140113.legalpadsig.1.sql b/resources/sql/autopatches/20140113.legalpadsig.1.sql new file mode 100644 index 0000000000..52f0ccdd61 --- /dev/null +++ b/resources/sql/autopatches/20140113.legalpadsig.1.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD secretKey VARCHAR(20) NOT NULL COLLATE utf8_bin; + +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD verified TINYINT(1) DEFAULT 0; + +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD KEY `secretKey` (secretKey); diff --git a/resources/sql/autopatches/20140113.legalpadsig.2.php b/resources/sql/autopatches/20140113.legalpadsig.2.php new file mode 100644 index 0000000000..8b244e2480 --- /dev/null +++ b/resources/sql/autopatches/20140113.legalpadsig.2.php @@ -0,0 +1,23 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $sig) { + $id = $sig->getID(); + + echo "Populating signature {$id}...\n"; + + if (!$sig->getSecretKey()) { + queryfx( + $conn_w, + 'UPDATE %T SET secretKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); + } +} + +echo "Done.\n"; diff --git a/resources/sql/autopatches/20140115.auth.1.id.sql b/resources/sql/autopatches/20140115.auth.1.id.sql new file mode 100644 index 0000000000..ba173c126a --- /dev/null +++ b/resources/sql/autopatches/20140115.auth.1.id.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_user.phabricator_session + DROP PRIMARY KEY; + +ALTER TABLE {$NAMESPACE}_user.phabricator_session + ADD id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; + +ALTER TABLE {$NAMESPACE}_user.phabricator_session + ADD KEY `key_identity` (userPHID, type); diff --git a/resources/sql/autopatches/20140115.auth.2.expires.sql b/resources/sql/autopatches/20140115.auth.2.expires.sql new file mode 100644 index 0000000000..94b36af7da --- /dev/null +++ b/resources/sql/autopatches/20140115.auth.2.expires.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_user.phabricator_session + ADD sessionExpires INT UNSIGNED NOT NULL; + +UPDATE {$NAMESPACE}_user.phabricator_session + SET sessionExpires = UNIX_TIMESTAMP() + (60 * 60 * 24 * 30); + +ALTER TABLE {$NAMESPACE}_user.phabricator_session + ADD KEY `key_expires` (sessionExpires); diff --git a/resources/sql/autopatches/20140115.auth.3.unlimit.php b/resources/sql/autopatches/20140115.auth.3.unlimit.php new file mode 100644 index 0000000000..80c316f64a --- /dev/null +++ b/resources/sql/autopatches/20140115.auth.3.unlimit.php @@ -0,0 +1,26 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($session_table) as $session) { + $id = $session->getID(); + + echo "Migrating session {$id}...\n"; + $old_type = $session->getType(); + $new_type = preg_replace('/-.*$/', '', $old_type); + + if ($old_type !== $new_type) { + queryfx( + $conn_w, + 'UPDATE %T SET type = %s WHERE id = %d', + $session_table->getTableName(), + $new_type, + $id); + } +} + +echo "Done.\n"; diff --git a/resources/sql/autopatches/20140115.legalpadsigkey.sql b/resources/sql/autopatches/20140115.legalpadsigkey.sql new file mode 100644 index 0000000000..c02873bebc --- /dev/null +++ b/resources/sql/autopatches/20140115.legalpadsigkey.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + DROP KEY `key_document`; + +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD KEY `key_document` (`documentPHID`,`signerPHID`, `documentVersion`); diff --git a/resources/sql/autopatches/20140116.reporefcursor.sql b/resources/sql/autopatches/20140116.reporefcursor.sql new file mode 100644 index 0000000000..e9593f7abc --- /dev/null +++ b/resources/sql/autopatches/20140116.reporefcursor.sql @@ -0,0 +1,11 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_refcursor ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + repositoryPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + refType VARCHAR(32) NOT NULL COLLATE utf8_bin, + refNameHash VARCHAR(12) NOT NULL COLLATE latin1_bin, + refNameRaw LONGTEXT NOT NULL COLLATE latin1_bin, + refNameEncoding VARCHAR(16) COLLATE utf8_bin, + commitIdentifier VARCHAR(40) NOT NULL COLLATE utf8_bin, + + KEY `key_cursor` (repositoryPHID, refType, refNameHash) +) ENGINE=InnoDB, COLLATE=utf8_general_ci; diff --git a/resources/sql/autopatches/20140126.diff.1.parentrevisionid.sql b/resources/sql/autopatches/20140126.diff.1.parentrevisionid.sql new file mode 100644 index 0000000000..4468e240b4 --- /dev/null +++ b/resources/sql/autopatches/20140126.diff.1.parentrevisionid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_diff + DROP COLUMN parentRevisionID; diff --git a/resources/sql/autopatches/20140126.diff.2.repositoryphid.sql b/resources/sql/autopatches/20140126.diff.2.repositoryphid.sql new file mode 100644 index 0000000000..336c15f741 --- /dev/null +++ b/resources/sql/autopatches/20140126.diff.2.repositoryphid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_diff + ADD COLUMN repositoryPHID VARCHAR(64) COLLATE utf8_bin AFTER authorPHID; diff --git a/resources/sql/autopatches/20140130.dash.1.board.sql b/resources/sql/autopatches/20140130.dash.1.board.sql new file mode 100644 index 0000000000..51f82c02a5 --- /dev/null +++ b/resources/sql/autopatches/20140130.dash.1.board.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_dashboard.dashboard ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE=utf8_general_ci; diff --git a/resources/sql/autopatches/20140130.dash.2.panel.sql b/resources/sql/autopatches/20140130.dash.2.panel.sql new file mode 100644 index 0000000000..3176ed3f63 --- /dev/null +++ b/resources/sql/autopatches/20140130.dash.2.panel.sql @@ -0,0 +1,11 @@ +CREATE TABLE {$NAMESPACE}_dashboard.dashboard_panel ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + properties LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE=utf8_general_ci; diff --git a/resources/sql/autopatches/20140130.mail.1.retry.sql b/resources/sql/autopatches/20140130.mail.1.retry.sql new file mode 100644 index 0000000000..42ff5afab2 --- /dev/null +++ b/resources/sql/autopatches/20140130.mail.1.retry.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_metamta.metamta_mail + DROP COLUMN retryCount; diff --git a/resources/sql/autopatches/20140130.mail.2.next.sql b/resources/sql/autopatches/20140130.mail.2.next.sql new file mode 100644 index 0000000000..78e5693cf1 --- /dev/null +++ b/resources/sql/autopatches/20140130.mail.2.next.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_metamta.metamta_mail + DROP COLUMN nextRetry; diff --git a/resources/sql/patches/20131107.buildlog.sql b/resources/sql/patches/20131107.buildlog.sql new file mode 100644 index 0000000000..e761b46c9a --- /dev/null +++ b/resources/sql/patches/20131107.buildlog.sql @@ -0,0 +1,26 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + buildPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + buildStepPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + logSource VARCHAR(255) NULL COLLATE utf8_bin, + logType VARCHAR(255) NULL COLLATE utf8_bin, + duration INT UNSIGNED NULL, + live BOOLEAN NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_build` (buildPHID, buildStepPHID), + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_build +ADD COLUMN cancelRequested BOOLEAN NOT NULL; + +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + logID INT UNSIGNED NOT NULL COLLATE utf8_bin, + encoding VARCHAR(30) NOT NULL COLLATE utf8_bin, + size LONG NULL, + chunk LONGBLOB NOT NULL, + KEY `key_log` (logID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131112.userverified.1.col.sql b/resources/sql/patches/20131112.userverified.1.col.sql new file mode 100644 index 0000000000..0612e79825 --- /dev/null +++ b/resources/sql/patches/20131112.userverified.1.col.sql @@ -0,0 +1,10 @@ +ALTER TABLE {$NAMESPACE}_user.user + ADD isEmailVerified INT UNSIGNED NOT NULL; + +ALTER TABLE {$NAMESPACE}_user.user + ADD isApproved INT UNSIGNED NOT NULL; + +ALTER TABLE {$NAMESPACE}_user.user + ADD KEY `key_approved` (isApproved); + +UPDATE {$NAMESPACE}_user.user SET isApproved = 1; diff --git a/resources/sql/patches/20131112.userverified.2.mig.php b/resources/sql/patches/20131112.userverified.2.mig.php new file mode 100644 index 0000000000..381fc966ab --- /dev/null +++ b/resources/sql/patches/20131112.userverified.2.mig.php @@ -0,0 +1,33 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $user) { + $username = $user->getUsername(); + echo "Migrating {$username}...\n"; + if ($user->getIsEmailVerified()) { + // Email already verified. + continue; + } + + $primary = $user->loadPrimaryEmail(); + if (!$primary) { + // No primary email. + continue; + } + + if (!$primary->getIsVerified()) { + // Primary email not verified. + continue; + } + + // Primary email is verified, so mark the account as verified. + queryfx( + $conn_w, + 'UPDATE %T SET isEmailVerified = 1 WHERE id = %d', + $table->getTableName(), + $user->getID()); +} + +echo "Done.\n"; diff --git a/resources/sql/patches/20131118.ownerorder.php b/resources/sql/patches/20131118.ownerorder.php new file mode 100644 index 0000000000..61192c8c27 --- /dev/null +++ b/resources/sql/patches/20131118.ownerorder.php @@ -0,0 +1,38 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $task) { + $id = $task->getID(); + + echo "Checking task T{$id}...\n"; + $owner_phid = $task->getOwnerPHID(); + + if (!$owner_phid && !$task->getOwnerOrdering()) { + // No owner and no ordering; we're all set. + continue; + } + + $owner_handle = id(new PhabricatorHandleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($owner_phid)) + ->executeOne(); + + if ($owner_handle) { + $value = $owner_handle->getName(); + } else { + $value = null; + } + + if ($value !== $task->getOwnerOrdering()) { + queryfx( + $conn_w, + 'UPDATE %T SET ownerOrdering = %ns WHERE id = %d', + $table->getTableName(), + $value, + $task->getID()); + } +} + +echo "Done.\n"; diff --git a/resources/sql/patches/20131119.passphrase.sql b/resources/sql/patches/20131119.passphrase.sql new file mode 100644 index 0000000000..3e1761867c --- /dev/null +++ b/resources/sql/patches/20131119.passphrase.sql @@ -0,0 +1,46 @@ +CREATE TABLE {$NAMESPACE}_passphrase.passphrase_credential ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL, + credentialType VARCHAR(64) NOT NULL COLLATE utf8_bin, + providesType VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + description LONGTEXT NOT NULL COLLATE utf8_bin, + username VARCHAR(255) NOT NULL, + secretID INT UNSIGNED, + isDestroyed BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_type` (credentialType), + KEY `key_provides` (providesType), + UNIQUE KEY `key_secret` (secretID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_passphrase.passphrase_secret ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + secretData LONGBLOB NOT NULL +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_passphrase.passphrase_credentialtransaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentPHID VARCHAR(64) COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131120.nuancesourcetype.sql b/resources/sql/patches/20131120.nuancesourcetype.sql new file mode 100644 index 0000000000..14133e605e --- /dev/null +++ b/resources/sql/patches/20131120.nuancesourcetype.sql @@ -0,0 +1,11 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_source + DROP KEY key_type; + +ALTER TABLE {$NAMESPACE}_nuance.nuance_source + DROP COLUMN type; + +ALTER TABLE {$NAMESPACE}_nuance.nuance_source + ADD type VARCHAR(32) NOT NULL COLLATE utf8_bin AFTER name; + +ALTER TABLE {$NAMESPACE}_nuance.nuance_source + ADD KEY `key_type` (type, dateModified); diff --git a/resources/sql/patches/20131121.passphraseedge.sql b/resources/sql/patches/20131121.passphraseedge.sql new file mode 100644 index 0000000000..38cfdb0453 --- /dev/null +++ b/resources/sql/patches/20131121.passphraseedge.sql @@ -0,0 +1,15 @@ +CREATE TABLE {$NAMESPACE}_passphrase.edge ( + src VARCHAR(64) NOT NULL COLLATE utf8_bin, + type VARCHAR(64) NOT NULL COLLATE utf8_bin, + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY (src, type, dateCreated, seq) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_passphrase.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE utf8_bin +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131121.repocredentials.1.col.sql b/resources/sql/patches/20131121.repocredentials.1.col.sql new file mode 100644 index 0000000000..9471c7189b --- /dev/null +++ b/resources/sql/patches/20131121.repocredentials.1.col.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository + ADD credentialPHID VARCHAR(64) COLLATE utf8_bin; diff --git a/resources/sql/patches/20131121.repocredentials.2.mig.php b/resources/sql/patches/20131121.repocredentials.2.mig.php new file mode 100644 index 0000000000..398401d3ac --- /dev/null +++ b/resources/sql/patches/20131121.repocredentials.2.mig.php @@ -0,0 +1,114 @@ +establishConnection('w'); +$viewer = PhabricatorUser::getOmnipotentUser(); + +$map = array(); +foreach (new LiskMigrationIterator($table) as $repository) { + $callsign = $repository->getCallsign(); + echo "Examining repository {$callsign}...\n"; + + if ($repository->getCredentialPHID()) { + echo "...already has a Credential.\n"; + continue; + } + + $raw_uri = $repository->getRemoteURI(); + if (!$raw_uri) { + echo "...no remote URI.\n"; + continue; + } + + $uri = new PhutilURI($raw_uri); + + $proto = strtolower($uri->getProtocol()); + if ($proto == 'http' || $proto == 'https' || $proto == 'svn') { + $username = $repository->getDetail('http-login'); + $secret = $repository->getDetail('http-pass'); + $type = PassphraseCredentialTypePassword::CREDENTIAL_TYPE; + } else { + $username = $repository->getDetail('ssh-login'); + if (!$username) { + // If there's no explicit username, check for one in the URI. This is + // possible with older repositories. + $username = $uri->getUser(); + if (!$username) { + // Also check for a Git/SCP-style URI. + $git_uri = new PhutilGitURI($raw_uri); + $username = $git_uri->getUser(); + } + } + $file = $repository->getDetail('ssh-keyfile'); + if ($file) { + $secret = $file; + $type = PassphraseCredentialTypeSSHPrivateKeyFile::CREDENTIAL_TYPE; + } else { + $secret = $repository->getDetail('ssh-key'); + $type = PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE; + } + } + + if (!$username || !$secret) { + echo "...no credentials set.\n"; + continue; + } + + $map[$type][$username][$secret][] = $repository; + echo "...will migrate.\n"; +} + +$passphrase = new PassphraseSecret(); +$passphrase->openTransaction(); +$table->openTransaction(); + +foreach ($map as $credential_type => $credential_usernames) { + $type = PassphraseCredentialType::getTypeByConstant($credential_type); + foreach ($credential_usernames as $username => $credential_secrets) { + foreach ($credential_secrets as $secret_plaintext => $repositories) { + $callsigns = mpull($repositories, 'getCallsign'); + $name = pht( + 'Migrated Repository Credential (%s)', + phutil_utf8_shorten(implode(', ', $callsigns), 128)); + + echo "Creating: {$name}...\n"; + + $secret = id(new PassphraseSecret()) + ->setSecretData($secret_plaintext) + ->save(); + + $secret_id = $secret->getID(); + + $credential = PassphraseCredential::initializeNewCredential($viewer) + ->setCredentialType($type->getCredentialType()) + ->setProvidesType($type->getProvidesType()) + ->setViewPolicy(PhabricatorPolicies::POLICY_ADMIN) + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) + ->setName($name) + ->setUsername($username) + ->setSecretID($secret_id) + ->save(); + + foreach ($repositories as $repository) { + queryfx( + $conn_w, + 'UPDATE %T SET credentialPHID = %s WHERE id = %d', + $table->getTableName(), + $credential->getPHID(), + $repository->getID()); + + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_USES_CREDENTIAL; + + id(new PhabricatorEdgeEditor()) + ->setActor($viewer) + ->addEdge($repository->getPHID(), $edge_type, $credential->getPHID()) + ->save(); + } + } + } +} + +$table->saveTransaction(); +$passphrase->saveTransaction(); + +echo "Done.\n"; diff --git a/resources/sql/patches/20131122.repomirror.sql b/resources/sql/patches/20131122.repomirror.sql new file mode 100644 index 0000000000..38d2bd55f9 --- /dev/null +++ b/resources/sql/patches/20131122.repomirror.sql @@ -0,0 +1,13 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_mirror ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + repositoryPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + remoteURI VARCHAR(255) NOT NULL COLLATE utf8_bin, + credentialPHID VARCHAR(64) COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_repository` (repositoryPHID) + +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131123.drydockblueprintpolicy.sql b/resources/sql/patches/20131123.drydockblueprintpolicy.sql new file mode 100644 index 0000000000..79f778c500 --- /dev/null +++ b/resources/sql/patches/20131123.drydockblueprintpolicy.sql @@ -0,0 +1,11 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_blueprint ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + className VARCHAR(255) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL, + editPolicy VARCHAR(64) NOT NULL, + details LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131129.drydockresourceblueprint.sql b/resources/sql/patches/20131129.drydockresourceblueprint.sql new file mode 100644 index 0000000000..d4b8c528ed --- /dev/null +++ b/resources/sql/patches/20131129.drydockresourceblueprint.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_resource +ADD COLUMN blueprintPHID VARCHAR(64) NOT NULL COLLATE utf8_bin; + +ALTER TABLE {$NAMESPACE}_drydock.drydock_resource +DROP COLUMN blueprintClass; diff --git a/resources/sql/patches/20131204.pushlog.sql b/resources/sql/patches/20131204.pushlog.sql new file mode 100644 index 0000000000..e98d163577 --- /dev/null +++ b/resources/sql/patches/20131204.pushlog.sql @@ -0,0 +1,25 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_pushlog ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + epoch INT UNSIGNED NOT NULL, + repositoryPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + pusherPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + remoteAddress INT UNSIGNED, + remoteProtocol VARCHAR(32), + transactionKey CHAR(12) NOT NULL COLLATE latin1_bin, + refType VARCHAR(12) NOT NULL COLLATE utf8_bin, + refNameHash VARCHAR(12) COLLATE latin1_bin, + refNameRaw LONGTEXT COLLATE latin1_bin, + refNameEncoding VARCHAR(16) COLLATE utf8_bin, + refOld VARCHAR(40) COLLATE latin1_bin, + refNew VARCHAR(40) NOT NULL COLLATE latin1_bin, + mergeBase VARCHAR(40) COLLATE latin1_bin, + changeFlags INT UNSIGNED NOT NULL, + rejectCode INT UNSIGNED NOT NULL, + rejectDetails VARCHAR(64) COLLATE utf8_bin, + + KEY `key_repository` (repositoryPHID), + KEY `key_ref` (repositoryPHID, refNew), + KEY `key_pusher` (pusherPHID), + KEY `key_name` (repositoryPHID, refNameHash) + +) ENGINE=InnoDB, COLLATE=utf8_general_ci; diff --git a/resources/sql/patches/20131205.buildsteporder.sql b/resources/sql/patches/20131205.buildsteporder.sql new file mode 100644 index 0000000000..43d3516df1 --- /dev/null +++ b/resources/sql/patches/20131205.buildsteporder.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildstep +ADD COLUMN sequence INT UNSIGNED NOT NULL; diff --git a/resources/sql/patches/20131205.buildstepordermig.php b/resources/sql/patches/20131205.buildstepordermig.php new file mode 100644 index 0000000000..9721e15271 --- /dev/null +++ b/resources/sql/patches/20131205.buildstepordermig.php @@ -0,0 +1,41 @@ +establishConnection('w'); +$viewer = PhabricatorUser::getOmnipotentUser(); + +// Since HarbormasterBuildStepQuery has been updated to handle the +// correct order, we can't use the built in database access. + +foreach (new LiskMigrationIterator($table) as $plan) { + $planname = $plan->getName(); + echo "Migrating steps in {$planname}...\n"; + + $rows = queryfx_all( + $conn_w, + "SELECT id, sequence FROM harbormaster_buildstep ". + "WHERE buildPlanPHID = %s ". + "ORDER BY id ASC", + $plan->getPHID()); + + $sequence = 1; + foreach ($rows as $row) { + $id = $row['id']; + $existing = $row['sequence']; + if ($existing != 0) { + echo " - {$id} (already migrated)...\n"; + continue; + } + echo " - {$id} to position {$sequence}...\n"; + queryfx( + $conn_w, + "UPDATE harbormaster_buildstep ". + "SET sequence = %d ". + "WHERE id = %d", + $sequence, + $id); + $sequence++; + } +} + +echo "Done.\n"; diff --git a/resources/sql/patches/20131205.buildtargets.sql b/resources/sql/patches/20131205.buildtargets.sql new file mode 100644 index 0000000000..cf8e33af08 --- /dev/null +++ b/resources/sql/patches/20131205.buildtargets.sql @@ -0,0 +1,32 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildtarget ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + buildPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + buildStepPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + className VARCHAR(255) NOT NULL COLLATE utf8_bin, + details LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + variables LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_build` (buildPHID, buildStepPHID), + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +TRUNCATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog; +TRUNCATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk; +TRUNCATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildartifact; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog +DROP COLUMN buildPHID; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog +DROP COLUMN buildStepPHID; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildartifact +DROP COLUMN buildablePHID; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog +ADD COLUMN buildTargetPHID VARCHAR(64) NOT NULL COLLATE utf8_bin; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildartifact +ADD COLUMN buildTargetPHID VARCHAR(64) NOT NULL COLLATE utf8_bin; diff --git a/resources/sql/patches/20131206.phragment.sql b/resources/sql/patches/20131206.phragment.sql new file mode 100644 index 0000000000..21d3dfd148 --- /dev/null +++ b/resources/sql/patches/20131206.phragment.sql @@ -0,0 +1,24 @@ +CREATE TABLE {$NAMESPACE}_phragment.phragment_fragment ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + path VARCHAR(254) NOT NULL COLLATE utf8_bin, + depth INT UNSIGNED NOT NULL, + latestVersionPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_path` (path) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_phragment.phragment_fragmentversion ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + sequence INT UNSIGNED NOT NULL, + fragmentPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + filePHID VARCHAR(64) NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_version` (fragmentPHID, sequence) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131206.phragmentnull.sql b/resources/sql/patches/20131206.phragmentnull.sql new file mode 100644 index 0000000000..01dbd9bfe3 --- /dev/null +++ b/resources/sql/patches/20131206.phragmentnull.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phragment.phragment_fragment +MODIFY latestVersionPHID VARCHAR(64) NULL; diff --git a/resources/sql/patches/20131208.phragmentsnapshot.sql b/resources/sql/patches/20131208.phragmentsnapshot.sql new file mode 100644 index 0000000000..ccf9f9b184 --- /dev/null +++ b/resources/sql/patches/20131208.phragmentsnapshot.sql @@ -0,0 +1,21 @@ +CREATE TABLE {$NAMESPACE}_phragment.phragment_snapshot ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + primaryFragmentPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(192) NOT NULL COLLATE utf8_bin, + description LONGTEXT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_name` (primaryFragmentPHID, name) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_phragment.phragment_snapshotchild ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + snapshotPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + fragmentPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + fragmentVersionPHID VARCHAR(64) NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_child` (snapshotPHID, fragmentPHID, fragmentVersionPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131211.phragmentedges.sql b/resources/sql/patches/20131211.phragmentedges.sql new file mode 100644 index 0000000000..51eead5c5d --- /dev/null +++ b/resources/sql/patches/20131211.phragmentedges.sql @@ -0,0 +1,15 @@ +CREATE TABLE {$NAMESPACE}_phragment.edge ( + src VARCHAR(64) NOT NULL COLLATE utf8_bin, + type VARCHAR(64) NOT NULL COLLATE utf8_bin, + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY (src, type, dateCreated, seq) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_phragment.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE utf8_bin +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/patches/20131217.pushlogphid.1.col.sql b/resources/sql/patches/20131217.pushlogphid.1.col.sql new file mode 100644 index 0000000000..d52daa2b27 --- /dev/null +++ b/resources/sql/patches/20131217.pushlogphid.1.col.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_pushlog + ADD phid VARCHAR(64) NOT NULL COLLATE utf8_bin AFTER id; diff --git a/resources/sql/patches/20131217.pushlogphid.2.mig.php b/resources/sql/patches/20131217.pushlogphid.2.mig.php new file mode 100644 index 0000000000..edda8d346d --- /dev/null +++ b/resources/sql/patches/20131217.pushlogphid.2.mig.php @@ -0,0 +1,20 @@ +establishConnection('w'); + +echo "Assigning PHIDs to push logs...\n"; + +$logs = new LiskMigrationIterator($table); +foreach ($logs as $log) { + $id = $log->getID(); + echo "Updating {$id}...\n"; + queryfx( + $conn_w, + 'UPDATE %T SET phid = %s WHERE id = %d', + $table->getTableName(), + $log->generatePHID(), + $id); +} + +echo "Done.\n"; diff --git a/resources/sql/patches/20131217.pushlogphid.3.key.sql b/resources/sql/patches/20131217.pushlogphid.3.key.sql new file mode 100644 index 0000000000..6e618cb5c6 --- /dev/null +++ b/resources/sql/patches/20131217.pushlogphid.3.key.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_pushlog + ADD UNIQUE KEY `key_phid` (phid); diff --git a/resources/sql/patches/20131219.pxdrop.sql b/resources/sql/patches/20131219.pxdrop.sql new file mode 100644 index 0000000000..c4c2e3d035 --- /dev/null +++ b/resources/sql/patches/20131219.pxdrop.sql @@ -0,0 +1 @@ +DROP TABLE {$NAMESPACE}_project.project_legacytransaction; diff --git a/resources/sql/patches/20131224.harbormanual.sql b/resources/sql/patches/20131224.harbormanual.sql new file mode 100644 index 0000000000..fb5a5ee970 --- /dev/null +++ b/resources/sql/patches/20131224.harbormanual.sql @@ -0,0 +1,6 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildable + ADD isManualBuildable BOOL NOT NULL; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildable + ADD KEY `key_manual` (isManualBuildable); + diff --git a/resources/sql/patches/20131227.heraldobject.sql b/resources/sql/patches/20131227.heraldobject.sql new file mode 100644 index 0000000000..b53d95ab86 --- /dev/null +++ b/resources/sql/patches/20131227.heraldobject.sql @@ -0,0 +1,6 @@ +ALTER TABLE {$NAMESPACE}_herald.herald_rule + ADD triggerObjectPHID VARCHAR(64) COLLATE utf8_bin; + +ALTER TABLE {$NAMESPACE}_herald.herald_rule + ADD KEY `key_trigger` (triggerObjectPHID); + diff --git a/resources/sql/patches/20131231.dropshortcut.sql b/resources/sql/patches/20131231.dropshortcut.sql new file mode 100644 index 0000000000..fe507ba9ab --- /dev/null +++ b/resources/sql/patches/20131231.dropshortcut.sql @@ -0,0 +1 @@ +DROP TABLE {$NAMESPACE}_repository.repository_shortcut; diff --git a/resources/sql/quickstart.sql b/resources/sql/quickstart.sql index 3439af6d4e..635442c7fa 100644 --- a/resources/sql/quickstart.sql +++ b/resources/sql/quickstart.sql @@ -35,36 +35,6 @@ CREATE TABLE `audit_inlinecomment` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE DATABASE `{$NAMESPACE}_cache` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_cache`; - -CREATE TABLE `cache_general` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `cacheKeyHash` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `cacheKey` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `cacheFormat` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `cacheData` longblob NOT NULL, - `cacheCreated` int(10) unsigned NOT NULL, - `cacheExpires` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `key_cacheKeyHash` (`cacheKeyHash`), - KEY `key_cacheCreated` (`cacheCreated`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `cache_markupcache` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `cacheKey` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `cacheData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `cacheKey` (`cacheKey`), - KEY `dateCreated` (`dateCreated`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - CREATE DATABASE `{$NAMESPACE}_calendar` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_calendar`; @@ -80,16 +50,30 @@ CREATE TABLE `calendar_holiday` ( CREATE DATABASE `{$NAMESPACE}_chatlog` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_chatlog`; +CREATE TABLE `chatlog_channel` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `serviceName` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `serviceType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `channelName` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_channel` (`channelName`,`serviceType`,`serviceName`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `chatlog_event` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `channel` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `epoch` int(10) unsigned NOT NULL, `author` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `type` varchar(4) NOT NULL, `message` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `loggedByPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `channelID` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), - KEY `channel` (`channel`,`epoch`) + KEY `channel` (`epoch`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -116,7 +100,8 @@ CREATE TABLE `conduit_connectionlog` ( `username` varchar(255) DEFAULT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `key_created` (`dateCreated`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -128,60 +113,28 @@ CREATE TABLE `conduit_methodcalllog` ( `duration` bigint(20) unsigned NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE DATABASE `{$NAMESPACE}_config` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_config`; - -CREATE TABLE `config_entry` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `namespace` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `configKey` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `value` longtext NOT NULL, - `isDeleted` tinyint(1) NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, + `callerPHID` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `key_phid` (`phid`), - UNIQUE KEY `key_name` (`namespace`,`configKey`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `config_transaction` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, - `commentVersion` int(10) unsigned NOT NULL, - `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `key_phid` (`phid`), - KEY `key_object` (`objectPHID`) + KEY `key_created` (`dateCreated`), + KEY `key_method` (`method`), + KEY `key_callermethod` (`callerPHID`,`method`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE DATABASE `{$NAMESPACE}_countdown` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_countdown`; -CREATE TABLE `countdown_timer` ( +CREATE TABLE `countdown` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `title` varchar(255) NOT NULL, `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `datepoint` int(10) unsigned NOT NULL, + `epoch` int(10) unsigned NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`) + `viewPolicy` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -193,7 +146,7 @@ CREATE TABLE `daemon_log` ( `daemon` varchar(255) NOT NULL, `host` varchar(255) NOT NULL, `pid` int(10) unsigned NOT NULL, - `argv` varchar(512) NOT NULL, + `argv` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, `status` varchar(8) NOT NULL, @@ -292,8 +245,41 @@ CREATE TABLE `differential_commit` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `differential_customfieldnumericindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`), + KEY `key_find` (`indexKey`,`indexValue`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `differential_customfieldstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldIndex` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldValue` longtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `objectPHID` (`objectPHID`,`fieldIndex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `differential_customfieldstringindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`(64)), + KEY `key_find` (`indexKey`,`indexValue`(64)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `differential_diff` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `revisionID` int(10) unsigned DEFAULT NULL, `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `sourceMachine` varchar(255) DEFAULT NULL, @@ -314,6 +300,7 @@ CREATE TABLE `differential_diff` ( `description` varchar(255) DEFAULT NULL, `repositoryUUID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), KEY `revisionID` (`revisionID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -391,13 +378,16 @@ CREATE TABLE `differential_revision` ( `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, `attached` longtext NOT NULL, - `unsubscribed` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `mailKey` varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `branchName` varchar(255) DEFAULT NULL, `arcanistProjectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `phid` (`phid`), - KEY `authorPHID` (`authorPHID`,`status`) + KEY `authorPHID` (`authorPHID`,`status`), + KEY `repositoryPHID` (`repositoryPHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -410,6 +400,60 @@ CREATE TABLE `differential_revisionhash` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `differential_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `differential_transaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `revisionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `changesetID` int(10) unsigned DEFAULT NULL, + `isNewFile` tinyint(1) NOT NULL, + `lineNumber` int(10) unsigned NOT NULL, + `lineLength` int(10) unsigned NOT NULL, + `fixedState` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `hasReplies` tinyint(1) NOT NULL, + `replyToCommentPHID` varchar(64) DEFAULT NULL, + `legacyCommentID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`), + KEY `key_changeset` (`changesetID`), + KEY `key_draft` (`authorPHID`,`transactionPHID`), + KEY `key_revision` (`revisionPHID`), + KEY `key_legacy` (`legacyCommentID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `edge` ( `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `type` int(10) unsigned NOT NULL, @@ -418,6 +462,7 @@ CREATE TABLE `edge` ( `seq` int(10) unsigned NOT NULL, `dataID` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`src`,`type`,`dst`), + UNIQUE KEY `key_dst` (`dst`,`type`,`src`), KEY `src` (`src`,`type`,`dateCreated`,`seq`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -448,6 +493,43 @@ CREATE TABLE `draft` ( CREATE DATABASE `{$NAMESPACE}_drydock` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_drydock`; +CREATE TABLE `drydock_blueprint` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `className` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `blueprintName` varchar(255) NOT NULL, + `viewPolicy` varchar(64) NOT NULL, + `editPolicy` varchar(64) NOT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `drydock_blueprinttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `drydock_lease` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, @@ -484,55 +566,17 @@ CREATE TABLE `drydock_resource` ( `name` varchar(255) NOT NULL, `ownerPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `status` int(10) unsigned NOT NULL, - `blueprintClass` varchar(255) NOT NULL, `type` varchar(64) NOT NULL, `attributes` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `capabilities` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, + `blueprintPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `phid` (`phid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE DATABASE `{$NAMESPACE}_fact` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_fact`; - -CREATE TABLE `fact_aggregate` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `factType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `valueX` bigint(20) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `factType` (`factType`,`objectPHID`), - KEY `factType_2` (`factType`,`valueX`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `fact_cursor` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `position` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `fact_raw` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `factType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `objectA` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `valueX` bigint(20) NOT NULL, - `valueY` bigint(20) NOT NULL, - `epoch` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `objectPHID` (`objectPHID`), - KEY `factType` (`factType`,`epoch`), - KEY `factType_2` (`factType`,`objectA`,`epoch`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - CREATE DATABASE `{$NAMESPACE}_feed` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_feed`; @@ -606,25 +650,36 @@ CREATE TABLE `file` ( `secretKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `contentHash` varchar(40) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `ttl` int(10) unsigned DEFAULT NULL, + `isExplicitUpload` tinyint(1) DEFAULT '1', + `mailKey` varchar(20) NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `phid` (`phid`), KEY `authorPHID` (`authorPHID`), - KEY `contentHash` (`contentHash`) + KEY `contentHash` (`contentHash`), + KEY `key_ttl` (`ttl`), + KEY `key_dateCreated` (`dateCreated`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `file_imagemacro` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `filePHID` varchar(64) NOT NULL, `name` varchar(255) NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, `isDisabled` tinyint(1) NOT NULL, + `audioPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `audioBehavior` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`), UNIQUE KEY `key_phid` (`phid`), - KEY `key_disabled` (`isDisabled`) + KEY `key_disabled` (`isDisabled`), + KEY `key_dateCreated` (`dateCreated`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -637,6 +692,48 @@ CREATE TABLE `file_storageblob` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `file_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `file_transaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`), + UNIQUE KEY `key_draft` (`authorPHID`,`transactionPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `file_transformedfile` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `originalPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, @@ -665,6 +762,7 @@ CREATE TABLE `macro_transaction` ( `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key_phid` (`phid`), KEY `key_object` (`objectPHID`) @@ -731,6 +829,161 @@ CREATE TABLE `edgedata` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `harbormaster_build` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildablePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildPlanPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildStatus` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_buildable` (`buildablePHID`), + KEY `key_plan` (`buildPlanPHID`), + KEY `key_status` (`buildStatus`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildable` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildablePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `containerPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `buildStatus` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildableStatus` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `isManualBuildable` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_buildable` (`buildablePHID`), + KEY `key_container` (`containerPHID`), + KEY `key_manual` (`isManualBuildable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildartifact` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `artifactType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `artifactIndex` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `artifactKey` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `artifactData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `buildTargetPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_artifact` (`artifactType`,`artifactIndex`), + UNIQUE KEY `key_artifact_type` (`artifactType`,`artifactIndex`), + KEY `key_garbagecollect` (`artifactType`,`dateCreated`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildcommand` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `targetPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `command` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `key_target` (`targetPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildlog` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `logSource` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `logType` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `duration` int(10) unsigned DEFAULT NULL, + `live` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `buildTargetPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildlogchunk` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `logID` int(10) unsigned NOT NULL, + `encoding` varchar(30) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `size` mediumtext, + `chunk` longblob NOT NULL, + PRIMARY KEY (`id`), + KEY `key_log` (`logID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildplan` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) NOT NULL, + `planStatus` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_status` (`planStatus`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildplantransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildstep` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildPlanPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `className` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `sequence` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_plan` (`buildPlanPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `harbormaster_buildtarget` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `buildStepPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `className` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `variables` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `targetStatus` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_build` (`buildPHID`,`buildStepPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `harbormaster_object` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, @@ -793,9 +1046,14 @@ CREATE TABLE `herald_rule` ( `dateModified` int(10) unsigned NOT NULL, `repetitionPolicy` int(10) unsigned DEFAULT NULL, `ruleType` varchar(255) NOT NULL DEFAULT 'global', + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDisabled` int(10) unsigned NOT NULL DEFAULT '0', + `triggerObjectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `authorPHID` (`authorPHID`,`name`), - KEY `IDX_RULE_TYPE` (`ruleType`) + UNIQUE KEY `phid` (`phid`), + KEY `IDX_RULE_TYPE` (`ruleType`), + KEY `key_trigger` (`triggerObjectPHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -820,6 +1078,47 @@ CREATE TABLE `herald_ruleedit` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `herald_ruletransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `herald_ruletransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `herald_savedheader` ( `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `header` varchar(255) NOT NULL, @@ -832,7 +1131,6 @@ CREATE TABLE `herald_transcript` ( `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `time` int(10) unsigned NOT NULL, `host` varchar(255) NOT NULL, - `psth` varchar(255) NOT NULL, `duration` float NOT NULL, `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `dryRun` tinyint(1) NOT NULL, @@ -870,17 +1168,45 @@ CREATE TABLE `edgedata` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE `maniphest_savedquery` ( +CREATE TABLE `maniphest_customfieldnumericindex` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `queryKey` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `name` varchar(128) NOT NULL, - `isDefault` tinyint(1) NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` bigint(20) NOT NULL, PRIMARY KEY (`id`), - KEY `userPHID` (`userPHID`,`name`), - KEY `userPHID_2` (`userPHID`,`isDefault`,`name`) + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`), + KEY `key_find` (`indexKey`,`indexValue`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `maniphest_customfieldstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldIndex` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldValue` longtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `objectPHID` (`objectPHID`,`fieldIndex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `maniphest_customfieldstringindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`(64)), + KEY `key_find` (`indexKey`,`indexValue`(64)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `maniphest_nameindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `indexedObjectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexedObjectName` varchar(128) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`indexedObjectPHID`), + KEY `key_name` (`indexedObjectName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -903,6 +1229,8 @@ CREATE TABLE `maniphest_task` ( `ownerOrdering` varchar(64) DEFAULT NULL, `originalEmailSource` varchar(255) DEFAULT NULL, `subpriority` double NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `phid` (`phid`), KEY `priority` (`priority`,`status`), @@ -910,7 +1238,8 @@ CREATE TABLE `maniphest_task` ( KEY `ownerPHID` (`ownerPHID`,`status`), KEY `authorPHID` (`authorPHID`,`status`), KEY `ownerOrdering` (`ownerOrdering`), - KEY `priority_2` (`priority`,`subpriority`) + KEY `priority_2` (`priority`,`subpriority`), + KEY `key_dateCreated` (`dateCreated`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -918,7 +1247,7 @@ CREATE TABLE `maniphest_taskauxiliarystorage` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `taskPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `name` varchar(255) NOT NULL, - `value` varchar(255) NOT NULL, + `value` longtext CHARACTER SET utf8 COLLATE utf8_bin, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), @@ -942,15 +1271,48 @@ CREATE TABLE `maniphest_tasksubscriber` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE `maniphest_touch` ( - `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `taskID` int(10) unsigned NOT NULL, - `touchedAt` int(10) unsigned NOT NULL, - PRIMARY KEY (`userPHID`,`taskID`) +CREATE TABLE `maniphest_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE `maniphest_transaction` ( +CREATE TABLE `maniphest_transaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `maniphest_transaction_legacy` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `taskID` int(10) unsigned NOT NULL, `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, @@ -976,239 +1338,8 @@ CREATE TABLE `patch_status` ( PRIMARY KEY (`patch`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `patch_status` (`patch`, `applied`) VALUES -('phabricator:000.project.sql', 1), -('phabricator:0000.legacy.sql', 1), -('phabricator:001.maniphest_projects.sql', 1), -('phabricator:002.oauth.sql', 1), -('phabricator:003.more_oauth.sql', 1), -('phabricator:004.daemonrepos.sql', 1), -('phabricator:005.workers.sql', 1), -('phabricator:006.repository.sql', 1), -('phabricator:007.daemonlog.sql', 1), -('phabricator:008.repoopt.sql', 1), -('phabricator:009.repo_summary.sql', 1), -('phabricator:010.herald.sql', 1), -('phabricator:011.badcommit.sql', 1), -('phabricator:012.dropphidtype.sql', 1), -('phabricator:013.commitdetail.sql', 1), -('phabricator:014.shortcuts.sql', 1), -('phabricator:015.preferences.sql', 1), -('phabricator:016.userrealnameindex.sql', 1), -('phabricator:017.sessionkeys.sql', 1), -('phabricator:018.owners.sql', 1), -('phabricator:019.arcprojects.sql', 1), -('phabricator:020.pathcapital.sql', 1), -('phabricator:021.xhpastview.sql', 1), -('phabricator:022.differentialcommit.sql', 1), -('phabricator:023.dxkeys.sql', 1), -('phabricator:024.mlistkeys.sql', 1), -('phabricator:025.commentopt.sql', 1), -('phabricator:026.diffpropkey.sql', 1), -('phabricator:027.metamtakeys.sql', 1), -('phabricator:028.systemagent.sql', 1), -('phabricator:029.cursors.sql', 1), -('phabricator:030.imagemacro.sql', 1), -('phabricator:031.workerrace.sql', 1), -('phabricator:032.viewtime.sql', 1), -('phabricator:033.privtest.sql', 1), -('phabricator:034.savedheader.sql', 1), -('phabricator:035.proxyimage.sql', 1), -('phabricator:036.mailkey.sql', 1), -('phabricator:037.setuptest.sql', 1), -('phabricator:038.admin.sql', 1), -('phabricator:039.userlog.sql', 1), -('phabricator:040.transform.sql', 1), -('phabricator:041.heraldrepetition.sql', 1), -('phabricator:042.commentmetadata.sql', 1), -('phabricator:043.pastebin.sql', 1), -('phabricator:044.countdown.sql', 1), -('phabricator:045.timezone.sql', 1), -('phabricator:046.conduittoken.sql', 1), -('phabricator:047.projectstatus.sql', 1), -('phabricator:048.relationshipkeys.sql', 1), -('phabricator:049.projectowner.sql', 1), -('phabricator:050.taskdenormal.sql', 1), -('phabricator:051.projectfilter.sql', 1), -('phabricator:052.pastelanguage.sql', 1), -('phabricator:053.feed.sql', 1), -('phabricator:054.subscribers.sql', 1), -('phabricator:055.add_author_to_files.sql', 1), -('phabricator:056.slowvote.sql', 1), -('phabricator:057.parsecache.sql', 1), -('phabricator:058.missingkeys.sql', 1), -('phabricator:059.engines.php', 1), -('phabricator:060.phriction.sql', 1), -('phabricator:061.phrictioncontent.sql', 1), -('phabricator:062.phrictionmenu.sql', 1), -('phabricator:063.pasteforks.sql', 1), -('phabricator:064.subprojects.sql', 1), -('phabricator:065.sshkeys.sql', 1), -('phabricator:066.phrictioncontent.sql', 1), -('phabricator:067.preferences.sql', 1), -('phabricator:068.maniphestauxiliarystorage.sql', 1), -('phabricator:069.heraldxscript.sql', 1), -('phabricator:070.differentialaux.sql', 1), -('phabricator:071.contentsource.sql', 1), -('phabricator:072.blamerevert.sql', 1), -('phabricator:073.reposymbols.sql', 1), -('phabricator:074.affectedpath.sql', 1), -('phabricator:075.revisionhash.sql', 1), -('phabricator:076.indexedlanguages.sql', 1), -('phabricator:077.originalemail.sql', 1), -('phabricator:078.nametoken.sql', 1), -('phabricator:079.nametokenindex.php', 1), -('phabricator:080.filekeys.sql', 1), -('phabricator:081.filekeys.php', 1), -('phabricator:082.xactionkey.sql', 1), -('phabricator:083.dxviewtime.sql', 1), -('phabricator:084.pasteauthorkey.sql', 1), -('phabricator:085.packagecommitrelationship.sql', 1), -('phabricator:086.formeraffil.sql', 1), -('phabricator:087.phrictiondelete.sql', 1), -('phabricator:088.audit.sql', 1), -('phabricator:089.projectwiki.sql', 1), -('phabricator:090.forceuniqueprojectnames.php', 1), -('phabricator:091.uniqueslugkey.sql', 1), -('phabricator:092.dropgithubnotification.sql', 1), -('phabricator:093.gitremotes.php', 1), -('phabricator:094.phrictioncolumn.sql', 1), -('phabricator:095.directory.sql', 1), -('phabricator:096.filename.sql', 1), -('phabricator:097.heraldruletypes.sql', 1), -('phabricator:098.heraldruletypemigration.php', 1), -('phabricator:099.drydock.sql', 1), -('phabricator:100.projectxaction.sql', 1), -('phabricator:101.heraldruleapplied.sql', 1), -('phabricator:102.heraldcleanup.php', 1), -('phabricator:103.heraldedithistory.sql', 1), -('phabricator:104.searchkey.sql', 1), -('phabricator:105.mimetype.sql', 1), -('phabricator:106.chatlog.sql', 1), -('phabricator:107.oauthserver.sql', 1), -('phabricator:108.oauthscope.sql', 1), -('phabricator:109.oauthclientphidkey.sql', 1), -('phabricator:110.commitaudit.sql', 1), -('phabricator:111.commitauditmigration.php', 1), -('phabricator:112.oauthaccesscoderedirecturi.sql', 1), -('phabricator:113.lastreviewer.sql', 1), -('phabricator:114.auditrequest.sql', 1), -('phabricator:115.prepareutf8.sql', 1), -('phabricator:116.utf8-backup-first-expect-wait.sql', 1), -('phabricator:117.repositorydescription.php', 1), -('phabricator:118.auditinline.sql', 1), -('phabricator:119.filehash.sql', 1), -('phabricator:120.noop.sql', 1), -('phabricator:121.drydocklog.sql', 1), -('phabricator:122.flag.sql', 1), -('phabricator:123.heraldrulelog.sql', 1), -('phabricator:124.subpriority.sql', 1), -('phabricator:125.ipv6.sql', 1), -('phabricator:126.edges.sql', 1), -('phabricator:127.userkeybody.sql', 1), -('phabricator:128.phabricatorcom.sql', 1), -('phabricator:129.savedquery.sql', 1), -('phabricator:130.denormalrevisionquery.sql', 1), -('phabricator:131.migraterevisionquery.php', 1), -('phabricator:132.phame.sql', 1), -('phabricator:133.imagemacro.sql', 1), -('phabricator:134.emptysearch.sql', 1), -('phabricator:135.datecommitted.sql', 1), -('phabricator:136.sex.sql', 1), -('phabricator:137.auditmetadata.sql', 1), -('phabricator:138.notification.sql', 1358377808), -('phabricator:20121209.pholioxactions.sql', 1358377847), -('phabricator:20121209.xmacroadd.sql', 1358377849), -('phabricator:20121209.xmacromigrate.php', 1358377849), -('phabricator:20121209.xmacromigratekey.sql', 1358377850), -('phabricator:20121220.generalcache.sql', 1358377851), -('phabricator:20121226.config.sql', 1358377852), -('phabricator:20130101.confxaction.sql', 1358377853), -('phabricator:20130102.metamtareceivedmailmessageidhash.sql', 1358377854), -('phabricator:20130103.filemetadata.sql', 1358377855), -('phabricator:daemonstatus.sql', 1358377823), -('phabricator:daemonstatuskey.sql', 1358377828), -('phabricator:daemontaskarchive.sql', 1358377838), -('phabricator:db.audit', 1), -('phabricator:db.cache', 1358377804), -('phabricator:db.calendar', 1358377802), -('phabricator:db.chatlog', 1), -('phabricator:db.conduit', 1), -('phabricator:db.config', 1358377852), -('phabricator:db.countdown', 1), -('phabricator:db.daemon', 1), -('phabricator:db.differential', 1), -('phabricator:db.draft', 1), -('phabricator:db.drydock', 1), -('phabricator:db.fact', 1358377804), -('phabricator:db.feed', 1), -('phabricator:db.file', 1), -('phabricator:db.flag', 1), -('phabricator:db.harbormaster', 1358377803), -('phabricator:db.herald', 1), -('phabricator:db.maniphest', 1), -('phabricator:db.metamta', 1), -('phabricator:db.meta_data', 1), -('phabricator:db.oauth_server', 1), -('phabricator:db.owners', 1), -('phabricator:db.pastebin', 1), -('phabricator:db.phame', 1), -('phabricator:db.phid', 1), -('phabricator:db.pholio', 1358377807), -('phabricator:db.phriction', 1), -('phabricator:db.ponder', 1358377805), -('phabricator:db.project', 1), -('phabricator:db.repository', 1), -('phabricator:db.search', 1), -('phabricator:db.slowvote', 1), -('phabricator:db.timeline', 1), -('phabricator:db.user', 1), -('phabricator:db.worker', 1), -('phabricator:db.xhpastview', 1), -('phabricator:db.xhprof', 1358377806), -('phabricator:differentialbookmarks.sql', 1358377817), -('phabricator:draft-metadata.sql', 1358377832), -('phabricator:dropfileproxyimage.sql', 1358377843), -('phabricator:drydockresoucetype.sql', 1358377840), -('phabricator:drydocktaskid.sql', 1358377839), -('phabricator:edgetype.sql', 1358377829), -('phabricator:emailtable.sql', 1358377810), -('phabricator:emailtableport.sql', 1358377811), -('phabricator:emailtableremove.sql', 1358377811), -('phabricator:fact-raw.sql', 1358377825), -('phabricator:harbormasterobject.sql', 1358377817), -('phabricator:holidays.sql', 1358377808), -('phabricator:ldapinfo.sql', 1358377814), -('phabricator:liskcounters-task.sql', 1358377844), -('phabricator:liskcounters.php', 1358377842), -('phabricator:liskcounters.sql', 1358377841), -('phabricator:maniphestxcache.sql', 1358377819), -('phabricator:markupcache.sql', 1358377818), -('phabricator:migrate-differential-dependencies.php', 1358377820), -('phabricator:migrate-maniphest-dependencies.php', 1358377820), -('phabricator:migrate-maniphest-revisions.php', 1358377822), -('phabricator:migrate-project-edges.php', 1358377824), -('phabricator:owners-exclude.sql', 1358377846), -('phabricator:pastepolicy.sql', 1358377831), -('phabricator:phameblog.sql', 1358377821), -('phabricator:phamedomain.sql', 1358377833), -('phabricator:phameoneblog.sql', 1358377837), -('phabricator:phamepolicy.sql', 1358377836), -('phabricator:phiddrop.sql', 1358377812), -('phabricator:pholio.sql', 1358377846), -('phabricator:policy-project.sql', 1358377827), -('phabricator:ponder-comments.sql', 1358377830), -('phabricator:ponder-mailkey-populate.php', 1358377835), -('phabricator:ponder-mailkey.sql', 1358377834), -('phabricator:ponder.sql', 1358377826), -('phabricator:repository-lint.sql', 1358377843), -('phabricator:statustxt.sql', 1358377837), -('phabricator:symbolcontexts.sql', 1358377823), -('phabricator:testdatabase.sql', 1358377813), -('phabricator:threadtopic.sql', 1358377815), -('phabricator:userstatus.sql', 1358377809), -('phabricator:usertranslation.sql', 1358377816), -('phabricator:xhprof.sql', 1358377832); + +INSERT INTO `patch_status` VALUES ('phabricator:000.project.sql',1),('phabricator:0000.legacy.sql',1),('phabricator:001.maniphest_projects.sql',1),('phabricator:002.oauth.sql',1),('phabricator:003.more_oauth.sql',1),('phabricator:004.daemonrepos.sql',1),('phabricator:005.workers.sql',1),('phabricator:006.repository.sql',1),('phabricator:007.daemonlog.sql',1),('phabricator:008.repoopt.sql',1),('phabricator:009.repo_summary.sql',1),('phabricator:010.herald.sql',1),('phabricator:011.badcommit.sql',1),('phabricator:012.dropphidtype.sql',1),('phabricator:013.commitdetail.sql',1),('phabricator:014.shortcuts.sql',1),('phabricator:015.preferences.sql',1),('phabricator:016.userrealnameindex.sql',1),('phabricator:017.sessionkeys.sql',1),('phabricator:018.owners.sql',1),('phabricator:019.arcprojects.sql',1),('phabricator:020.pathcapital.sql',1),('phabricator:021.xhpastview.sql',1),('phabricator:022.differentialcommit.sql',1),('phabricator:023.dxkeys.sql',1),('phabricator:024.mlistkeys.sql',1),('phabricator:025.commentopt.sql',1),('phabricator:026.diffpropkey.sql',1),('phabricator:027.metamtakeys.sql',1),('phabricator:028.systemagent.sql',1),('phabricator:029.cursors.sql',1),('phabricator:030.imagemacro.sql',1),('phabricator:031.workerrace.sql',1),('phabricator:032.viewtime.sql',1),('phabricator:033.privtest.sql',1),('phabricator:034.savedheader.sql',1),('phabricator:035.proxyimage.sql',1),('phabricator:036.mailkey.sql',1),('phabricator:037.setuptest.sql',1),('phabricator:038.admin.sql',1),('phabricator:039.userlog.sql',1),('phabricator:040.transform.sql',1),('phabricator:041.heraldrepetition.sql',1),('phabricator:042.commentmetadata.sql',1),('phabricator:043.pastebin.sql',1),('phabricator:044.countdown.sql',1),('phabricator:045.timezone.sql',1),('phabricator:046.conduittoken.sql',1),('phabricator:047.projectstatus.sql',1),('phabricator:048.relationshipkeys.sql',1),('phabricator:049.projectowner.sql',1),('phabricator:050.taskdenormal.sql',1),('phabricator:051.projectfilter.sql',1),('phabricator:052.pastelanguage.sql',1),('phabricator:053.feed.sql',1),('phabricator:054.subscribers.sql',1),('phabricator:055.add_author_to_files.sql',1),('phabricator:056.slowvote.sql',1),('phabricator:057.parsecache.sql',1),('phabricator:058.missingkeys.sql',1),('phabricator:059.engines.php',1),('phabricator:060.phriction.sql',1),('phabricator:061.phrictioncontent.sql',1),('phabricator:062.phrictionmenu.sql',1),('phabricator:063.pasteforks.sql',1),('phabricator:064.subprojects.sql',1),('phabricator:065.sshkeys.sql',1),('phabricator:066.phrictioncontent.sql',1),('phabricator:067.preferences.sql',1),('phabricator:068.maniphestauxiliarystorage.sql',1),('phabricator:069.heraldxscript.sql',1),('phabricator:070.differentialaux.sql',1),('phabricator:071.contentsource.sql',1),('phabricator:072.blamerevert.sql',1),('phabricator:073.reposymbols.sql',1),('phabricator:074.affectedpath.sql',1),('phabricator:075.revisionhash.sql',1),('phabricator:076.indexedlanguages.sql',1),('phabricator:077.originalemail.sql',1),('phabricator:078.nametoken.sql',1),('phabricator:079.nametokenindex.php',1),('phabricator:080.filekeys.sql',1),('phabricator:081.filekeys.php',1),('phabricator:082.xactionkey.sql',1),('phabricator:083.dxviewtime.sql',1),('phabricator:084.pasteauthorkey.sql',1),('phabricator:085.packagecommitrelationship.sql',1),('phabricator:086.formeraffil.sql',1),('phabricator:087.phrictiondelete.sql',1),('phabricator:088.audit.sql',1),('phabricator:089.projectwiki.sql',1),('phabricator:090.forceuniqueprojectnames.php',1),('phabricator:091.uniqueslugkey.sql',1),('phabricator:092.dropgithubnotification.sql',1),('phabricator:093.gitremotes.php',1),('phabricator:094.phrictioncolumn.sql',1),('phabricator:095.directory.sql',1),('phabricator:096.filename.sql',1),('phabricator:097.heraldruletypes.sql',1),('phabricator:098.heraldruletypemigration.php',1),('phabricator:099.drydock.sql',1),('phabricator:100.projectxaction.sql',1),('phabricator:101.heraldruleapplied.sql',1),('phabricator:102.heraldcleanup.php',1),('phabricator:103.heraldedithistory.sql',1),('phabricator:104.searchkey.sql',1),('phabricator:105.mimetype.sql',1),('phabricator:106.chatlog.sql',1),('phabricator:107.oauthserver.sql',1),('phabricator:108.oauthscope.sql',1),('phabricator:109.oauthclientphidkey.sql',1),('phabricator:110.commitaudit.sql',1),('phabricator:111.commitauditmigration.php',1),('phabricator:112.oauthaccesscoderedirecturi.sql',1),('phabricator:113.lastreviewer.sql',1),('phabricator:114.auditrequest.sql',1),('phabricator:115.prepareutf8.sql',1),('phabricator:116.utf8-backup-first-expect-wait.sql',1),('phabricator:117.repositorydescription.php',1),('phabricator:118.auditinline.sql',1),('phabricator:119.filehash.sql',1),('phabricator:120.noop.sql',1),('phabricator:121.drydocklog.sql',1),('phabricator:122.flag.sql',1),('phabricator:123.heraldrulelog.sql',1),('phabricator:124.subpriority.sql',1),('phabricator:125.ipv6.sql',1),('phabricator:126.edges.sql',1),('phabricator:127.userkeybody.sql',1),('phabricator:128.phabricatorcom.sql',1),('phabricator:129.savedquery.sql',1),('phabricator:130.denormalrevisionquery.sql',1),('phabricator:131.migraterevisionquery.php',1),('phabricator:132.phame.sql',1),('phabricator:133.imagemacro.sql',1),('phabricator:134.emptysearch.sql',1),('phabricator:135.datecommitted.sql',1),('phabricator:136.sex.sql',1),('phabricator:137.auditmetadata.sql',1),('phabricator:138.notification.sql',1358377808),('phabricator:20121209.pholioxactions.sql',1358377847),('phabricator:20121209.xmacroadd.sql',1358377849),('phabricator:20121209.xmacromigrate.php',1358377849),('phabricator:20121209.xmacromigratekey.sql',1358377850),('phabricator:20121220.generalcache.sql',1358377851),('phabricator:20121226.config.sql',1358377852),('phabricator:20130101.confxaction.sql',1358377853),('phabricator:20130102.metamtareceivedmailmessageidhash.sql',1358377854),('phabricator:20130103.filemetadata.sql',1358377855),('phabricator:20130111.conpherence.sql',1390001562),('phabricator:20130127.altheraldtranscript.sql',1390001562),('phabricator:20130131.conpherencepics.sql',1390001562),('phabricator:20130201.revisionunsubscribed.php',1390001562),('phabricator:20130201.revisionunsubscribed.sql',1390001562),('phabricator:20130214.chatlogchannel.sql',1390001562),('phabricator:20130214.chatlogchannelid.sql',1390001562),('phabricator:20130214.token.sql',1390001562),('phabricator:20130215.phabricatorfileaddttl.sql',1390001562),('phabricator:20130217.cachettl.sql',1390001562),('phabricator:20130218.longdaemon.sql',1390001562),('phabricator:20130218.updatechannelid.php',1390001562),('phabricator:20130219.commitsummary.sql',1390001562),('phabricator:20130219.commitsummarymig.php',1390001562),('phabricator:20130222.dropchannel.sql',1390001562),('phabricator:20130226.commitkey.sql',1390001562),('phabricator:20130304.lintauthor.sql',1390001562),('phabricator:20130310.xactionmeta.sql',1390001562),('phabricator:20130317.phrictionedge.sql',1390001562),('phabricator:20130319.conpherence.sql',1390001562),('phabricator:20130319.phabricatorfileexplicitupload.sql',1390001562),('phabricator:20130320.phlux.sql',1390001562),('phabricator:20130321.token.sql',1390001562),('phabricator:20130322.phortune.sql',1390001562),('phabricator:20130323.phortunepayment.sql',1390001562),('phabricator:20130324.phortuneproduct.sql',1390001562),('phabricator:20130330.phrequent.sql',1390001562),('phabricator:20130403.conpherencecache.sql',1390001562),('phabricator:20130403.conpherencecachemig.php',1390001562),('phabricator:20130409.commitdrev.php',1390001562),('phabricator:20130417.externalaccount.sql',1390001562),('phabricator:20130423.conpherenceindices.sql',1390001562),('phabricator:20130423.phortunepaymentrevised.sql',1390001562),('phabricator:20130423.updateexternalaccount.sql',1390001562),('phabricator:20130426.search_savedquery.sql',1390001562),('phabricator:20130502.countdownrevamp1.sql',1390001562),('phabricator:20130502.countdownrevamp2.php',1390001562),('phabricator:20130502.countdownrevamp3.sql',1390001562),('phabricator:20130507.releephrqmailkey.sql',1390001562),('phabricator:20130507.releephrqmailkeypop.php',1390001562),('phabricator:20130507.releephrqsimplifycols.sql',1390001562),('phabricator:20130508.releephtransactions.sql',1390001562),('phabricator:20130508.releephtransactionsmig.php',1390001562),('phabricator:20130508.search_namedquery.sql',1390001562),('phabricator:20130513.receviedmailstatus.sql',1390001562),('phabricator:20130519.diviner.sql',1390001563),('phabricator:20130521.dropconphimages.sql',1390001563),('phabricator:20130523.maniphest_owners.sql',1390001563),('phabricator:20130524.repoxactions.sql',1390001563),('phabricator:20130529.macroauthor.sql',1390001563),('phabricator:20130529.macroauthormig.php',1390001563),('phabricator:20130530.macrodatekey.sql',1390001563),('phabricator:20130530.pastekeys.sql',1390001563),('phabricator:20130530.sessionhash.php',1390001563),('phabricator:20130531.filekeys.sql',1390001563),('phabricator:20130602.morediviner.sql',1390001563),('phabricator:20130602.namedqueries.sql',1390001563),('phabricator:20130606.userxactions.sql',1390001563),('phabricator:20130607.xaccount.sql',1390001563),('phabricator:20130611.migrateoauth.php',1390001563),('phabricator:20130611.nukeldap.php',1390001563),('phabricator:20130613.authdb.sql',1390001563),('phabricator:20130619.authconf.php',1390001563),('phabricator:20130620.diffxactions.sql',1390001563),('phabricator:20130621.diffcommentphid.sql',1390001563),('phabricator:20130621.diffcommentphidmig.php',1390001563),('phabricator:20130621.diffcommentunphid.sql',1390001563),('phabricator:20130622.doorkeeper.sql',1390001563),('phabricator:20130628.legalpadv0.sql',1390001563),('phabricator:20130701.conduitlog.sql',1390001563),('phabricator:20130703.legalpaddocdenorm.php',1390001563),('phabricator:20130703.legalpaddocdenorm.sql',1390001563),('phabricator:20130709.droptimeline.sql',1390001563),('phabricator:20130709.legalpadsignature.sql',1390001563),('phabricator:20130711.pholioimageobsolete.php',1390001563),('phabricator:20130711.pholioimageobsolete.sql',1390001563),('phabricator:20130711.pholioimageobsolete2.sql',1390001563),('phabricator:20130711.trimrealnames.php',1390001563),('phabricator:20130714.votexactions.sql',1390001563),('phabricator:20130715.votecomments.php',1390001563),('phabricator:20130715.voteedges.sql',1390001563),('phabricator:20130716.archivememberlessprojects.php',1390001563),('phabricator:20130722.pholioreplace.sql',1390001563),('phabricator:20130723.taskstarttime.sql',1390001563),('phabricator:20130726.ponderxactions.sql',1390001564),('phabricator:20130727.ponderquestionstatus.sql',1390001563),('phabricator:20130728.ponderunique.php',1390001564),('phabricator:20130728.ponderuniquekey.sql',1390001564),('phabricator:20130728.ponderxcomment.php',1390001564),('phabricator:20130731.releephcutpointidentifier.sql',1390001564),('phabricator:20130731.releephproject.sql',1390001564),('phabricator:20130731.releephrepoid.sql',1390001564),('phabricator:20130801.pastexactions.php',1390001564),('phabricator:20130801.pastexactions.sql',1390001564),('phabricator:20130802.heraldphid.sql',1390001564),('phabricator:20130802.heraldphids.php',1390001564),('phabricator:20130802.heraldphidukey.sql',1390001564),('phabricator:20130802.heraldxactions.sql',1390001564),('phabricator:20130805.pasteedges.sql',1390001564),('phabricator:20130805.pastemailkey.sql',1390001564),('phabricator:20130805.pastemailkeypop.php',1390001564),('phabricator:20130814.usercustom.sql',1390001564),('phabricator:20130820.file-mailkey-populate.php',1390001564),('phabricator:20130820.filemailkey.sql',1390001564),('phabricator:20130820.filexactions.sql',1390001564),('phabricator:20130820.releephxactions.sql',1390001564),('phabricator:20130826.divinernode.sql',1390001564),('phabricator:20130912.maniphest.1.touch.sql',1390001564),('phabricator:20130912.maniphest.2.created.sql',1390001564),('phabricator:20130912.maniphest.3.nameindex.sql',1390001564),('phabricator:20130912.maniphest.4.fillindex.php',1390001564),('phabricator:20130913.maniphest.1.migratesearch.php',1390001564),('phabricator:20130914.usercustom.sql',1390001564),('phabricator:20130915.maniphestcustom.sql',1390001564),('phabricator:20130915.maniphestmigrate.php',1390001564),('phabricator:20130915.maniphestqdrop.sql',1390001565),('phabricator:20130919.mfieldconf.php',1390001564),('phabricator:20130920.repokeyspolicy.sql',1390001564),('phabricator:20130921.mtransactions.sql',1390001564),('phabricator:20130921.xmigratemaniphest.php',1390001564),('phabricator:20130923.mrename.sql',1390001564),('phabricator:20130924.mdraftkey.sql',1390001564),('phabricator:20130925.mpolicy.sql',1390001564),('phabricator:20130925.xpolicy.sql',1390001564),('phabricator:20130926.dcustom.sql',1390001564),('phabricator:20130926.dinkeys.sql',1390001565),('phabricator:20130926.dinline.php',1390001565),('phabricator:20130927.audiomacro.sql',1390001565),('phabricator:20130929.filepolicy.sql',1390001565),('phabricator:20131004.dxedgekey.sql',1390001565),('phabricator:20131004.dxreviewers.php',1390001565),('phabricator:20131006.hdisable.sql',1390001565),('phabricator:20131010.pstorage.sql',1390001565),('phabricator:20131015.cpolicy.sql',1390001565),('phabricator:20131020.col1.sql',1390001565),('phabricator:20131020.harbormaster.sql',1390001565),('phabricator:20131020.pcustom.sql',1390001565),('phabricator:20131020.pxaction.sql',1390001565),('phabricator:20131020.pxactionmig.php',1390001565),('phabricator:20131025.repopush.sql',1390001565),('phabricator:20131026.commitstatus.sql',1390001565),('phabricator:20131030.repostatusmessage.sql',1390001565),('phabricator:20131031.vcspassword.sql',1390001565),('phabricator:20131105.buildstep.sql',1390001565),('phabricator:20131106.diffphid.1.col.sql',1390001565),('phabricator:20131106.diffphid.2.mig.php',1390001565),('phabricator:20131106.diffphid.3.key.sql',1390001565),('phabricator:20131106.nuance-v0.sql',1390001565),('phabricator:20131107.buildlog.sql',1390001565),('phabricator:20131112.userverified.1.col.sql',1390001565),('phabricator:20131112.userverified.2.mig.php',1390001565),('phabricator:20131118.ownerorder.php',1390001565),('phabricator:20131119.passphrase.sql',1390001565),('phabricator:20131120.nuancesourcetype.sql',1390001565),('phabricator:20131121.passphraseedge.sql',1390001565),('phabricator:20131121.repocredentials.1.col.sql',1390001565),('phabricator:20131121.repocredentials.2.mig.php',1390001565),('phabricator:20131122.repomirror.sql',1390001565),('phabricator:20131123.drydockblueprintpolicy.sql',1390001565),('phabricator:20131129.drydockresourceblueprint.sql',1390001565),('phabricator:20131204.pushlog.sql',1390001565),('phabricator:20131205.buildsteporder.sql',1390001565),('phabricator:20131205.buildstepordermig.php',1390001565),('phabricator:20131205.buildtargets.sql',1390001565),('phabricator:20131206.phragment.sql',1390001565),('phabricator:20131206.phragmentnull.sql',1390001565),('phabricator:20131208.phragmentsnapshot.sql',1390001565),('phabricator:20131211.phragmentedges.sql',1390001565),('phabricator:20131217.pushlogphid.1.col.sql',1390001565),('phabricator:20131217.pushlogphid.2.mig.php',1390001565),('phabricator:20131217.pushlogphid.3.key.sql',1390001565),('phabricator:20131219.pxdrop.sql',1390001565),('phabricator:20131224.harbormanual.sql',1390001566),('phabricator:20131227.heraldobject.sql',1390001566),('phabricator:20131231.dropshortcut.sql',1390001566),('phabricator:20131302.maniphestvalue.sql',1390001562),('phabricator:20140104.harbormastercmd.sql',1390001566),('phabricator:20140106.macromailkey.1.sql',1390001566),('phabricator:20140106.macromailkey.2.php',1390001566),('phabricator:20140108.ddbpname.1.sql',1390001566),('phabricator:20140108.ddbpname.2.php',1390001566),('phabricator:20140109.ddxactions.sql',1390001566),('phabricator:20140109.projectcolumnsdates.sql',1390001566),('phabricator:20140113.legalpadsig.1.sql',1390001566),('phabricator:20140113.legalpadsig.2.php',1390001566),('phabricator:20140115.auth.1.id.sql',1390001566),('phabricator:20140115.auth.2.expires.sql',1390001566),('phabricator:20140115.auth.3.unlimit.php',1390001566),('phabricator:20140115.legalpadsigkey.sql',1390001566),('phabricator:20140116.reporefcursor.sql',1390001566),('phabricator:daemonstatus.sql',1358377823),('phabricator:daemonstatuskey.sql',1358377828),('phabricator:daemontaskarchive.sql',1358377838),('phabricator:db.audit',1),('phabricator:db.auth',1390001562),('phabricator:db.cache',1358377804),('phabricator:db.calendar',1358377802),('phabricator:db.chatlog',1),('phabricator:db.conduit',1),('phabricator:db.config',1358377852),('phabricator:db.conpherence',1390001562),('phabricator:db.countdown',1),('phabricator:db.daemon',1),('phabricator:db.differential',1),('phabricator:db.diviner',1390001562),('phabricator:db.doorkeeper',1390001562),('phabricator:db.draft',1),('phabricator:db.drydock',1),('phabricator:db.fact',1358377804),('phabricator:db.feed',1),('phabricator:db.file',1),('phabricator:db.flag',1),('phabricator:db.harbormaster',1358377803),('phabricator:db.herald',1),('phabricator:db.legalpad',1390001562),('phabricator:db.maniphest',1),('phabricator:db.metamta',1),('phabricator:db.meta_data',1),('phabricator:db.nuance',1390001562),('phabricator:db.oauth_server',1),('phabricator:db.owners',1),('phabricator:db.passphrase',1390001562),('phabricator:db.pastebin',1),('phabricator:db.phame',1),('phabricator:db.phid',1),('phabricator:db.phlux',1390001562),('phabricator:db.pholio',1358377807),('phabricator:db.phortune',1390001562),('phabricator:db.phragment',1390001562),('phabricator:db.phrequent',1390001562),('phabricator:db.phriction',1),('phabricator:db.policy',1390001562),('phabricator:db.ponder',1358377805),('phabricator:db.project',1),('phabricator:db.releeph',1390001562),('phabricator:db.repository',1),('phabricator:db.search',1),('phabricator:db.slowvote',1),('phabricator:db.timeline',1),('phabricator:db.token',1390001562),('phabricator:db.user',1),('phabricator:db.worker',1),('phabricator:db.xhpastview',1),('phabricator:db.xhprof',1358377806),('phabricator:differentialbookmarks.sql',1358377817),('phabricator:draft-metadata.sql',1358377832),('phabricator:dropfileproxyimage.sql',1358377843),('phabricator:drydockresoucetype.sql',1358377840),('phabricator:drydocktaskid.sql',1358377839),('phabricator:edgetype.sql',1358377829),('phabricator:emailtable.sql',1358377810),('phabricator:emailtableport.sql',1358377811),('phabricator:emailtableremove.sql',1358377811),('phabricator:fact-raw.sql',1358377825),('phabricator:harbormasterobject.sql',1358377817),('phabricator:holidays.sql',1358377808),('phabricator:ldapinfo.sql',1358377814),('phabricator:legalpad-mailkey-populate.php',1390001563),('phabricator:legalpad-mailkey.sql',1390001563),('phabricator:liskcounters-task.sql',1358377844),('phabricator:liskcounters.php',1358377842),('phabricator:liskcounters.sql',1358377841),('phabricator:maniphestxcache.sql',1358377819),('phabricator:markupcache.sql',1358377818),('phabricator:migrate-differential-dependencies.php',1358377820),('phabricator:migrate-maniphest-dependencies.php',1358377820),('phabricator:migrate-maniphest-revisions.php',1358377822),('phabricator:migrate-project-edges.php',1358377824),('phabricator:owners-exclude.sql',1358377846),('phabricator:pastepolicy.sql',1358377831),('phabricator:phameblog.sql',1358377821),('phabricator:phamedomain.sql',1358377833),('phabricator:phameoneblog.sql',1358377837),('phabricator:phamepolicy.sql',1358377836),('phabricator:phiddrop.sql',1358377812),('phabricator:pholio.sql',1358377846),('phabricator:policy-project.sql',1358377827),('phabricator:ponder-comments.sql',1358377830),('phabricator:ponder-mailkey-populate.php',1358377835),('phabricator:ponder-mailkey.sql',1358377834),('phabricator:ponder.sql',1358377826),('phabricator:releeph.sql',1390001562),('phabricator:repository-lint.sql',1358377843),('phabricator:statustxt.sql',1358377837),('phabricator:symbolcontexts.sql',1358377823),('phabricator:testdatabase.sql',1358377813),('phabricator:threadtopic.sql',1358377815),('phabricator:userstatus.sql',1358377809),('phabricator:usertranslation.sql',1358377816),('phabricator:xhprof.sql',1358377832); CREATE DATABASE `{$NAMESPACE}_metamta` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_metamta`; @@ -1274,6 +1405,7 @@ CREATE TABLE `metamta_receivedmail` ( `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, `messageIDHash` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `status` varchar(32) NOT NULL, PRIMARY KEY (`id`), KEY `relatedPHID` (`relatedPHID`), KEY `authorPHID` (`authorPHID`), @@ -1380,6 +1512,25 @@ CREATE TABLE `owners_path` ( CREATE DATABASE `{$NAMESPACE}_pastebin` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_pastebin`; +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `pastebin_paste` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, @@ -1391,9 +1542,55 @@ CREATE TABLE `pastebin_paste` ( `language` varchar(64) NOT NULL, `parentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `mailKey` varchar(20) NOT NULL, PRIMARY KEY (`id`), KEY `parentPHID` (`parentPHID`), - KEY `authorPHID` (`authorPHID`) + KEY `authorPHID` (`authorPHID`), + KEY `key_dateCreated` (`dateCreated`), + KEY `key_language` (`language`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `pastebin_pastetransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `pastebin_pastetransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `lineNumber` int(10) unsigned DEFAULT NULL, + `lineLength` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1459,12 +1656,12 @@ CREATE TABLE `phame_post` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE DATABASE `{$NAMESPACE}_pholio` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_pholio`; +CREATE DATABASE `{$NAMESPACE}_phriction` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_phriction`; CREATE TABLE `edge` ( `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` int(10) unsigned NOT NULL, `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `seq` int(10) unsigned NOT NULL, @@ -1481,88 +1678,6 @@ CREATE TABLE `edgedata` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE `pholio_image` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `mockID` int(10) unsigned NOT NULL, - `filePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `name` varchar(128) NOT NULL, - `description` longtext NOT NULL, - `sequence` int(10) unsigned NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `mockID` (`mockID`,`sequence`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `pholio_mock` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `name` varchar(128) NOT NULL, - `originalName` varchar(128) NOT NULL, - `description` longtext NOT NULL, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `coverPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `phid` (`phid`), - KEY `authorPHID` (`authorPHID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `pholio_transaction` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, - `commentVersion` int(10) unsigned NOT NULL, - `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `key_phid` (`phid`), - KEY `key_object` (`objectPHID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `pholio_transaction_comment` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `commentVersion` int(10) unsigned NOT NULL, - `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `isDeleted` tinyint(1) NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - `mockID` int(10) unsigned DEFAULT NULL, - `imageID` int(10) unsigned DEFAULT NULL, - `x` int(10) unsigned DEFAULT NULL, - `y` int(10) unsigned DEFAULT NULL, - `width` int(10) unsigned DEFAULT NULL, - `height` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `key_phid` (`phid`), - UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`), - UNIQUE KEY `key_draft` (`authorPHID`,`mockID`,`transactionPHID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE DATABASE `{$NAMESPACE}_phriction` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_phriction`; - CREATE TABLE `phriction_content` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `documentID` int(10) unsigned NOT NULL, @@ -1597,78 +1712,6 @@ CREATE TABLE `phriction_document` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE DATABASE `{$NAMESPACE}_ponder` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_ponder`; - -CREATE TABLE `edge` ( - `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `type` int(10) unsigned NOT NULL, - `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `seq` int(10) unsigned NOT NULL, - `dataID` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`src`,`type`,`dst`), - KEY `src` (`src`,`type`,`dateCreated`,`seq`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `edgedata` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `ponder_answer` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `questionID` int(10) unsigned NOT NULL, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `voteCount` int(10) NOT NULL, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `content` longtext NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - `contentSource` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `phid` (`phid`), - KEY `questionID` (`questionID`), - KEY `authorPHID` (`authorPHID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `ponder_comment` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `targetPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `content` longtext NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `authorPHID` (`authorPHID`), - KEY `targetPHID` (`targetPHID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `ponder_question` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `title` varchar(255) NOT NULL, - `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `voteCount` int(10) NOT NULL, - `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `content` longtext NOT NULL, - `dateCreated` int(10) unsigned NOT NULL, - `dateModified` int(10) unsigned NOT NULL, - `contentSource` varchar(255) DEFAULT NULL, - `heat` float NOT NULL, - `answerCount` int(10) unsigned NOT NULL, - `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `phid` (`phid`), - KEY `authorPHID` (`authorPHID`), - KEY `heat` (`heat`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=11; - - CREATE DATABASE `{$NAMESPACE}_project` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_project`; @@ -1725,6 +1768,52 @@ CREATE TABLE `project_affiliation` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `project_column` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) NOT NULL, + `sequence` int(10) unsigned NOT NULL, + `projectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_sequence` (`projectPHID`,`sequence`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `project_customfieldnumericindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`), + KEY `key_find` (`indexKey`,`indexValue`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `project_customfieldstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldIndex` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldValue` longtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `objectPHID` (`objectPHID`,`fieldIndex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `project_customfieldstringindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`(64)), + KEY `key_find` (`indexKey`,`indexValue`(64)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `project_profile` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `projectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, @@ -1747,15 +1836,23 @@ CREATE TABLE `project_subproject` ( CREATE TABLE `project_transaction` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `projectID` int(10) unsigned NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `transactionType` varchar(32) NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), - KEY `projectID` (`projectID`) + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1791,9 +1888,15 @@ CREATE TABLE `repository` ( `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, `uuid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `pushPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `credentialPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `callsign` (`callsign`), - UNIQUE KEY `phid` (`phid`) + UNIQUE KEY `phid` (`phid`), + KEY `key_name` (`name`), + KEY `key_vcs` (`versionControlSystem`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1850,11 +1953,14 @@ CREATE TABLE `repository_commit` ( `mailKey` varchar(20) NOT NULL, `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `auditStatus` int(10) unsigned NOT NULL, + `summary` varchar(80) NOT NULL, + `importStatus` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `phid` (`phid`), - UNIQUE KEY `repositoryID` (`repositoryID`,`commitIdentifier`(16)), + UNIQUE KEY `key_commit_identity` (`commitIdentifier`,`repositoryID`), KEY `repositoryID_2` (`repositoryID`,`epoch`), - KEY `authorPHID` (`authorPHID`,`auditStatus`,`epoch`) + KEY `authorPHID` (`authorPHID`,`auditStatus`,`epoch`), + KEY `repositoryID` (`repositoryID`,`importStatus`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1887,13 +1993,29 @@ CREATE TABLE `repository_lintmessage` ( `branchID` int(10) unsigned NOT NULL, `path` varchar(512) NOT NULL, `line` int(10) unsigned NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `code` varchar(32) NOT NULL, `severity` varchar(16) NOT NULL, `name` varchar(255) NOT NULL, `description` text NOT NULL, PRIMARY KEY (`id`), KEY `branchID` (`branchID`,`path`(64)), - KEY `branchID_2` (`branchID`,`code`,`path`(64)) + KEY `branchID_2` (`branchID`,`code`,`path`(64)), + KEY `key_author` (`authorPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `repository_mirror` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `remoteURI` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `credentialPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_repository` (`repositoryPHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1921,13 +2043,56 @@ CREATE TABLE `repository_pathchange` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE `repository_shortcut` ( +CREATE TABLE `repository_pushlog` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL, - `href` varchar(255) NOT NULL, - `description` varchar(255) NOT NULL, - `sequence` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`) + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `epoch` int(10) unsigned NOT NULL, + `repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `pusherPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `remoteAddress` int(10) unsigned DEFAULT NULL, + `remoteProtocol` varchar(32) DEFAULT NULL, + `transactionKey` char(12) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `refType` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `refNameHash` varchar(12) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `refNameRaw` longtext CHARACTER SET latin1 COLLATE latin1_bin, + `refNameEncoding` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `refOld` varchar(40) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `refNew` varchar(40) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `mergeBase` varchar(40) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `changeFlags` int(10) unsigned NOT NULL, + `rejectCode` int(10) unsigned NOT NULL, + `rejectDetails` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_repository` (`repositoryPHID`), + KEY `key_ref` (`repositoryPHID`,`refNew`), + KEY `key_pusher` (`pusherPHID`), + KEY `key_name` (`repositoryPHID`,`refNameHash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `repository_refcursor` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `refType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `refNameHash` varchar(12) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `refNameRaw` longtext CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `refNameEncoding` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commitIdentifier` varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + KEY `key_cursor` (`repositoryPHID`,`refType`,`refNameHash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `repository_statusmessage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `repositoryID` int(10) unsigned NOT NULL, + `statusType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `statusCode` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `parameters` longtext NOT NULL, + `epoch` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `repositoryID` (`repositoryID`,`statusType`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1936,7 +2101,8 @@ CREATE TABLE `repository_summary` ( `size` int(10) unsigned NOT NULL, `lastCommitID` int(10) unsigned NOT NULL, `epoch` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`repositoryID`) + PRIMARY KEY (`repositoryID`), + KEY `key_epoch` (`epoch`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1952,6 +2118,39 @@ CREATE TABLE `repository_symbol` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `repository_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `repository_vcspassword` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `passwordHash` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`userPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE DATABASE `{$NAMESPACE}_search` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_search`; @@ -1989,6 +2188,22 @@ CREATE TABLE `search_documentrelationship` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `search_namedquery` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `engineClassName` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `queryName` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `queryKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `isBuiltin` tinyint(1) NOT NULL DEFAULT '0', + `isDisabled` tinyint(1) NOT NULL DEFAULT '0', + `sequence` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `key_userquery` (`userPHID`,`engineClassName`,`queryKey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `search_query` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `query` varchar(255) NOT NULL, @@ -2001,9 +2216,40 @@ CREATE TABLE `search_query` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `search_savedquery` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `engineClassName` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `parameters` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `queryKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_queryKey` (`queryKey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE DATABASE `{$NAMESPACE}_slowvote` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_slowvote`; +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `slowvote_choice` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `pollID` int(10) unsigned NOT NULL, @@ -2050,35 +2296,51 @@ CREATE TABLE `slowvote_poll` ( `method` int(10) unsigned NOT NULL, `dateCreated` int(10) unsigned NOT NULL, `dateModified` int(10) unsigned NOT NULL, + `description` longtext NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `phid` (`phid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE DATABASE `{$NAMESPACE}_timeline` /*!40100 DEFAULT CHARACTER SET utf8 */; -USE `{$NAMESPACE}_timeline`; - -CREATE TABLE `timeline_cursor` ( - `name` varchar(255) NOT NULL, - `position` int(10) unsigned NOT NULL, - PRIMARY KEY (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - - -CREATE TABLE `timeline_event` ( +CREATE TABLE `slowvote_transaction` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `type` char(4) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - `dataID` int(10) unsigned DEFAULT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `dataID` (`dataID`), - KEY `type` (`type`,`id`) + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE `timeline_eventdata` ( +CREATE TABLE `slowvote_transaction_comment` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `eventData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, - PRIMARY KEY (`id`) + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -2105,12 +2367,16 @@ CREATE TABLE `edgedata` ( CREATE TABLE `phabricator_session` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `sessionKey` varchar(40) NOT NULL, `sessionStart` int(10) unsigned NOT NULL, - PRIMARY KEY (`userPHID`,`type`), - UNIQUE KEY `sessionKey` (`sessionKey`) + `sessionExpires` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `sessionKey` (`sessionKey`), + KEY `key_identity` (`userPHID`,`type`), + KEY `key_expires` (`sessionExpires`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -2134,10 +2400,45 @@ CREATE TABLE `user` ( `isDisabled` tinyint(1) NOT NULL, `isAdmin` tinyint(1) NOT NULL, `timezoneIdentifier` varchar(255) NOT NULL, + `isEmailVerified` int(10) unsigned NOT NULL, + `isApproved` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `userName` (`userName`), UNIQUE KEY `phid` (`phid`), - KEY `realName` (`realName`) + KEY `realName` (`realName`), + KEY `key_approved` (`isApproved`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `user_configuredcustomfieldstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldIndex` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fieldValue` longtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `objectPHID` (`objectPHID`,`fieldIndex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `user_customfieldnumericindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`), + KEY `key_find` (`indexKey`,`indexValue`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `user_customfieldstringindex` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexKey` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `indexValue` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY `key_join` (`objectPHID`,`indexKey`,`indexValue`(64)), + KEY `key_find` (`indexKey`,`indexValue`(64)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -2156,6 +2457,31 @@ CREATE TABLE `user_email` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `user_externalaccount` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `accountType` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `accountDomain` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `accountSecret` longtext CHARACTER SET utf8 COLLATE utf8_bin, + `accountID` varchar(160) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `displayName` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `realName` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `emailVerified` tinyint(1) NOT NULL, + `accountURI` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `profileImagePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `properties` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + UNIQUE KEY `account_details` (`accountType`,`accountDomain`,`accountID`), + KEY `key_userAccounts` (`userPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE TABLE `user_ldapinfo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `userID` int(10) unsigned NOT NULL, @@ -2268,6 +2594,28 @@ CREATE TABLE `user_status` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `user_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE DATABASE `{$NAMESPACE}_worker` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_worker`; @@ -2277,8 +2625,8 @@ CREATE TABLE `lisk_counter` ( PRIMARY KEY (`counterName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `lisk_counter` (`counterName`, `counterValue`) VALUES -('worker_activetask', 2); + +INSERT INTO `lisk_counter` VALUES ('worker_activetask',2); CREATE TABLE `worker_activetask` ( `id` int(10) unsigned NOT NULL, @@ -2287,11 +2635,13 @@ CREATE TABLE `worker_activetask` ( `leaseExpires` int(10) unsigned DEFAULT NULL, `failureCount` int(10) unsigned NOT NULL, `dataID` int(10) unsigned DEFAULT NULL, + `failureTime` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `dataID` (`dataID`), KEY `taskClass` (`taskClass`), KEY `leaseExpires` (`leaseExpires`), - KEY `leaseOwner` (`leaseOwner`(16)) + KEY `leaseOwner` (`leaseOwner`(16)), + KEY `key_failuretime` (`failureTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -2332,6 +2682,232 @@ CREATE TABLE `xhpastview_parsetree` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE DATABASE `{$NAMESPACE}_cache` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_cache`; + +CREATE TABLE `cache_general` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `cacheKeyHash` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `cacheKey` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `cacheFormat` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `cacheData` longblob NOT NULL, + `cacheCreated` int(10) unsigned NOT NULL, + `cacheExpires` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_cacheKeyHash` (`cacheKeyHash`), + KEY `key_cacheCreated` (`cacheCreated`), + KEY `key_ttl` (`cacheExpires`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `cache_markupcache` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `cacheKey` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `cacheData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `cacheKey` (`cacheKey`), + KEY `dateCreated` (`dateCreated`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_fact` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_fact`; + +CREATE TABLE `fact_aggregate` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `factType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `valueX` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `factType` (`factType`,`objectPHID`), + KEY `factType_2` (`factType`,`valueX`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `fact_cursor` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `position` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `fact_raw` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `factType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectA` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `valueX` bigint(20) NOT NULL, + `valueY` bigint(20) NOT NULL, + `epoch` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `objectPHID` (`objectPHID`), + KEY `factType` (`factType`,`epoch`), + KEY `factType_2` (`factType`,`objectA`,`epoch`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_ponder` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_ponder`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` int(10) unsigned NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_answer` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `questionID` int(10) unsigned NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `voteCount` int(10) NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `content` longtext NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `contentSource` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + UNIQUE KEY `key_oneanswerperquestion` (`questionID`,`authorPHID`), + KEY `questionID` (`questionID`), + KEY `authorPHID` (`authorPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_answertransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_answertransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `targetPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `content` longtext NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `authorPHID` (`authorPHID`), + KEY `targetPHID` (`targetPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_question` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `voteCount` int(10) NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `status` int(10) unsigned NOT NULL, + `content` longtext NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `contentSource` varchar(255) DEFAULT NULL, + `heat` float NOT NULL, + `answerCount` int(10) unsigned NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + KEY `authorPHID` (`authorPHID`), + KEY `heat` (`heat`), + KEY `status` (`status`) +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_questiontransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `ponder_questiontransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + CREATE DATABASE `{$NAMESPACE}_xhprof` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `{$NAMESPACE}_xhprof`; @@ -2349,3 +2925,1327 @@ CREATE TABLE `xhprof_sample` ( PRIMARY KEY (`id`), UNIQUE KEY `filePHID` (`filePHID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_pholio` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_pholio`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `pholio_image` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `mockID` int(10) unsigned DEFAULT NULL, + `filePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(128) NOT NULL, + `description` longtext NOT NULL, + `sequence` int(10) unsigned NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `isObsolete` tinyint(1) NOT NULL DEFAULT '0', + `replacesImagePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `keyPHID` (`phid`), + KEY `mockID` (`mockID`,`isObsolete`,`sequence`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `pholio_mock` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(128) NOT NULL, + `originalName` varchar(128) NOT NULL, + `description` longtext NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `coverPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + KEY `authorPHID` (`authorPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `pholio_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `pholio_transaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `mockID` int(10) unsigned DEFAULT NULL, + `imageID` int(10) unsigned DEFAULT NULL, + `x` int(10) unsigned DEFAULT NULL, + `y` int(10) unsigned DEFAULT NULL, + `width` int(10) unsigned DEFAULT NULL, + `height` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`), + UNIQUE KEY `key_draft` (`authorPHID`,`mockID`,`transactionPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_conpherence` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_conpherence`; + +CREATE TABLE `conpherence_participant` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `participantPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `conpherencePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `participationStatus` int(10) unsigned NOT NULL DEFAULT '0', + `dateTouched` int(10) unsigned NOT NULL, + `behindTransactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `seenMessageCount` bigint(20) unsigned NOT NULL, + `settings` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `conpherencePHID` (`conpherencePHID`,`participantPHID`), + KEY `unreadCount` (`participantPHID`,`participationStatus`), + KEY `participationIndex` (`participantPHID`,`dateTouched`,`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `conpherence_thread` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `title` varchar(255) DEFAULT NULL, + `messageCount` bigint(20) unsigned NOT NULL, + `recentParticipantPHIDs` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `conpherence_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `conpherence_transaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `conpherencePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`), + UNIQUE KEY `key_draft` (`authorPHID`,`conpherencePHID`,`transactionPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_config` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_config`; + +CREATE TABLE `config_entry` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `namespace` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `configKey` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `value` longtext NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_name` (`namespace`,`configKey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `config_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_token` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_token`; + +CREATE TABLE `token_count` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `tokenCount` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_objectPHID` (`objectPHID`), + KEY `key_count` (`tokenCount`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `token_given` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `tokenPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_all` (`objectPHID`,`authorPHID`), + KEY `key_author` (`authorPHID`), + KEY `key_token` (`tokenPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_releeph` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_releeph`; + +CREATE TABLE `releeph_branch` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `basename` varchar(64) NOT NULL, + `releephProjectID` int(10) unsigned NOT NULL, + `createdByUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `cutPointCommitPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isActive` tinyint(1) NOT NULL DEFAULT '1', + `symbolicName` varchar(64) DEFAULT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(128) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `releephProjectID_2` (`releephProjectID`,`basename`), + UNIQUE KEY `releephProjectID_name` (`releephProjectID`,`name`), + UNIQUE KEY `releephProjectID` (`releephProjectID`,`symbolicName`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_branchtransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_event` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `releephProjectID` int(10) unsigned NOT NULL, + `releephBranchID` int(10) unsigned DEFAULT NULL, + `type` varchar(32) NOT NULL, + `epoch` int(10) unsigned DEFAULT NULL, + `actorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_project` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) NOT NULL, + `trunkBranch` varchar(255) NOT NULL, + `repositoryPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `arcanistProjectID` int(10) unsigned NOT NULL, + `createdByUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isActive` tinyint(1) NOT NULL DEFAULT '1', + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `projectName` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_projecttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_request` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `branchID` int(10) unsigned NOT NULL, + `summary` longtext CHARACTER SET utf8 COLLATE utf8_bin, + `requestUserPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `requestCommitPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commitIdentifier` varchar(40) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commitPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `pickStatus` tinyint(4) DEFAULT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `userIntents` longtext CHARACTER SET utf8 COLLATE utf8_bin, + `inBranch` tinyint(1) NOT NULL DEFAULT '0', + `mailKey` varchar(20) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`), + UNIQUE KEY `requestIdentifierBranch` (`requestCommitPHID`,`branchID`), + KEY `branchID` (`branchID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_requestevent` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `releephRequestID` int(10) unsigned NOT NULL, + `actorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `details` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(32) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_requesttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `releeph_requesttransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_phlux` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_phlux`; + +CREATE TABLE `phlux_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phlux_variable` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `variableKey` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `variableValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_key` (`variableKey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_phortune` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_phortune`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` int(10) unsigned NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phortune_account` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) NOT NULL, + `balanceInCents` bigint(20) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phortune_accounttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phortune_paymentmethod` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) NOT NULL, + `status` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `accountPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `brand` varchar(64) NOT NULL, + `expires` varchar(16) NOT NULL, + `providerType` varchar(16) NOT NULL, + `providerDomain` varchar(64) NOT NULL, + `lastFourDigits` varchar(16) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_account` (`accountPHID`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phortune_product` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `productName` varchar(255) NOT NULL, + `productType` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `status` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `priceInCents` bigint(20) NOT NULL, + `billingIntervalInMonths` int(10) unsigned DEFAULT NULL, + `trialPeriodInDays` int(10) unsigned DEFAULT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phortune_producttransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_phrequent` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_phrequent`; + +CREATE TABLE `phrequent_usertime` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `note` longtext CHARACTER SET utf8 COLLATE utf8_bin, + `dateStarted` int(10) unsigned NOT NULL, + `dateEnded` int(10) unsigned DEFAULT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_diviner` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_diviner`; + +CREATE TABLE `diviner_liveatom` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `symbolPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `atomData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `symbolPHID` (`symbolPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `diviner_livebook` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `configurationData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + UNIQUE KEY `phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `diviner_livesymbol` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `bookPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `context` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `atomIndex` int(10) unsigned NOT NULL, + `identityHash` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `graphHash` varchar(33) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + `groupName` varchar(255) DEFAULT NULL, + `summary` longtext CHARACTER SET utf8 COLLATE utf8_bin, + `isDocumentable` tinyint(1) NOT NULL, + `nodeHash` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `identityHash` (`identityHash`), + UNIQUE KEY `phid` (`phid`), + UNIQUE KEY `graphHash` (`graphHash`), + UNIQUE KEY `nodeHash` (`nodeHash`), + KEY `bookPHID` (`bookPHID`,`type`,`name`(64),`context`(64),`atomIndex`), + KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_auth` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_auth`; + +CREATE TABLE `auth_providerconfig` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `providerClass` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `providerType` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `providerDomain` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isEnabled` tinyint(1) NOT NULL, + `shouldAllowLogin` tinyint(1) NOT NULL, + `shouldAllowRegistration` tinyint(1) NOT NULL, + `shouldAllowLink` tinyint(1) NOT NULL, + `shouldAllowUnlink` tinyint(1) NOT NULL, + `properties` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_provider` (`providerType`,`providerDomain`), + KEY `key_class` (`providerClass`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `auth_providerconfigtransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_doorkeeper` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_doorkeeper`; + +CREATE TABLE `doorkeeper_externalobject` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectKey` char(12) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `applicationType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `applicationDomain` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectURI` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `importerPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `properties` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_object` (`objectKey`), + KEY `key_full` (`applicationType`,`applicationDomain`,`objectType`,`objectID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` int(10) unsigned NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_legalpad` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_legalpad`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `legalpad_document` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `title` varchar(255) NOT NULL, + `contributorCount` int(10) unsigned NOT NULL DEFAULT '0', + `recentContributorPHIDs` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `creatorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `versions` int(10) unsigned NOT NULL DEFAULT '0', + `documentBodyPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_creator` (`creatorPHID`,`dateModified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `legalpad_documentbody` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `creatorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `documentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `version` int(10) unsigned NOT NULL DEFAULT '0', + `title` varchar(255) NOT NULL, + `text` longtext, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_document` (`documentPHID`,`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `legalpad_documentsignature` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `documentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `documentVersion` int(10) unsigned NOT NULL DEFAULT '0', + `signerPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `signatureData` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `secretKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `verified` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`), + KEY `key_signer` (`signerPHID`,`dateModified`), + KEY `secretKey` (`secretKey`), + KEY `key_document` (`documentPHID`,`signerPHID`,`documentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `legalpad_transaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `legalpad_transaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `documentID` int(10) unsigned DEFAULT NULL, + `lineNumber` int(10) unsigned NOT NULL, + `lineLength` int(10) unsigned NOT NULL, + `fixedState` varchar(12) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `hasReplies` tinyint(1) NOT NULL, + `replyToCommentPHID` varchar(64) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`), + UNIQUE KEY `key_draft` (`authorPHID`,`documentID`,`transactionPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_policy` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_policy`; + +CREATE TABLE `policy` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `rules` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `defaultAction` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_nuance` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_nuance`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_item` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `ownerPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `requestorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sourcePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sourceLabel` varchar(255) DEFAULT NULL, + `status` int(10) unsigned NOT NULL, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + `dateNuanced` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_source` (`sourcePHID`,`status`,`dateNuanced`,`id`), + KEY `key_owner` (`ownerPHID`,`status`,`dateNuanced`,`id`), + KEY `key_contacter` (`requestorPHID`,`status`,`dateNuanced`,`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_itemtransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_itemtransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_queue` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) DEFAULT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) NOT NULL, + `editPolicy` varchar(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_queueitem` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `queuePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `itemPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `itemStatus` int(10) unsigned NOT NULL, + `itemDateNuanced` int(10) unsigned NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_one_per_queue` (`itemPHID`,`queuePHID`), + KEY `key_queue` (`queuePHID`,`itemStatus`,`itemDateNuanced`,`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_queuetransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_queuetransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_requestor` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_requestorsource` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `requestorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sourcePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sourceKey` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_source_key` (`sourcePHID`,`sourceKey`), + KEY `key_requestor` (`requestorPHID`,`id`), + KEY `key_source` (`sourcePHID`,`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_requestortransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_requestortransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_source` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) DEFAULT NULL, + `type` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `mailKey` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) NOT NULL, + `editPolicy` varchar(64) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_type` (`type`,`dateModified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_sourcetransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `nuance_sourcetransaction_comment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `transactionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `content` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `isDeleted` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_passphrase` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_passphrase`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `passphrase_credential` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(255) NOT NULL, + `credentialType` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `providesType` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `description` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `username` varchar(255) NOT NULL, + `secretID` int(10) unsigned DEFAULT NULL, + `isDestroyed` tinyint(1) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_secret` (`secretID`), + KEY `key_type` (`credentialType`), + KEY `key_provides` (`providesType`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `passphrase_credentialtransaction` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `authorPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `objectPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `commentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `commentVersion` int(10) unsigned NOT NULL, + `transactionType` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `oldValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `newValue` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `contentSource` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `metadata` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `passphrase_secret` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `secretData` longblob NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE DATABASE `{$NAMESPACE}_phragment` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `{$NAMESPACE}_phragment`; + +CREATE TABLE `edge` ( + `src` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `type` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dst` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `seq` int(10) unsigned NOT NULL, + `dataID` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`src`,`type`,`dst`), + KEY `src` (`src`,`type`,`dateCreated`,`seq`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `edgedata` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `data` longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phragment_fragment` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `path` varchar(254) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `depth` int(10) unsigned NOT NULL, + `latestVersionPHID` varchar(64) DEFAULT NULL, + `viewPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `editPolicy` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_path` (`path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phragment_fragmentversion` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sequence` int(10) unsigned NOT NULL, + `fragmentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `filePHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_version` (`fragmentPHID`,`sequence`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phragment_snapshot` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `phid` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `primaryFragmentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `name` varchar(192) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `description` longtext CHARACTER SET utf8 COLLATE utf8_bin, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_name` (`primaryFragmentPHID`,`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `phragment_snapshotchild` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `snapshotPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fragmentPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `fragmentVersionPHID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_child` (`snapshotPHID`,`fragmentPHID`,`fragmentVersionPHID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/resources/sshd/phabricator-ssh-hook.sh b/resources/sshd/phabricator-ssh-hook.sh index e405729cef..fc7de81dad 100755 --- a/resources/sshd/phabricator-ssh-hook.sh +++ b/resources/sshd/phabricator-ssh-hook.sh @@ -1,8 +1,14 @@ #!/bin/sh -### -### WARNING: This feature is new and experimental. Use it at your own risk! -### +# NOTE: Replace this with the username that you expect users to connect with. +VCSUSER="vcs-user" + +# NOTE: Replace this with the path to your Phabricator directory. +ROOT="/path/to/phabricator" + +if [ "$1" != "$VCSUSER" ]; +then + exit 1 +fi -ROOT=/INSECURE/devtools/phabricator exec "$ROOT/bin/ssh-auth" $@ diff --git a/resources/sshd/sshd_config.example b/resources/sshd/sshd_config.phabricator.example similarity index 58% rename from resources/sshd/sshd_config.example rename to resources/sshd/sshd_config.phabricator.example index da3897fc8e..2e28fa8458 100644 --- a/resources/sshd/sshd_config.example +++ b/resources/sshd/sshd_config.phabricator.example @@ -1,17 +1,15 @@ -### -### WARNING: This feature is new and experimental. Use it at your own risk! -### +# NOTE: You must have OpenSSHD 6.2 or newer; support for AuthorizedKeysCommand +# was added in this version. -# You must have OpenSSHD 6.2 or newer; support for AuthorizedKeysCommand was -# added in this version. +# NOTE: Edit these to the correct values for your setup. -Port 2222 AuthorizedKeysCommand /etc/phabricator-ssh-hook.sh -AuthorizedKeysCommandUser some-unprivileged-user +AuthorizedKeysCommandUser vcs-user # You may need to tweak these options, but mostly they just turn off everything # dangerous. +Port 22 Protocol 2 PermitRootLogin no AllowAgentForwarding no diff --git a/scripts/cache/manage_cache.php b/scripts/cache/manage_cache.php index 20c4bc1d84..61fc612b50 100755 --- a/scripts/cache/manage_cache.php +++ b/scripts/cache/manage_cache.php @@ -5,7 +5,7 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; $args = new PhutilArgumentParser($argv); -$args->setTagline('manage mail'); +$args->setTagline('manage cache'); $args->setSynopsis(<< "New Year's Day", - '2013-01-21' => "Birthday of Martin Luther King, Jr.", - '2013-02-18' => "Washington's Birthday", - '2013-05-27' => "Memorial Day", - '2013-07-04' => "Independence Day", - '2013-09-02' => "Labor Day", - '2013-10-14' => "Columbus Day", - '2013-11-11' => "Veterans Day", - '2013-11-28' => "Thanksgiving Day", - '2013-12-25' => "Christmas Day", '2014-01-01' => "New Year's Day", '2014-01-20' => "Birthday of Martin Luther King, Jr.", '2014-02-17' => "Washington's Birthday", @@ -46,6 +36,16 @@ $holidays = array( '2016-11-11' => "Veterans Day", '2016-11-24' => "Thanksgiving Day", '2016-12-26' => "Christmas Day", + '2017-01-02' => "New Year's Day", + '2017-01-16' => "Birthday of Martin Luther King, Jr.", + '2017-02-10' => "Washington's Birthday", + '2017-05-29' => "Memorial Day", + '2017-07-04' => "Independence Day", + '2017-09-04' => "Labor Day", + '2017-10-09' => "Columbus Day", + '2017-11-10' => "Veterans Day", + '2017-11-23' => "Thanksgiving Day", + '2017-12-25' => "Christmas Day", ); $table = new PhabricatorCalendarHoliday(); diff --git a/scripts/celerity/generate_sprites.php b/scripts/celerity/generate_sprites.php index 7c11133918..e0d3c72f16 100755 --- a/scripts/celerity/generate_sprites.php +++ b/scripts/celerity/generate_sprites.php @@ -38,6 +38,7 @@ $sheets = array( 'tokens' => $generator->buildTokenSheet(), 'docs' => $generator->buildDocsSheet(), 'gradient' => $generator->buildGradientSheet(), + 'main-header' => $generator->buildMainHeaderSheet(), 'login' => $generator->buildLoginSheet(), 'status' => $generator->buildStatusSheet(), 'projects' => $generator->buildProjectsSheet(), diff --git a/scripts/celerity/install_merge.sh b/scripts/celerity/install_merge.sh index d6c0e97866..84a627b5b2 100755 --- a/scripts/celerity/install_merge.sh +++ b/scripts/celerity/install_merge.sh @@ -1,9 +1,9 @@ #!/bin/sh -echo "src/__celerity_resource_map__.php merge=celerity" \ +echo "resources/celerity/map.php merge=celerity" \ >> `dirname "$0"`/../../.git/info/attributes git config merge.celerity.name "Celerity Mapper" git config merge.celerity.driver \ - 'php $GIT_DIR/../scripts/celerity_mapper.php $GIT_DIR/../webroot' + 'php $GIT_DIR/../bin/celerity map' diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php index 259da2369a..08be4b3ef1 100755 --- a/scripts/celerity_mapper.php +++ b/scripts/celerity_mapper.php @@ -1,415 +1,5 @@ #!/usr/bin/env php array( - 'javelin-util', - 'javelin-install', - 'javelin-event', - 'javelin-stratcom', - 'javelin-behavior', - 'javelin-resource', - 'javelin-request', - 'javelin-vector', - 'javelin-dom', - 'javelin-json', - 'javelin-uri', - 'javelin-workflow', - 'javelin-mask', - 'javelin-typeahead', - 'javelin-typeahead-normalizer', - 'javelin-typeahead-source', - 'javelin-typeahead-preloaded-source', - 'javelin-typeahead-ondemand-source', - 'javelin-tokenizer', - 'javelin-history', - ), - 'core.pkg.js' => array( - 'javelin-behavior-aphront-basic-tokenizer', - 'javelin-behavior-workflow', - 'javelin-behavior-aphront-form-disable-on-submit', - 'phabricator-keyboard-shortcut-manager', - 'phabricator-keyboard-shortcut', - 'javelin-behavior-phabricator-keyboard-shortcuts', - 'javelin-behavior-refresh-csrf', - 'javelin-behavior-phabricator-watch-anchor', - 'javelin-behavior-phabricator-autofocus', - 'phabricator-menu-item', - 'phabricator-dropdown-menu', - 'phabricator-phtize', - 'javelin-behavior-phabricator-oncopy', - 'phabricator-tooltip', - 'javelin-behavior-phabricator-tooltips', - 'phabricator-prefab', - 'javelin-behavior-device', - 'javelin-behavior-toggle-class', - 'javelin-behavior-lightbox-attachments', - 'phabricator-busy', - 'javelin-aphlict', - 'phabricator-notification', - 'javelin-behavior-aphlict-listen', - 'javelin-behavior-phabricator-search-typeahead', - 'javelin-behavior-konami', - 'javelin-behavior-aphlict-dropdown', - 'javelin-behavior-history-install', - 'javelin-behavior-phabricator-gesture', - - 'javelin-behavior-phabricator-active-nav', - 'javelin-behavior-phabricator-nav', - 'javelin-behavior-phabricator-remarkup-assist', - 'phabricator-textareautils', - 'phabricator-file-upload', - 'javelin-behavior-global-drag-and-drop', - 'javelin-behavior-phabricator-reveal-content', - 'phabricator-hovercard', - 'javelin-behavior-phabricator-hovercards', - 'javelin-color', - 'javelin-fx', - ), - 'core.pkg.css' => array( - 'phabricator-core-css', - 'phabricator-zindex-css', - 'phui-button-css', - 'phabricator-standard-page-view', - 'aphront-dialog-view-css', - 'phui-form-view-css', - 'aphront-panel-view-css', - 'aphront-table-view-css', - 'aphront-tokenizer-control-css', - 'aphront-typeahead-control-css', - 'aphront-list-filter-view-css', - - 'phabricator-jump-nav', - - 'phabricator-welcome-page', - - 'phabricator-remarkup-css', - 'syntax-highlighting-css', - 'aphront-pager-view-css', - 'phabricator-transaction-view-css', - 'aphront-tooltip-css', - 'phabricator-flag-css', - 'aphront-error-view-css', - - 'sprite-icons-css', - 'sprite-gradient-css', - 'sprite-menu-css', - 'sprite-apps-large-css', - 'sprite-status-css', - - 'phabricator-main-menu-view', - 'phabricator-notification-css', - 'phabricator-notification-menu-css', - 'lightbox-attachment-css', - 'phui-header-view-css', - 'phabricator-filetree-view-css', - 'phabricator-nav-view-css', - 'phabricator-side-menu-view-css', - 'phabricator-crumbs-view-css', - 'phui-object-item-list-view-css', - 'global-drag-and-drop-css', - 'phui-spacing-css', - 'phui-form-css', - 'phui-icon-view-css', - - 'phabricator-application-launch-view-css', - 'phabricator-action-list-view-css', - 'phui-property-list-view-css', - 'phabricator-tag-view-css', - 'phui-list-view-css', - ), - 'differential.pkg.css' => array( - 'differential-core-view-css', - 'differential-changeset-view-css', - 'differential-results-table-css', - 'differential-revision-history-css', - 'differential-revision-list-css', - 'differential-table-of-contents-css', - 'differential-revision-comment-css', - 'differential-revision-add-comment-css', - 'differential-revision-comment-list-css', - 'phabricator-object-selector-css', - 'phabricator-content-source-view-css', - 'differential-local-commits-view-css', - 'inline-comment-summary-css', - ), - 'differential.pkg.js' => array( - 'phabricator-drag-and-drop-file-upload', - 'phabricator-shaped-request', - - 'javelin-behavior-differential-feedback-preview', - 'javelin-behavior-differential-edit-inline-comments', - 'javelin-behavior-differential-populate', - 'javelin-behavior-differential-show-more', - 'javelin-behavior-differential-diff-radios', - 'javelin-behavior-differential-accept-with-errors', - 'javelin-behavior-differential-comment-jump', - 'javelin-behavior-differential-add-reviewers-and-ccs', - 'javelin-behavior-differential-keyboard-navigation', - 'javelin-behavior-aphront-drag-and-drop-textarea', - 'javelin-behavior-phabricator-object-selector', - 'javelin-behavior-repository-crossreference', - 'javelin-behavior-load-blame', - - 'differential-inline-comment-editor', - 'javelin-behavior-differential-dropdown-menus', - 'javelin-behavior-differential-toggle-files', - 'javelin-behavior-differential-user-select', - ), - 'diffusion.pkg.css' => array( - 'diffusion-commit-view-css', - 'diffusion-icons-css', - ), - 'diffusion.pkg.js' => array( - 'javelin-behavior-diffusion-pull-lastmodified', - 'javelin-behavior-diffusion-commit-graph', - 'javelin-behavior-audit-preview', - ), - 'maniphest.pkg.css' => array( - 'maniphest-task-summary-css', - 'phabricator-project-tag-css', - ), - 'maniphest.pkg.js' => array( - 'javelin-behavior-maniphest-batch-selector', - 'javelin-behavior-maniphest-transaction-controls', - 'javelin-behavior-maniphest-transaction-preview', - 'javelin-behavior-maniphest-transaction-expand', - 'javelin-behavior-maniphest-subpriority-editor', - ), - 'darkconsole.pkg.js' => array( - 'javelin-behavior-dark-console', - 'javelin-behavior-error-log', - ), -); - - -require_once dirname(__FILE__).'/__init_script__.php'; - -$args = new PhutilArgumentParser($argv); -$args->setTagline('map static resources'); -$args->setSynopsis( - "**celerity_mapper.php** [--output __path__] [--with-custom] "); -$args->parse( - array( - array( - 'name' => 'output', - 'param' => 'path', - 'default' => '../src/__celerity_resource_map__.php', - 'help' => "Set the path for resource map. It is usually useful for ". - "'celerity.resource-path' configuration.", - ), - array( - 'name' => 'with-custom', - 'help' => 'Include resources in /rsrc/custom/.', - ), - array( - 'name' => 'webroot', - 'wildcard' => true, - ), - )); - -$root = $args->getArg('webroot'); -if (count($root) != 1 || !is_dir(reset($root))) { - $args->printHelpAndExit(); -} -$root = Filesystem::resolvePath(reset($root)); - -$celerity_path = Filesystem::resolvePath($args->getArg('output'), $root); -$with_custom = $args->getArg('with-custom'); - -$resource_hash = PhabricatorEnv::getEnvConfig('celerity.resource-hash'); -$runtime_map = array(); - -echo "Finding raw static resources...\n"; -$finder = id(new FileFinder($root)) - ->withType('f') - ->withSuffix('png') - ->withSuffix('jpg') - ->withSuffix('gif') - ->withSuffix('swf') - ->withFollowSymlinks(true) - ->setGenerateChecksums(true); -if (!$with_custom) { - $finder->excludePath('./rsrc/custom'); -} -$raw_files = $finder->find(); - -echo "Processing ".count($raw_files)." files"; -foreach ($raw_files as $path => $hash) { - echo "."; - $path = '/'.Filesystem::readablePath($path, $root); - $type = CelerityResourceTransformer::getResourceType($path); - - $hash = md5($hash.$path.$resource_hash); - $uri = '/res/'.substr($hash, 0, 8).$path; - - $runtime_map[$path] = array( - 'hash' => $hash, - 'uri' => $uri, - 'disk' => $path, - 'type' => $type, - ); -} -echo "\n"; - -$xformer = id(new CelerityResourceTransformer()) - ->setMinify(false) - ->setRawResourceMap($runtime_map); - -echo "Finding transformable static resources...\n"; -$finder = id(new FileFinder($root)) - ->withType('f') - ->withSuffix('js') - ->withSuffix('css') - ->withFollowSymlinks(true) - ->setGenerateChecksums(true); -if (!$with_custom) { - $finder->excludePath('./rsrc/custom'); -} -$files = $finder->find(); - -echo "Processing ".count($files)." files"; - -$file_map = array(); -foreach ($files as $path => $raw_hash) { - echo "."; - $path = '/'.Filesystem::readablePath($path, $root); - $data = Filesystem::readFile($root.$path); - - $data = $xformer->transformResource($path, $data); - $hash = md5($data); - $hash = md5($hash.$path.$resource_hash); - - $file_map[$path] = array( - 'hash' => $hash, - 'disk' => $path, - ); -} -echo "\n"; - -$resource_graph = array(); -$hash_map = array(); - -$parser = new PhutilDocblockParser(); -foreach ($file_map as $path => $info) { - $type = CelerityResourceTransformer::getResourceType($path); - - $data = Filesystem::readFile($root.$info['disk']); - $matches = array(); - $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); - if (!$ok) { - throw new Exception( - "File {$path} does not have a header doc comment. Encode dependency ". - "data in a header docblock."); - } - - list($description, $metadata) = $parser->parse($matches[0]); - - $provides = preg_split('/\s+/', trim(idx($metadata, 'provides'))); - $requires = preg_split('/\s+/', trim(idx($metadata, 'requires'))); - $provides = array_filter($provides); - $requires = array_filter($requires); - - if (!$provides) { - // Tests and documentation-only JS is permitted to @provide no targets. - continue; - } - - if (count($provides) > 1) { - throw new Exception( - "File {$path} must @provide at most one Celerity target."); - } - - $provides = reset($provides); - - $uri = '/res/'.substr($info['hash'], 0, 8).$path; - - $hash_map[$provides] = $info['hash']; - - $resource_graph[$provides] = $requires; - - $runtime_map[$provides] = array( - 'uri' => $uri, - 'type' => $type, - 'requires' => $requires, - 'disk' => $path, - ); -} - -$celerity_resource_graph = new CelerityResourceGraph(); -$celerity_resource_graph->addNodes($resource_graph); -$celerity_resource_graph->setResourceGraph($resource_graph); -$celerity_resource_graph->loadGraph(); - -foreach ($resource_graph as $provides => $requires) { - $cycle = $celerity_resource_graph->detectCycles($provides); - if ($cycle) { - throw new Exception( - "Cycle detected in resource graph: ". implode($cycle, " => ") - ); - } -} - -$package_map = array(); -foreach ($package_spec as $name => $package) { - $hashes = array(); - $type = null; - foreach ($package as $symbol) { - if (empty($hash_map[$symbol])) { - throw new Exception( - "Package specification for '{$name}' includes '{$symbol}', but that ". - "symbol is not defined anywhere."); - } - if ($type === null) { - $type = $runtime_map[$symbol]['type']; - } else { - $ntype = $runtime_map[$symbol]['type']; - if ($type !== $ntype) { - throw new Exception( - "Package specification for '{$name}' mixes resources of type ". - "'{$type}' with resources of type '{$ntype}'. Each package may only ". - "contain one type of resource."); - } - } - $hashes[] = $symbol.':'.$hash_map[$symbol]; - } - $key = substr(md5(implode("\n", $hashes)), 0, 8); - $package_map['packages'][$key] = array( - 'name' => $name, - 'symbols' => $package, - 'uri' => '/res/pkg/'.$key.'/'.$name, - 'type' => $type, - ); - foreach ($package as $symbol) { - $package_map['reverse'][$symbol] = $key; - } -} - -ksort($runtime_map); -$runtime_map = var_export($runtime_map, true); -$runtime_map = preg_replace('/\s+$/m', '', $runtime_map); -$runtime_map = preg_replace('/array \(/', 'array(', $runtime_map); - -$package_map['packages'] = isort($package_map['packages'], 'name'); -ksort($package_map['reverse']); -$package_map = var_export($package_map, true); -$package_map = preg_replace('/\s+$/m', '', $package_map); -$package_map = preg_replace('/array \(/', 'array(', $package_map); - -$generated = '@'.'generated'; -$resource_map = <<setTagline('manage daemons'); $args->setSynopsis(<<parseStandardArguments(); -$args->parseWorkflows( - array( - new DivinerGenerateWorkflow(), - new DivinerAtomizeWorkflow(), - new PhutilHelpArgumentWorkflow(), - )); +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('DivinerWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/scripts/drydock/drydock_control.php b/scripts/drydock/drydock_control.php index efa9496b7c..0a01b91fa3 100755 --- a/scripts/drydock/drydock_control.php +++ b/scripts/drydock/drydock_control.php @@ -14,12 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new DrydockManagementWaitForLeaseWorkflow(), - new DrydockManagementLeaseWorkflow(), - new DrydockManagementCloseWorkflow(), - new DrydockManagementReleaseWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('DrydockManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/fact/manage_facts.php b/scripts/fact/manage_facts.php index 13e3598651..973e5bfa21 100755 --- a/scripts/fact/manage_facts.php +++ b/scripts/fact/manage_facts.php @@ -15,13 +15,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorFactManagementDestroyWorkflow(), - new PhabricatorFactManagementAnalyzeWorkflow(), - new PhabricatorFactManagementStatusWorkflow(), - new PhabricatorFactManagementListWorkflow(), - new PhabricatorFactManagementCursorsWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorFactManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/files/manage_files.php b/scripts/files/manage_files.php index c1f276999c..661d65ea3f 100755 --- a/scripts/files/manage_files.php +++ b/scripts/files/manage_files.php @@ -14,12 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorFilesManagementEnginesWorkflow(), - new PhabricatorFilesManagementMigrateWorkflow(), - new PhabricatorFilesManagementRebuildWorkflow(), - new PhabricatorFilesManagementPurgeWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorFilesManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/install/install_ubuntu.sh b/scripts/install/install_ubuntu.sh index 6f33b6b853..c9ec45b602 100755 --- a/scripts/install/install_ubuntu.sh +++ b/scripts/install/install_ubuntu.sh @@ -43,7 +43,9 @@ echo set +x sudo apt-get -qq update -sudo apt-get install $GIT mysql-server apache2 php5 php5-mysql php5-gd php5-dev php5-curl php-apc php5-cli dpkg-dev +sudo apt-get install \ + $GIT mysql-server apache2 dpkg-dev \ + php5 php5-mysql php5-gd php5-dev php5-curl php-apc php5-cli php5-json # Enable mod_rewrite sudo a2enmod rewrite diff --git a/scripts/install/update_phabricator.sh b/scripts/install/update_phabricator.sh index c89dac967b..5aad808087 100755 --- a/scripts/install/update_phabricator.sh +++ b/scripts/install/update_phabricator.sh @@ -33,13 +33,17 @@ $ROOT/phabricator/bin/phd stop # Stop the webserver (apache, nginx, lighttpd, etc). This command will differ # depending on which system and webserver you are running: replace it with an # appropriate command for your system. +# NOTE: If you're running php-fpm, you should stop it here too. + sudo /etc/init.d/httpd stop + # Upgrade the database schema. You may want to add the "--force" flag to allow # this script to run noninteractively. $ROOT/phabricator/bin/storage upgrade # Restart the webserver. As above, this depends on your system and webserver. +# NOTE: If you're running php-fpm, restart it here too. sudo /etc/init.d/httpd start # Restart daemons. diff --git a/scripts/lipsum/manage_lipsum.php b/scripts/lipsum/manage_lipsum.php index e7092f8797..3a7c0a23a8 100755 --- a/scripts/lipsum/manage_lipsum.php +++ b/scripts/lipsum/manage_lipsum.php @@ -14,9 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorLipsumGenerateWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorLipsumManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php index 19e315364d..11d8f1c29f 100755 --- a/scripts/mail/mail_handler.php +++ b/scripts/mail/mail_handler.php @@ -1,14 +1,37 @@ #!/usr/bin/env php 1) { - $_SERVER['PHABRICATOR_ENV'] = $argv[1]; + foreach (array_slice($argv, 1) as $arg) { + if (!preg_match('/^-/', $arg)) { + $_SERVER['PHABRICATOR_ENV'] = $arg; + break; + } + } } $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; require_once $root.'/externals/mimemailparser/MimeMailParser.class.php'; +$args = new PhutilArgumentParser($argv); +$args->parseStandardArguments(); +$args->parse( + array( + array( + 'name' => 'process-duplicates', + 'help' => pht( + "Process this message, even if it's a duplicate of another message. ". + "This is mostly useful when debugging issues with mail routing."), + ), + array( + 'name' => 'env', + 'wildcard' => true, + ), + )); + $parser = new MimeMailParser(); $parser->setText(file_get_contents('php://stdin')); @@ -28,6 +51,10 @@ $headers = $parser->getHeaders(); $headers['subject'] = iconv_mime_decode($headers['subject'], 0, "UTF-8"); $headers['from'] = iconv_mime_decode($headers['from'], 0, "UTF-8"); +if ($args->getArg('process-duplicates')) { + $headers['message-id'] = Filesystem::readRandomCharacters(64); +} + $received = new PhabricatorMetaMTAReceivedMail(); $received->setHeaders($headers); $received->setBodies(array( @@ -62,6 +89,8 @@ try { $received ->setMessage('EXCEPTION: '.$e->getMessage()) ->save(); + + throw $e; } diff --git a/scripts/mail/manage_mail.php b/scripts/mail/manage_mail.php index 66ecaea18e..fefb1e0da3 100755 --- a/scripts/mail/manage_mail.php +++ b/scripts/mail/manage_mail.php @@ -14,15 +14,9 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhutilHelpArgumentWorkflow(), - new PhabricatorMailManagementResendWorkflow(), - new PhabricatorMailManagementShowOutboundWorkflow(), - new PhabricatorMailManagementShowInboundWorkflow(), - new PhabricatorMailManagementSendTestWorkflow(), - new PhabricatorMailManagementReceiveTestWorkflow(), - new PhabricatorMailManagementListInboundWorkflow(), - new PhabricatorMailManagementListOutboundWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorMailManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); + diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php new file mode 100755 index 0000000000..2c7fe4289b --- /dev/null +++ b/scripts/repository/commit_hook.php @@ -0,0 +1,142 @@ +#!/usr/bin/env php +')); +} + +$engine = new DiffusionCommitHookEngine(); + +$repository = id(new PhabricatorRepositoryQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withCallsigns(array($argv[1])) + ->needProjectPHIDs(true) + ->executeOne(); + +if (!$repository) { + throw new Exception(pht('No such repository "%s"!', $argv[1])); +} + +if (!$repository->isHosted()) { + // This should be redundant, but double check just in case. + throw new Exception(pht('Repository "%s" is not hosted!', $argv[1])); +} + +$engine->setRepository($repository); + + +// Figure out which user is writing the commit. + +if ($repository->isGit() || $repository->isHg()) { + $username = getenv(DiffusionCommitHookEngine::ENV_USER); + if (!strlen($username)) { + throw new Exception( + pht('usage: %s should be defined!', DiffusionCommitHookEngine::ENV_USER)); + } + + if ($repository->isHg()) { + // We respond to several different hooks in Mercurial. + $engine->setMercurialHook($argv[2]); + } + +} else if ($repository->isSVN()) { + // NOTE: In Subversion, the entire environment gets wiped so we can't read + // DiffusionCommitHookEngine::ENV_USER. Instead, we've set "--tunnel-user" to + // specify the correct user; read this user out of the commit log. + + if ($argc < 4) { + throw new Exception(pht('usage: commit-hook ')); + } + + $svn_repo = $argv[2]; + $svn_txn = $argv[3]; + list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo); + $username = rtrim($username, "\n"); + + $engine->setSubversionTransactionInfo($svn_txn, $svn_repo); +} else { + throw new Exception(pht('Unknown repository type.')); +} + +$user = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUsernames(array($username)) + ->executeOne(); + +if (!$user) { + throw new Exception(pht('No such user "%s"!', $username)); +} + +$engine->setViewer($user); + + +// Read stdin for the hook engine. + +if ($repository->isHg()) { + // Mercurial leaves stdin open, so we can't just read it until EOF. + $stdin = ''; +} else { + // Git and Subversion write data into stdin and then close it. Read the + // data. + $stdin = @file_get_contents('php://stdin'); + if ($stdin === false) { + throw new Exception(pht('Failed to read stdin!')); + } +} + +$engine->setStdin($stdin); +$engine->setOriginalArgv(array_slice($argv, 2)); + +$remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS); +if (strlen($remote_address)) { + $engine->setRemoteAddress($remote_address); +} + +$remote_protocol = getenv(DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL); +if (strlen($remote_protocol)) { + $engine->setRemoteProtocol($remote_protocol); +} + +try { + $err = $engine->execute(); +} catch (DiffusionCommitHookRejectException $ex) { + $console = PhutilConsole::getConsole(); + + if (PhabricatorEnv::getEnvConfig('phabricator.serious-business')) { + $preamble = pht('*** PUSH REJECTED BY COMMIT HOOK ***'); + } else { + $preamble = pht(<<writeErr("%s\n\n", $preamble); + $console->writeErr("%s\n\n", $ex->getMessage()); + $err = 1; +} + +exit($err); diff --git a/scripts/repository/manage_repositories.php b/scripts/repository/manage_repositories.php index 359186f9dd..b3ebac0842 100755 --- a/scripts/repository/manage_repositories.php +++ b/scripts/repository/manage_repositories.php @@ -15,14 +15,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorRepositoryManagementPullWorkflow(), - new PhabricatorRepositoryManagementDiscoverWorkflow(), - new PhabricatorRepositoryManagementListWorkflow(), - new PhabricatorRepositoryManagementDeleteWorkflow(), - new PhabricatorRepositoryManagementMarkImportedWorkflow(), - new PhabricatorRepositoryManagementImportingWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorRepositoryManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/repository/test_connection.php b/scripts/repository/test_connection.php index 20c8486d3a..6047f3429d 100755 --- a/scripts/repository/test_connection.php +++ b/scripts/repository/test_connection.php @@ -1,95 +1,6 @@ #!/usr/bin/env php \n"; - exit(1); -} - -echo phutil_console_wrap( - phutil_console_format( - 'This script will test that you have configured valid credentials for '. - 'access to a repository, so the Phabricator daemons can pull from it. '. - 'You should run this as the **same user you will run the daemons as**, '. - 'from the **same machine they will run from**. Doing this will help '. - 'detect various problems with your configuration, such as SSH issues.')); - -list($whoami) = execx('whoami'); -$whoami = trim($whoami); - -$ok = phutil_console_confirm("Do you want to continue as '{$whoami}'?"); -if (!$ok) { - die(1); -} - -$callsign = $argv[1]; -echo "Loading '{$callsign}' repository...\n"; -$repository = id(new PhabricatorRepository())->loadOneWhere( - 'callsign = %s', - $argv[1]); -if (!$repository) { - throw new Exception("No such repository exists!"); -} - -$vcs = $repository->getVersionControlSystem(); - -PhutilServiceProfiler::installEchoListener(); - -echo phutil_console_format( - "\n". - "**NOTE:** If you are prompted for an SSH password in the next step, the\n". - "daemon won't work because it doesn't have the password and can't respond\n". - "to an interactive prompt. Instead of typing the password, it will hang\n". - "forever when prompted. There are several ways to resolve this:\n\n". - " - Run the daemon inside an ssh-agent session where you have unlocked\n". - " the key (most secure, but most complicated).\n". - " - Generate a new, passwordless certificate for the daemon to use\n". - " (usually quite easy).\n". - " - Remove the passphrase from the key with `ssh-keygen -p`\n". - " (easy, but questionable)."); - -phutil_console_confirm('Did you read all that?', $default_no = false); - -echo "Trying to connect to the remote...\n"; -switch ($vcs) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $err = $repository->passthruRemoteCommand( - '--limit 1 log %s', - $repository->getRemoteURI()); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - // Do an ls-remote on a nonexistent ref, which we expect to just return - // nothing. - $err = $repository->passthruRemoteCommand( - 'ls-remote %s %s', - $repository->getRemoteURI(), - 'just-testing'); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: - // TODO: 'hg id' doesn't support --insecure so we can't tell it not to - // spew. If 'hg id' eventually supports --insecure, consider using it. - echo "(It is safe to ignore any 'certificate with fingerprint ... not ". - "verified' warnings, although you may want to configure Mercurial ". - "to recognize the server's fingerprint/certificate.)\n"; - $err = $repository->passthruRemoteCommand( - 'id --rev tip %s', - $repository->getRemoteURI()); - break; - default: - throw new Exception("Unsupported repository type."); -} - -if ($err) { - echo phutil_console_format( - "** FAIL ** Connection failed. The credentials for this ". - "repository appear to be incorrectly configured.\n"); - exit(1); -} else { - echo phutil_console_format( - "** OKAY ** Connection successful. The credentials for ". - "this repository appear to be correctly configured.\n"); -} +echo "This script is obsolete. Use `bin/repository` to manage repositories.\n"; +exit(1); diff --git a/scripts/search/manage_search.php b/scripts/search/manage_search.php index 25c391b6c3..b07ff54bad 100755 --- a/scripts/search/manage_search.php +++ b/scripts/search/manage_search.php @@ -14,9 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorSearchManagementIndexWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorSearchManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_audit.php b/scripts/setup/manage_audit.php index 8502db2674..9a085c6947 100755 --- a/scripts/setup/manage_audit.php +++ b/scripts/setup/manage_audit.php @@ -14,9 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorAuditManagementDeleteWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorAuditManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_auth.php b/scripts/setup/manage_auth.php index f4c87e95d8..a674c92b01 100755 --- a/scripts/setup/manage_auth.php +++ b/scripts/setup/manage_auth.php @@ -14,11 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorAuthManagementRecoverWorkflow(), - new PhabricatorAuthManagementRefreshWorkflow(), - new PhabricatorAuthManagementLDAPWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorAuthManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_celerity.php b/scripts/setup/manage_celerity.php new file mode 100755 index 0000000000..4a559711c2 --- /dev/null +++ b/scripts/setup/manage_celerity.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline('manage celerity'); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('CelerityManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_config.php b/scripts/setup/manage_config.php index 1610ebd257..5c07e7293f 100755 --- a/scripts/setup/manage_config.php +++ b/scripts/setup/manage_config.php @@ -14,12 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorConfigManagementListWorkflow(), - new PhabricatorConfigManagementSetWorkflow(), - new PhabricatorConfigManagementGetWorkflow(), - new PhabricatorConfigManagementDeleteWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorConfigManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_feed.php b/scripts/setup/manage_feed.php index 46547f4dc0..81d2cb502b 100755 --- a/scripts/setup/manage_feed.php +++ b/scripts/setup/manage_feed.php @@ -14,9 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorFeedManagementRepublishWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorFeedManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_harbormaster.php b/scripts/setup/manage_harbormaster.php new file mode 100755 index 0000000000..1903bdd907 --- /dev/null +++ b/scripts/setup/manage_harbormaster.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline('manage Harbormaster'); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('HarbormasterManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/scripts/setup/manage_policy.php b/scripts/setup/manage_policy.php index bb80f0876b..a9ab27731a 100755 --- a/scripts/setup/manage_policy.php +++ b/scripts/setup/manage_policy.php @@ -14,10 +14,8 @@ EOSYNOPSIS ); $args->parseStandardArguments(); -$workflows = array( - new PhabricatorPolicyManagementShowWorkflow(), - new PhabricatorPolicyManagementUnlockWorkflow(), - new PhutilHelpArgumentWorkflow(), -); - +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorPolicyManagementWorkflow') + ->loadObjects(); +$workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php index 14ec913623..a94ee75f3c 100755 --- a/scripts/sql/manage_storage.php +++ b/scripts/sql/manage_storage.php @@ -99,14 +99,9 @@ try { exit(1); } -$workflows = array( - new PhabricatorStorageManagementDatabasesWorkflow(), - new PhabricatorStorageManagementDestroyWorkflow(), - new PhabricatorStorageManagementDumpWorkflow(), - new PhabricatorStorageManagementStatusWorkflow(), - new PhabricatorStorageManagementProbeWorkflow(), - new PhabricatorStorageManagementUpgradeWorkflow(), -); +$workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorStorageManagementWorkflow') + ->loadObjects(); $patches = PhabricatorSQLPatchList::buildAllPatches(); diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php index dc7b4d098e..aba2b20f01 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth.php @@ -15,6 +15,11 @@ $rows = queryfx_all( $user_dao->getTableName(), $ssh_dao->getTableName()); +if (!$rows) { + echo pht("No keys found.")."\n"; + exit(1); +} + $bin = $root.'/bin/ssh-exec'; foreach ($rows as $row) { $user = $row['userName']; diff --git a/scripts/ssh/ssh-connect.php b/scripts/ssh/ssh-connect.php new file mode 100755 index 0000000000..8a142090c3 --- /dev/null +++ b/scripts/ssh/ssh-connect.php @@ -0,0 +1,68 @@ +#!/usr/bin/env php +parsePartial( + array( + array( + 'name' => 'port', + 'short' => 'p', + 'param' => pht('port'), + 'help' => pht('Port number to connect to.'), + ), + )); +$unconsumed_argv = $args->getUnconsumedArgumentVector(); + +$pattern = array(); +$arguments = array(); + +$pattern[] = 'ssh'; + +$pattern[] = '-o'; +$pattern[] = 'StrictHostKeyChecking=no'; + +// This prevents "known host" failures, and covers for issues where HOME is set +// to something unusual. +$pattern[] = '-o'; +$pattern[] = 'UserKnownHostsFile=/dev/null'; + +$credential_phid = getenv('PHABRICATOR_CREDENTIAL'); +if ($credential_phid) { + $viewer = PhabricatorUser::getOmnipotentUser(); + $key = PassphraseSSHKey::loadFromPHID($credential_phid, $viewer); + + $pattern[] = '-l %P'; + $arguments[] = $key->getUsernameEnvelope(); + $pattern[] = '-i %P'; + $arguments[] = $key->getKeyfileEnvelope(); +} + +$port = $args->getArg('port'); +if ($port) { + $pattern[] = '-p %d'; + $arguments[] = $port; +} + +$pattern[] = '--'; + +$passthru_args = $unconsumed_argv; +foreach ($passthru_args as $passthru_arg) { + $pattern[] = '%s'; + $arguments[] = $passthru_arg; +} + +$pattern = implode(' ', $pattern); +array_unshift($arguments, $pattern); + +$err = newv('PhutilExecPassthru', $arguments) + ->execute(); + +exit($err); diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index cdc62d7a8d..6dba54b1ea 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -1,9 +1,13 @@ #!/usr/bin/env php setTagline('receive SSH requests'); @@ -38,8 +42,14 @@ try { throw new Exception("Invalid username."); } - if ($user->getIsDisabled()) { - throw new Exception("You have been exiled."); + $ssh_log->setData( + array( + 'u' => $user->getUsername(), + 'P' => $user->getPHID(), + )); + + if (!$user->isUserActivated()) { + throw new Exception(pht("Your account is not activated.")); } if ($args->getArg('ssh-command')) { @@ -48,25 +58,43 @@ try { $original_command = getenv('SSH_ORIGINAL_COMMAND'); } + $workflows = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorSSHWorkflow') + ->loadObjects(); + + $workflow_names = mpull($workflows, 'getName', 'getName'); + // Now, rebuild the original command. $original_argv = id(new PhutilShellLexer()) ->splitArguments($original_command); if (!$original_argv) { - throw new Exception("No interactive logins."); + throw new Exception( + pht( + "Welcome to Phabricator.\n\n". + "You are logged in as %s.\n\n". + "You haven't specified a command to run. This means you're requesting ". + "an interactive shell, but Phabricator does not provide an ". + "interactive shell over SSH.\n\n". + "Usually, you should run a command like `git clone` or `hg push` ". + "rather than connecting directly with SSH.\n\n". + "Supported commands are: %s.", + $user->getUsername(), + implode(', ', $workflow_names))); } + + $ssh_log->setData( + array( + 'C' => $original_argv[0], + 'U' => phutil_utf8_shorten( + implode(' ', array_slice($original_argv, 1)), + 128), + )); + $command = head($original_argv); array_unshift($original_argv, 'phabricator-ssh-exec'); $original_args = new PhutilArgumentParser($original_argv); - $workflows = array( - new ConduitSSHWorkflow(), - - new DiffusionSSHGitUploadPackWorkflow(), - new DiffusionSSHGitReceivePackWorkflow(), - ); - - $workflow_names = mpull($workflows, 'getName', 'getName'); if (empty($workflow_names[$command])) { throw new Exception("Invalid command."); } @@ -97,11 +125,35 @@ try { $workflow->setIOChannel($metrics_channel); $workflow->setErrorChannel($error_channel); - $err = $workflow->execute($original_args); + $rethrow = null; + try { + $err = $workflow->execute($original_args); - $metrics_channel->flush(); - $error_channel->flush(); + $metrics_channel->flush(); + $error_channel->flush(); + } catch (Exception $ex) { + $rethrow = $ex; + } + + // Always write this if we got as far as building a metrics channel. + $ssh_log->setData( + array( + 'i' => $metrics_channel->getBytesRead(), + 'o' => $metrics_channel->getBytesWritten(), + )); + + if ($rethrow) { + throw $rethrow; + } } catch (Exception $ex) { fwrite(STDERR, "phabricator-ssh-exec: ".$ex->getMessage()."\n"); - exit(1); + $err = 1; } + +$ssh_log->setData( + array( + 'c' => $err, + 'T' => (int)(1000000 * (microtime(true) - $ssh_start_time)), + )); + +exit($err); diff --git a/scripts/symbols/generate_ctags_symbols.php b/scripts/symbols/generate_ctags_symbols.php index 15bc14b481..9a8ee30604 100755 --- a/scripts/symbols/generate_ctags_symbols.php +++ b/scripts/symbols/generate_ctags_symbols.php @@ -71,7 +71,7 @@ foreach (Futures($futures)->limit(8) as $file => $future) { // also, "normalize" c++ and c# $language = str_ireplace("c++", "cpp", $language); - $language = str_ireplace("c#", "csharp", $language); + $language = str_ireplace("c#", "cs", $language); // Ruby has "singleton method", for example $type = substr(str_replace(' ', '_', $type), 0, 12); diff --git a/scripts/symbols/generate_php_symbols.php b/scripts/symbols/generate_php_symbols.php index 65eecbb199..f8888d1efa 100755 --- a/scripts/symbols/generate_php_symbols.php +++ b/scripts/symbols/generate_php_symbols.php @@ -34,6 +34,10 @@ foreach (Futures($futures)->limit(8) as $file => $future) { $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name = $function->getChildByIndex(2); + // Skip anonymous functions + if (!$name->getConcreteString()) { + continue; + } print_symbol($file, 'function', $name); } diff --git a/scripts/symbols/import_project_symbols.php b/scripts/symbols/import_project_symbols.php index 6271dc7f75..2f970182c2 100755 --- a/scripts/symbols/import_project_symbols.php +++ b/scripts/symbols/import_project_symbols.php @@ -1,6 +1,7 @@ #!/usr/bin/env php parse( 'help' => 'If a line can\'t be parsed, ignore that line and '. 'continue instead of exiting.', ), + array( + 'name' => 'max-transaction', + 'param' => 'num-syms', + 'default' => '100000', + 'help' => 'Maximum number of symbols that should '. + 'be part of a single transaction', + ), array( 'name' => 'more', 'wildcard' => true, @@ -53,6 +61,52 @@ $input = file_get_contents('php://stdin'); $input = trim($input); $input = explode("\n", $input); + +function commit_symbols ($syms, $project, $no_purge) { + echo "Looking up path IDs...\n"; + $path_map = + PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( + ipull($syms, 'path')); + + $symbol = new PhabricatorRepositorySymbol(); + $conn_w = $symbol->establishConnection('w'); + + echo "Preparing queries...\n"; + $sql = array(); + foreach ($syms as $dict) { + $sql[] = qsprintf( + $conn_w, + '(%d, %s, %s, %s, %s, %d, %d)', + $project->getID(), + $dict['ctxt'], + $dict['name'], + $dict['type'], + $dict['lang'], + $dict['line'], + $path_map[$dict['path']]); + } + + if (!$no_purge) { + echo "Purging old syms...\n"; + queryfx($conn_w, + 'DELETE FROM %T WHERE arcanistProjectID = %d', + $symbol->getTableName(), + $project->getID()); + } + + echo "Loading ".number_format(count($sql))." syms...\n"; + foreach (array_chunk($sql, 128) as $chunk) { + queryfx($conn_w, + 'INSERT INTO %T + (arcanistProjectID, symbolContext, symbolName, symbolType, + symbolLanguage, lineNumber, pathID) VALUES %Q', + $symbol->getTableName(), + implode(', ', $chunk)); + } + +} + +$no_purge = $args->getArg('no-purge'); $symbols = array(); foreach ($input as $key => $line) { try { @@ -129,48 +183,26 @@ foreach ($input as $key => $line) { throw $e; } } + + if (count ($symbols) >= $args->getArg('max-transaction')) { + try { + echo "Committing {$args->getArg('max-transaction')} symbols....\n"; + commit_symbols($symbols, $project, $no_purge); + $no_purge = true; + unset($symbols); + $symbols = array(); + } catch (Exception $e) { + if ($args->getArg('ignore-errors')) { + continue; + } else { + throw $e; + } + } + } } -echo "Looking up path IDs...\n"; -$path_map = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( - ipull($symbols, 'path')); - -$symbol = new PhabricatorRepositorySymbol(); -$conn_w = $symbol->establishConnection('w'); - -echo "Preparing queries...\n"; -$sql = array(); -foreach ($symbols as $dict) { - $sql[] = qsprintf( - $conn_w, - '(%d, %s, %s, %s, %s, %d, %d)', - $project->getID(), - $dict['ctxt'], - $dict['name'], - $dict['type'], - $dict['lang'], - $dict['line'], - $path_map[$dict['path']]); -} - -if (!$args->getArg('no-purge')) { - echo "Purging old symbols...\n"; - queryfx( - $conn_w, - 'DELETE FROM %T WHERE arcanistProjectID = %d', - $symbol->getTableName(), - $project->getID()); -} - -echo "Loading ".number_format(count($sql))." symbols...\n"; -foreach (array_chunk($sql, 128) as $chunk) { - queryfx( - $conn_w, - 'INSERT INTO %T - (arcanistProjectID, symbolContext, symbolName, symbolType, - symbolLanguage, lineNumber, pathID) VALUES %Q', - $symbol->getTableName(), - implode(', ', $chunk)); +if (count($symbols)) { + commit_symbols($symbols, $project, $no_purge); } echo "Done.\n"; diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index d966240f15..bb0b060940 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -4,6 +4,29 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; +$table = new PhabricatorUser(); +$any_user = queryfx_one( + $table->establishConnection('r'), + 'SELECT * FROM %T LIMIT 1', + $table->getTableName()); +$is_first_user = (!$any_user); + +if ($is_first_user) { + echo pht( + "WARNING\n\n". + "You're about to create the first account on this install. Normally, you ". + "should use the web interface to create the first account, not this ". + "script.\n\n". + "If you use the web interface, it will drop you into a nice UI workflow ". + "which gives you more help setting up your install. If you create an ". + "account with this script instead, you will skip the setup help and you ". + "will not be able to access it later."); + if (!phutil_console_confirm(pht("Skip easy setup and create account?"))) { + echo pht("Cancelled.")."\n"; + exit(1); + } +} + echo "Enter a username to create a new account or edit an existing account."; $username = phutil_console_prompt("Enter a username:"); @@ -178,9 +201,13 @@ $user->openTransaction(); ->setAddress($create_email) ->setIsVerified(1); + // Unconditionally approve new accounts created from the CLI. + $user->setIsApproved(1); + $editor->createNewUser($user, $email); } else { if ($verify_email) { + $user->setIsEmailVerified(1); $verify_email->setIsVerified($set_verified ? 1 : 0); } $editor->updateUser($user, $verify_email); diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php index edb6a1a26b..958ff4b893 100755 --- a/scripts/user/add_user.php +++ b/scripts/user/add_user.php @@ -42,6 +42,7 @@ if ($existing_email) { $user = new PhabricatorUser(); $user->setUsername($username); $user->setRealname($realname); +$user->setIsApproved(1); $email_object = id(new PhabricatorUserEmail()) ->setAddress($email) diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php deleted file mode 100644 index 352bf114f8..0000000000 --- a/src/__celerity_resource_map__.php +++ /dev/null @@ -1,4739 +0,0 @@ - - array( - 'hash' => '098abd520f2ba8d1370299e7e4e01758', - 'uri' => '/res/098abd52/rsrc/custom/image/blender_logo.png', - 'disk' => '/rsrc/custom/image/blender_logo.png', - 'type' => 'png', - ), - '/rsrc/image/actions/edit.png' => - array( - 'hash' => 'ae90914d120ac3838ddc633b480343f3', - 'uri' => '/res/ae90914d/rsrc/image/actions/edit.png', - 'disk' => '/rsrc/image/actions/edit.png', - 'type' => 'png', - ), - '/rsrc/image/apple-touch-icon.png' => - array( - 'hash' => '3380adf2dd4a5efa0885618bc5943640', - 'uri' => '/res/3380adf2/rsrc/image/apple-touch-icon.png', - 'disk' => '/rsrc/image/apple-touch-icon.png', - 'type' => 'png', - ), - '/rsrc/image/avatar.png' => - array( - 'hash' => '1c5f255071537f05406adee86717ff27', - 'uri' => '/res/1c5f2550/rsrc/image/avatar.png', - 'disk' => '/rsrc/image/avatar.png', - 'type' => 'png', - ), - '/rsrc/image/checker_dark.png' => - array( - 'hash' => '640f795343df76ebe5409aae6187e57f', - 'uri' => '/res/640f7953/rsrc/image/checker_dark.png', - 'disk' => '/rsrc/image/checker_dark.png', - 'type' => 'png', - ), - '/rsrc/image/checker_light.png' => - array( - 'hash' => '7f8f3ef8beb0f2cc4cc69efb9e1c3308', - 'uri' => '/res/7f8f3ef8/rsrc/image/checker_light.png', - 'disk' => '/rsrc/image/checker_light.png', - 'type' => 'png', - ), - '/rsrc/image/credit_cards.png' => - array( - 'hash' => '681448de424ea159b6ea68af04c046ae', - 'uri' => '/res/681448de/rsrc/image/credit_cards.png', - 'disk' => '/rsrc/image/credit_cards.png', - 'type' => 'png', - ), - '/rsrc/image/darkload.gif' => - array( - 'hash' => '3a52cb7145d6e70f461fed21273117f2', - 'uri' => '/res/3a52cb71/rsrc/image/darkload.gif', - 'disk' => '/rsrc/image/darkload.gif', - 'type' => 'gif', - ), - '/rsrc/image/divot.png' => - array( - 'hash' => '3be267bd11ea375bf68e808893718e0e', - 'uri' => '/res/3be267bd/rsrc/image/divot.png', - 'disk' => '/rsrc/image/divot.png', - 'type' => 'png', - ), - '/rsrc/image/grippy_texture.png' => - array( - 'hash' => 'a8945e12ceeaddd5b491a8d81cfa19c1', - 'uri' => '/res/a8945e12/rsrc/image/grippy_texture.png', - 'disk' => '/rsrc/image/grippy_texture.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/arrow_branch.png' => - array( - 'hash' => 'f27b67520766e3d971722bcff703f3a8', - 'uri' => '/res/f27b6752/rsrc/image/icon/fatcow/arrow_branch.png', - 'disk' => '/rsrc/image/icon/fatcow/arrow_branch.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/arrow_merge.png' => - array( - 'hash' => 'c4bd97f3b1257439e2123ef69d2194d0', - 'uri' => '/res/c4bd97f3/rsrc/image/icon/fatcow/arrow_merge.png', - 'disk' => '/rsrc/image/icon/fatcow/arrow_merge.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/bullet_black.png' => - array( - 'hash' => 'c148284c84aa02ba1190dcf7e31c8985', - 'uri' => '/res/c148284c/rsrc/image/icon/fatcow/bullet_black.png', - 'disk' => '/rsrc/image/icon/fatcow/bullet_black.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/bullet_orange.png' => - array( - 'hash' => '397bd1c948d9aaac5e440a9270c3697a', - 'uri' => '/res/397bd1c9/rsrc/image/icon/fatcow/bullet_orange.png', - 'disk' => '/rsrc/image/icon/fatcow/bullet_orange.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/bullet_red.png' => - array( - 'hash' => '470e3b2c2ca84ebdd476271b681f421b', - 'uri' => '/res/470e3b2c/rsrc/image/icon/fatcow/bullet_red.png', - 'disk' => '/rsrc/image/icon/fatcow/bullet_red.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/calendar_edit.png' => - array( - 'hash' => 'de249c0f4f37bf5b2c69ff39ec5573fb', - 'uri' => '/res/de249c0f/rsrc/image/icon/fatcow/calendar_edit.png', - 'disk' => '/rsrc/image/icon/fatcow/calendar_edit.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/document_black.png' => - array( - 'hash' => '44d65a7f05a9c921719deedc160d68f7', - 'uri' => '/res/44d65a7f/rsrc/image/icon/fatcow/document_black.png', - 'disk' => '/rsrc/image/icon/fatcow/document_black.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_blue.png' => - array( - 'hash' => '75a080492f900fbe489e4b27e403962b', - 'uri' => '/res/75a08049/rsrc/image/icon/fatcow/flag_blue.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_blue.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_finish.png' => - array( - 'hash' => '4af11fc7fab8e4610cbc3c88a02d4f78', - 'uri' => '/res/4af11fc7/rsrc/image/icon/fatcow/flag_finish.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_finish.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_ghost.png' => - array( - 'hash' => '14c9f30a37b43f276f27a27a924bf02d', - 'uri' => '/res/14c9f30a/rsrc/image/icon/fatcow/flag_ghost.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_ghost.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_green.png' => - array( - 'hash' => 'fed01374cd396cb774872762dcc447e1', - 'uri' => '/res/fed01374/rsrc/image/icon/fatcow/flag_green.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_green.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_orange.png' => - array( - 'hash' => '88008cb8bb99761a37e5a743e2455aeb', - 'uri' => '/res/88008cb8/rsrc/image/icon/fatcow/flag_orange.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_orange.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_pink.png' => - array( - 'hash' => '2f199f06ffc3dfc81b7561a057e0bc33', - 'uri' => '/res/2f199f06/rsrc/image/icon/fatcow/flag_pink.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_pink.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_purple.png' => - array( - 'hash' => '16358629dc86c39550b575586eb5df80', - 'uri' => '/res/16358629/rsrc/image/icon/fatcow/flag_purple.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_purple.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_red.png' => - array( - 'hash' => '210c28b4d93c439a499f5814f5e05772', - 'uri' => '/res/210c28b4/rsrc/image/icon/fatcow/flag_red.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_red.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/flag_yellow.png' => - array( - 'hash' => 'bdfd73744a80bb80329ae50bc8a5f962', - 'uri' => '/res/bdfd7374/rsrc/image/icon/fatcow/flag_yellow.png', - 'disk' => '/rsrc/image/icon/fatcow/flag_yellow.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/folder.png' => - array( - 'hash' => '25e46cf9d210dde2242332296f79938c', - 'uri' => '/res/25e46cf9/rsrc/image/icon/fatcow/folder.png', - 'disk' => '/rsrc/image/icon/fatcow/folder.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/folder_go.png' => - array( - 'hash' => 'ba922ff7959309f51a14cb7ed5124d8b', - 'uri' => '/res/ba922ff7/rsrc/image/icon/fatcow/folder_go.png', - 'disk' => '/rsrc/image/icon/fatcow/folder_go.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/key_question.png' => - array( - 'hash' => '530a6448a4b91edec091a9292ccfd3d9', - 'uri' => '/res/530a6448/rsrc/image/icon/fatcow/key_question.png', - 'disk' => '/rsrc/image/icon/fatcow/key_question.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/link.png' => - array( - 'hash' => 'be1bea49b216548433014f3324902928', - 'uri' => '/res/be1bea49/rsrc/image/icon/fatcow/link.png', - 'disk' => '/rsrc/image/icon/fatcow/link.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/page_white_edit.png' => - array( - 'hash' => 'e7b7e7f2d9730bc80bc5c9eac1f3e36d', - 'uri' => '/res/e7b7e7f2/rsrc/image/icon/fatcow/page_white_edit.png', - 'disk' => '/rsrc/image/icon/fatcow/page_white_edit.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/page_white_link.png' => - array( - 'hash' => '1cfbad14412bda6c6f132dcc7c8725fd', - 'uri' => '/res/1cfbad14/rsrc/image/icon/fatcow/page_white_link.png', - 'disk' => '/rsrc/image/icon/fatcow/page_white_link.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/page_white_put.png' => - array( - 'hash' => 'bb7308aa5ac40137a8262da395a267fd', - 'uri' => '/res/bb7308aa/rsrc/image/icon/fatcow/page_white_put.png', - 'disk' => '/rsrc/image/icon/fatcow/page_white_put.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/page_white_text.png' => - array( - 'hash' => 'e47d590b626f617fb7d1d44e96e8fd11', - 'uri' => '/res/e47d590b/rsrc/image/icon/fatcow/page_white_text.png', - 'disk' => '/rsrc/image/icon/fatcow/page_white_text.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/source/conduit.png' => - array( - 'hash' => '1cae0656580aa3cd0b54b9d98306b1b9', - 'uri' => '/res/1cae0656/rsrc/image/icon/fatcow/source/conduit.png', - 'disk' => '/rsrc/image/icon/fatcow/source/conduit.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/source/email.png' => - array( - 'hash' => '93bdb3e168da1ed68f50c42125729d4e', - 'uri' => '/res/93bdb3e1/rsrc/image/icon/fatcow/source/email.png', - 'disk' => '/rsrc/image/icon/fatcow/source/email.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/source/fax.png' => - array( - 'hash' => 'd7dedf229841f2d041b347afd881596f', - 'uri' => '/res/d7dedf22/rsrc/image/icon/fatcow/source/fax.png', - 'disk' => '/rsrc/image/icon/fatcow/source/fax.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/source/mobile.png' => - array( - 'hash' => '786e7146d1e7d7318baf76c9d2baad97', - 'uri' => '/res/786e7146/rsrc/image/icon/fatcow/source/mobile.png', - 'disk' => '/rsrc/image/icon/fatcow/source/mobile.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/source/tablet.png' => - array( - 'hash' => '374cd40e4965be6b2fbdef4059d0ca05', - 'uri' => '/res/374cd40e/rsrc/image/icon/fatcow/source/tablet.png', - 'disk' => '/rsrc/image/icon/fatcow/source/tablet.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/source/web.png' => - array( - 'hash' => 'f4882a8f5619ba505ca033f72a340635', - 'uri' => '/res/f4882a8f/rsrc/image/icon/fatcow/source/web.png', - 'disk' => '/rsrc/image/icon/fatcow/source/web.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/default160x120.png' => - array( - 'hash' => '1b52ebd1fe0eee3ed0abfc382991b265', - 'uri' => '/res/1b52ebd1/rsrc/image/icon/fatcow/thumbnails/default160x120.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/default160x120.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/default60x45.png' => - array( - 'hash' => '048d851d8d1daad4754e891e734c1899', - 'uri' => '/res/048d851d/rsrc/image/icon/fatcow/thumbnails/default60x45.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/default60x45.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/image160x120.png' => - array( - 'hash' => '434acbd8dbbc2da9f09f6205a396eba1', - 'uri' => '/res/434acbd8/rsrc/image/icon/fatcow/thumbnails/image160x120.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/image160x120.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/image60x45.png' => - array( - 'hash' => '29f7872dc53588fe0b8f0b330c7ee23a', - 'uri' => '/res/29f7872d/rsrc/image/icon/fatcow/thumbnails/image60x45.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/image60x45.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png' => - array( - 'hash' => '39d2e22541658a3472ba41ae2fa548e5', - 'uri' => '/res/39d2e225/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/pdf160x120.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png' => - array( - 'hash' => 'b3572e9317cbed5184d12bdfabed2727', - 'uri' => '/res/b3572e93/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/pdf60x45.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/zip160x120.png' => - array( - 'hash' => 'e505108688a903b5cfb674707a289bcc', - 'uri' => '/res/e5051086/rsrc/image/icon/fatcow/thumbnails/zip160x120.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/zip160x120.png', - 'type' => 'png', - ), - '/rsrc/image/icon/fatcow/thumbnails/zip60x45.png' => - array( - 'hash' => 'f00716f4e8f7a95e70d43504f06be0a6', - 'uri' => '/res/f00716f4/rsrc/image/icon/fatcow/thumbnails/zip60x45.png', - 'disk' => '/rsrc/image/icon/fatcow/thumbnails/zip60x45.png', - 'type' => 'png', - ), - '/rsrc/image/icon/lightbox/close-2.png' => - array( - 'hash' => '72ff3ddcc1ed5d19a715ed6242114b53', - 'uri' => '/res/72ff3ddc/rsrc/image/icon/lightbox/close-2.png', - 'disk' => '/rsrc/image/icon/lightbox/close-2.png', - 'type' => 'png', - ), - '/rsrc/image/icon/lightbox/close-hover-2.png' => - array( - 'hash' => '6ad4bd4a7820547a1d9041752546ba16', - 'uri' => '/res/6ad4bd4a/rsrc/image/icon/lightbox/close-hover-2.png', - 'disk' => '/rsrc/image/icon/lightbox/close-hover-2.png', - 'type' => 'png', - ), - '/rsrc/image/icon/lightbox/left-arrow-2.png' => - array( - 'hash' => 'd84cbb0d42739f87b8f25b2f1d2f1153', - 'uri' => '/res/d84cbb0d/rsrc/image/icon/lightbox/left-arrow-2.png', - 'disk' => '/rsrc/image/icon/lightbox/left-arrow-2.png', - 'type' => 'png', - ), - '/rsrc/image/icon/lightbox/left-arrow-hover-2.png' => - array( - 'hash' => 'cdf05f98fff3f390cd8df0c89894a3e1', - 'uri' => '/res/cdf05f98/rsrc/image/icon/lightbox/left-arrow-hover-2.png', - 'disk' => '/rsrc/image/icon/lightbox/left-arrow-hover-2.png', - 'type' => 'png', - ), - '/rsrc/image/icon/lightbox/right-arrow-2.png' => - array( - 'hash' => '52021038cb6995c71f62a804bc2d420d', - 'uri' => '/res/52021038/rsrc/image/icon/lightbox/right-arrow-2.png', - 'disk' => '/rsrc/image/icon/lightbox/right-arrow-2.png', - 'type' => 'png', - ), - '/rsrc/image/icon/lightbox/right-arrow-hover-2.png' => - array( - 'hash' => '65d5756b7b9cfcdeb2eb197a9aa6bbd2', - 'uri' => '/res/65d5756b/rsrc/image/icon/lightbox/right-arrow-hover-2.png', - 'disk' => '/rsrc/image/icon/lightbox/right-arrow-hover-2.png', - 'type' => 'png', - ), - '/rsrc/image/icon/subscribe.png' => - array( - 'hash' => '5f47a4b17de245af39a4e7a097e40623', - 'uri' => '/res/5f47a4b1/rsrc/image/icon/subscribe.png', - 'disk' => '/rsrc/image/icon/subscribe.png', - 'type' => 'png', - ), - '/rsrc/image/icon/tango/attachment.png' => - array( - 'hash' => '776fed2de89803fd8a0ba4b9deede230', - 'uri' => '/res/776fed2d/rsrc/image/icon/tango/attachment.png', - 'disk' => '/rsrc/image/icon/tango/attachment.png', - 'type' => 'png', - ), - '/rsrc/image/icon/tango/edit.png' => - array( - 'hash' => 'c0028d99dcf4e9559bbf3c88ce2d8a8d', - 'uri' => '/res/c0028d99/rsrc/image/icon/tango/edit.png', - 'disk' => '/rsrc/image/icon/tango/edit.png', - 'type' => 'png', - ), - '/rsrc/image/icon/tango/go-down.png' => - array( - 'hash' => '96862812cbb0445573c264dc057b8300', - 'uri' => '/res/96862812/rsrc/image/icon/tango/go-down.png', - 'disk' => '/rsrc/image/icon/tango/go-down.png', - 'type' => 'png', - ), - '/rsrc/image/icon/tango/log.png' => - array( - 'hash' => 'a6f72499bef279ff6807a7dbc5148f1e', - 'uri' => '/res/a6f72499/rsrc/image/icon/tango/log.png', - 'disk' => '/rsrc/image/icon/tango/log.png', - 'type' => 'png', - ), - '/rsrc/image/icon/tango/upload.png' => - array( - 'hash' => '8c11b63d6d99db3d7159c5d9a94e3062', - 'uri' => '/res/8c11b63d/rsrc/image/icon/tango/upload.png', - 'disk' => '/rsrc/image/icon/tango/upload.png', - 'type' => 'png', - ), - '/rsrc/image/icon/unsubscribe.png' => - array( - 'hash' => '29429ad65aa3af50b072b32087057361', - 'uri' => '/res/29429ad6/rsrc/image/icon/unsubscribe.png', - 'disk' => '/rsrc/image/icon/unsubscribe.png', - 'type' => 'png', - ), - '/rsrc/image/loading.gif' => - array( - 'hash' => '664297671941142f37d8c89e717ff2ce', - 'uri' => '/res/66429767/rsrc/image/loading.gif', - 'disk' => '/rsrc/image/loading.gif', - 'type' => 'gif', - ), - '/rsrc/image/loading/boating_24.gif' => - array( - 'hash' => '2cd349ded48d698ebe886ba97b2db0f7', - 'uri' => '/res/2cd349de/rsrc/image/loading/boating_24.gif', - 'disk' => '/rsrc/image/loading/boating_24.gif', - 'type' => 'gif', - ), - '/rsrc/image/loading/compass_24.gif' => - array( - 'hash' => '726c1ed4bf23446e044d6b9d28250a07', - 'uri' => '/res/726c1ed4/rsrc/image/loading/compass_24.gif', - 'disk' => '/rsrc/image/loading/compass_24.gif', - 'type' => 'gif', - ), - '/rsrc/image/loading/loading_24.gif' => - array( - 'hash' => 'd6dcc5e6111a44fb9a160fc27b19d85c', - 'uri' => '/res/d6dcc5e6/rsrc/image/loading/loading_24.gif', - 'disk' => '/rsrc/image/loading/loading_24.gif', - 'type' => 'gif', - ), - '/rsrc/image/loading/loading_48.gif' => - array( - 'hash' => 'cb6fc6eb9c0a0efaf589978029080c58', - 'uri' => '/res/cb6fc6eb/rsrc/image/loading/loading_48.gif', - 'disk' => '/rsrc/image/loading/loading_48.gif', - 'type' => 'gif', - ), - '/rsrc/image/loading/loading_d48.gif' => - array( - 'hash' => 'c5181f5e0ac8125ad9beda73fdf18e91', - 'uri' => '/res/c5181f5e/rsrc/image/loading/loading_d48.gif', - 'disk' => '/rsrc/image/loading/loading_d48.gif', - 'type' => 'gif', - ), - '/rsrc/image/loading/loading_w24.gif' => - array( - 'hash' => '231857d68736e9bdda6bdbaaf924b8da', - 'uri' => '/res/231857d6/rsrc/image/loading/loading_w24.gif', - 'disk' => '/rsrc/image/loading/loading_w24.gif', - 'type' => 'gif', - ), - '/rsrc/image/main_texture.png' => - array( - 'hash' => 'e34d8143384721be73ec9b7532a977ab', - 'uri' => '/res/e34d8143/rsrc/image/main_texture.png', - 'disk' => '/rsrc/image/main_texture.png', - 'type' => 'png', - ), - '/rsrc/image/menu_texture.png' => - array( - 'hash' => 'ad020b1529b3a3b3480ca9de1d5f1e40', - 'uri' => '/res/ad020b15/rsrc/image/menu_texture.png', - 'disk' => '/rsrc/image/menu_texture.png', - 'type' => 'png', - ), - '/rsrc/image/people/harding.png' => - array( - 'hash' => '818b035ace2c480aa8df7b7f11cef58b', - 'uri' => '/res/818b035a/rsrc/image/people/harding.png', - 'disk' => '/rsrc/image/people/harding.png', - 'type' => 'png', - ), - '/rsrc/image/people/jefferson.png' => - array( - 'hash' => '55fe807ff02f9320e595fb59442e2038', - 'uri' => '/res/55fe807f/rsrc/image/people/jefferson.png', - 'disk' => '/rsrc/image/people/jefferson.png', - 'type' => 'png', - ), - '/rsrc/image/people/lincoln.png' => - array( - 'hash' => '2363337947ab52fd5fda79e4a004e930', - 'uri' => '/res/23633379/rsrc/image/people/lincoln.png', - 'disk' => '/rsrc/image/people/lincoln.png', - 'type' => 'png', - ), - '/rsrc/image/people/mckinley.png' => - array( - 'hash' => '0b7b05dd47c49a0874670e5e8200bba8', - 'uri' => '/res/0b7b05dd/rsrc/image/people/mckinley.png', - 'disk' => '/rsrc/image/people/mckinley.png', - 'type' => 'png', - ), - '/rsrc/image/people/taft.png' => - array( - 'hash' => 'f3e47d45b59b0b009fd536dabae9a151', - 'uri' => '/res/f3e47d45/rsrc/image/people/taft.png', - 'disk' => '/rsrc/image/people/taft.png', - 'type' => 'png', - ), - '/rsrc/image/people/washington.png' => - array( - 'hash' => '01412761cab769f7993d69eba986d949', - 'uri' => '/res/01412761/rsrc/image/people/washington.png', - 'disk' => '/rsrc/image/people/washington.png', - 'type' => 'png', - ), - '/rsrc/image/phrequent_active.png' => - array( - 'hash' => '716cddc08630eaa33934b2008723cac0', - 'uri' => '/res/716cddc0/rsrc/image/phrequent_active.png', - 'disk' => '/rsrc/image/phrequent_active.png', - 'type' => 'png', - ), - '/rsrc/image/phrequent_inactive.png' => - array( - 'hash' => 'f9099683873c01c5de1dc6650bd668fe', - 'uri' => '/res/f9099683/rsrc/image/phrequent_inactive.png', - 'disk' => '/rsrc/image/phrequent_inactive.png', - 'type' => 'png', - ), - '/rsrc/image/search.png' => - array( - 'hash' => 'ff7da044e6f923b8f569dec11f97e5e5', - 'uri' => '/res/ff7da044/rsrc/image/search.png', - 'disk' => '/rsrc/image/search.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-actions-X2.png' => - array( - 'hash' => '06962a5e8bea98ba7418d1d6cabcd7dc', - 'uri' => '/res/06962a5e/rsrc/image/sprite-actions-X2.png', - 'disk' => '/rsrc/image/sprite-actions-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-actions.png' => - array( - 'hash' => 'd5dda5fab1e61b00538c9a4fa1ee94c8', - 'uri' => '/res/d5dda5fa/rsrc/image/sprite-actions.png', - 'disk' => '/rsrc/image/sprite-actions.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-apps-X2.png' => - array( - 'hash' => '67e8a6bf2d7fbb0b7961f1a0dcf8592b', - 'uri' => '/res/67e8a6bf/rsrc/image/sprite-apps-X2.png', - 'disk' => '/rsrc/image/sprite-apps-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-apps-large-X2.png' => - array( - 'hash' => '9bed1778022e2bd25d658842be54844d', - 'uri' => '/res/9bed1778/rsrc/image/sprite-apps-large-X2.png', - 'disk' => '/rsrc/image/sprite-apps-large-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-apps-large.png' => - array( - 'hash' => '518d6e5487c8d3758921ad85c1bb7d60', - 'uri' => '/res/518d6e54/rsrc/image/sprite-apps-large.png', - 'disk' => '/rsrc/image/sprite-apps-large.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-apps-xlarge.png' => - array( - 'hash' => '992d2c278b6a22c0fa874d457a252fbd', - 'uri' => '/res/992d2c27/rsrc/image/sprite-apps-xlarge.png', - 'disk' => '/rsrc/image/sprite-apps-xlarge.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-apps.png' => - array( - 'hash' => '9024ab95247f936f41c0b51c75e2e228', - 'uri' => '/res/9024ab95/rsrc/image/sprite-apps.png', - 'disk' => '/rsrc/image/sprite-apps.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-conpherence-X2.png' => - array( - 'hash' => '5e47868b00933a9afb6c844e464e6b23', - 'uri' => '/res/5e47868b/rsrc/image/sprite-conpherence-X2.png', - 'disk' => '/rsrc/image/sprite-conpherence-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-conpherence.png' => - array( - 'hash' => 'ca51f1be25213262d68e626e4cab7f0f', - 'uri' => '/res/ca51f1be/rsrc/image/sprite-conpherence.png', - 'disk' => '/rsrc/image/sprite-conpherence.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-docs-X2.png' => - array( - 'hash' => '57d3286ce88133f3ec9240e35f6bb897', - 'uri' => '/res/57d3286c/rsrc/image/sprite-docs-X2.png', - 'disk' => '/rsrc/image/sprite-docs-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-docs.png' => - array( - 'hash' => 'b2b089072d6eddd831402a77c02b5736', - 'uri' => '/res/b2b08907/rsrc/image/sprite-docs.png', - 'disk' => '/rsrc/image/sprite-docs.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-gradient.png' => - array( - 'hash' => '1f0306b0ca281b1e5b96de0096269f1d', - 'uri' => '/res/1f0306b0/rsrc/image/sprite-gradient.png', - 'disk' => '/rsrc/image/sprite-gradient.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-icons-X2.png' => - array( - 'hash' => '4fcceb49691b148ad5dc6671295ff378', - 'uri' => '/res/4fcceb49/rsrc/image/sprite-icons-X2.png', - 'disk' => '/rsrc/image/sprite-icons-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-icons.png' => - array( - 'hash' => '736b067d369ff89f1ab2756c619ba129', - 'uri' => '/res/736b067d/rsrc/image/sprite-icons.png', - 'disk' => '/rsrc/image/sprite-icons.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-login-X2.png' => - array( - 'hash' => '7176335e4e1604f94eacdb1790660560', - 'uri' => '/res/7176335e/rsrc/image/sprite-login-X2.png', - 'disk' => '/rsrc/image/sprite-login-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-login.png' => - array( - 'hash' => '7d3eee260ee0beb90c12e26fbc48fd9c', - 'uri' => '/res/7d3eee26/rsrc/image/sprite-login.png', - 'disk' => '/rsrc/image/sprite-login.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-menu-X2.png' => - array( - 'hash' => '63b649a6ccba7bf76bc9456dc5dfb12b', - 'uri' => '/res/63b649a6/rsrc/image/sprite-menu-X2.png', - 'disk' => '/rsrc/image/sprite-menu-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-menu.png' => - array( - 'hash' => 'e0e16618691d2cffe64e9c57843828ff', - 'uri' => '/res/e0e16618/rsrc/image/sprite-menu.png', - 'disk' => '/rsrc/image/sprite-menu.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-minicons-X2.png' => - array( - 'hash' => 'c420c6462f7e50ca9941ccc5dd9e3dec', - 'uri' => '/res/c420c646/rsrc/image/sprite-minicons-X2.png', - 'disk' => '/rsrc/image/sprite-minicons-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-minicons.png' => - array( - 'hash' => '168bb875933624b3080a1cc134e5b4ed', - 'uri' => '/res/168bb875/rsrc/image/sprite-minicons.png', - 'disk' => '/rsrc/image/sprite-minicons.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-payments.png' => - array( - 'hash' => '5ce73fb580609e7cda16832e3577b147', - 'uri' => '/res/5ce73fb5/rsrc/image/sprite-payments.png', - 'disk' => '/rsrc/image/sprite-payments.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-projects-X2.png' => - array( - 'hash' => '3bd29905e197068a75ace63880a2b6eb', - 'uri' => '/res/3bd29905/rsrc/image/sprite-projects-X2.png', - 'disk' => '/rsrc/image/sprite-projects-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-projects.png' => - array( - 'hash' => 'd9ec3fa470e6523520726ef75b011a03', - 'uri' => '/res/d9ec3fa4/rsrc/image/sprite-projects.png', - 'disk' => '/rsrc/image/sprite-projects.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-status-X2.png' => - array( - 'hash' => 'bb0d9cc2fec8e852c69790cbb626c6b1', - 'uri' => '/res/bb0d9cc2/rsrc/image/sprite-status-X2.png', - 'disk' => '/rsrc/image/sprite-status-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-status.png' => - array( - 'hash' => 'b78e998cb34964052b17a8777651ecbd', - 'uri' => '/res/b78e998c/rsrc/image/sprite-status.png', - 'disk' => '/rsrc/image/sprite-status.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-tokens-X2.png' => - array( - 'hash' => '8b822687e6b1088cbb5ea89cf6d351a4', - 'uri' => '/res/8b822687/rsrc/image/sprite-tokens-X2.png', - 'disk' => '/rsrc/image/sprite-tokens-X2.png', - 'type' => 'png', - ), - '/rsrc/image/sprite-tokens.png' => - array( - 'hash' => '67c46fd75c885b76ecbfe46e71a476cc', - 'uri' => '/res/67c46fd7/rsrc/image/sprite-tokens.png', - 'disk' => '/rsrc/image/sprite-tokens.png', - 'type' => 'png', - ), - '/rsrc/image/texture/card-gradient.png' => - array( - 'hash' => '268b7fdd758d4bf99db8de6770aae8af', - 'uri' => '/res/268b7fdd/rsrc/image/texture/card-gradient.png', - 'disk' => '/rsrc/image/texture/card-gradient.png', - 'type' => 'png', - ), - '/rsrc/image/texture/dark-menu-hover.png' => - array( - 'hash' => 'a214a732644be34872e895b338b5d639', - 'uri' => '/res/a214a732/rsrc/image/texture/dark-menu-hover.png', - 'disk' => '/rsrc/image/texture/dark-menu-hover.png', - 'type' => 'png', - ), - '/rsrc/image/texture/dark-menu.png' => - array( - 'hash' => '41ee673a762cec48a154b456ad5ac204', - 'uri' => '/res/41ee673a/rsrc/image/texture/dark-menu.png', - 'disk' => '/rsrc/image/texture/dark-menu.png', - 'type' => 'png', - ), - '/rsrc/image/texture/grip.png' => - array( - 'hash' => 'f11bc231d241f1335cfca2933ad234e0', - 'uri' => '/res/f11bc231/rsrc/image/texture/grip.png', - 'disk' => '/rsrc/image/texture/grip.png', - 'type' => 'png', - ), - '/rsrc/image/texture/panel-header-gradient.png' => - array( - 'hash' => 'ad9204dd3ef5b12b645d80677d8ccead', - 'uri' => '/res/ad9204dd/rsrc/image/texture/panel-header-gradient.png', - 'disk' => '/rsrc/image/texture/panel-header-gradient.png', - 'type' => 'png', - ), - '/rsrc/image/texture/phlnx-bg.png' => - array( - 'hash' => 'a55a694da8b3874ca7a3105b7818f3a0', - 'uri' => '/res/a55a694d/rsrc/image/texture/phlnx-bg.png', - 'disk' => '/rsrc/image/texture/phlnx-bg.png', - 'type' => 'png', - ), - '/rsrc/image/texture/pholio-background.gif' => - array( - 'hash' => 'cf4561af116edf393dc583e5119fb412', - 'uri' => '/res/cf4561af/rsrc/image/texture/pholio-background.gif', - 'disk' => '/rsrc/image/texture/pholio-background.gif', - 'type' => 'gif', - ), - '/rsrc/image/texture/table_header.png' => - array( - 'hash' => '4ed3f56a30d3749e8f62052b9735a316', - 'uri' => '/res/4ed3f56a/rsrc/image/texture/table_header.png', - 'disk' => '/rsrc/image/texture/table_header.png', - 'type' => 'png', - ), - '/rsrc/image/texture/table_header_hover.png' => - array( - 'hash' => 'ea1f71a604e9b4859de1e25751540437', - 'uri' => '/res/ea1f71a6/rsrc/image/texture/table_header_hover.png', - 'disk' => '/rsrc/image/texture/table_header_hover.png', - 'type' => 'png', - ), - '/rsrc/image/texture/table_header_tall.png' => - array( - 'hash' => 'b05525601f78d759f1c5e47fd9c1a8aa', - 'uri' => '/res/b0552560/rsrc/image/texture/table_header_tall.png', - 'disk' => '/rsrc/image/texture/table_header_tall.png', - 'type' => 'png', - ), - '/rsrc/swf/aphlict.swf' => - array( - 'hash' => '4b9a9d83bebaf254f3790e87b45c1f92', - 'uri' => '/res/4b9a9d83/rsrc/swf/aphlict.swf', - 'disk' => '/rsrc/swf/aphlict.swf', - 'type' => 'swf', - ), - 'aphront-bars' => - array( - 'uri' => '/res/dc8fd846/rsrc/css/aphront/aphront-bars.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/aphront-bars.css', - ), - 'aphront-calendar-view-css' => - array( - 'uri' => '/res/d5a33deb/rsrc/css/aphront/calendar-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/calendar-view.css', - ), - 'aphront-contextbar-view-css' => - array( - 'uri' => '/res/46c6248f/rsrc/css/aphront/context-bar.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/context-bar.css', - ), - 'aphront-dark-console-css' => - array( - 'uri' => '/res/5c341863/rsrc/css/aphront/dark-console.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/dark-console.css', - ), - 'aphront-dialog-view-css' => - array( - 'uri' => '/res/830fa2de/rsrc/css/aphront/dialog-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/dialog-view.css', - ), - 'aphront-error-view-css' => - array( - 'uri' => '/res/cb571901/rsrc/css/aphront/error-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/error-view.css', - ), - 'aphront-list-filter-view-css' => - array( - 'uri' => '/res/b770e0da/rsrc/css/aphront/list-filter-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/list-filter-view.css', - ), - 'aphront-multi-column-view-css' => - array( - 'uri' => '/res/9d2b2374/rsrc/css/aphront/multi-column.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/multi-column.css', - ), - 'aphront-notes' => - array( - 'uri' => '/res/ac115367/rsrc/css/aphront/aphront-notes.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/aphront-notes.css', - ), - 'aphront-pager-view-css' => - array( - 'uri' => '/res/ea81aec0/rsrc/css/aphront/pager-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/pager-view.css', - ), - 'aphront-panel-view-css' => - array( - 'uri' => '/res/70d7011b/rsrc/css/aphront/panel-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/panel-view.css', - ), - 'aphront-request-failure-view-css' => - array( - 'uri' => '/res/c9a43002/rsrc/css/aphront/request-failure-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/request-failure-view.css', - ), - 'aphront-table-view-css' => - array( - 'uri' => '/res/24f51f0b/rsrc/css/aphront/table-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/table-view.css', - ), - 'aphront-tokenizer-control-css' => - array( - 'uri' => '/res/36192cf2/rsrc/css/aphront/tokenizer.css', - 'type' => 'css', - 'requires' => - array( - 0 => 'aphront-typeahead-control-css', - ), - 'disk' => '/rsrc/css/aphront/tokenizer.css', - ), - 'aphront-tooltip-css' => - array( - 'uri' => '/res/3a7d8e07/rsrc/css/aphront/tooltip.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/tooltip.css', - ), - 'aphront-two-column-view-css' => - array( - 'uri' => '/res/4263aa98/rsrc/css/aphront/two-column.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/two-column.css', - ), - 'aphront-typeahead-control-css' => - array( - 'uri' => '/res/c6ad64bb/rsrc/css/aphront/typeahead.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/typeahead.css', - ), - 'auth-css' => - array( - 'uri' => '/res/9e544d3c/rsrc/css/application/auth/auth.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/auth/auth.css', - ), - 'config-options-css' => - array( - 'uri' => '/res/4b5b6779/rsrc/css/application/config/config-options.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/config/config-options.css', - ), - 'conpherence-menu-css' => - array( - 'uri' => '/res/cae40b18/rsrc/css/application/conpherence/menu.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/conpherence/menu.css', - ), - 'conpherence-message-pane-css' => - array( - 'uri' => '/res/150f96d4/rsrc/css/application/conpherence/message-pane.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/conpherence/message-pane.css', - ), - 'conpherence-notification-css' => - array( - 'uri' => '/res/232c8cdb/rsrc/css/application/conpherence/notification.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/conpherence/notification.css', - ), - 'conpherence-update-css' => - array( - 'uri' => '/res/92094ed7/rsrc/css/application/conpherence/update.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/conpherence/update.css', - ), - 'conpherence-widget-pane-css' => - array( - 'uri' => '/res/13478b94/rsrc/css/application/conpherence/widget-pane.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/conpherence/widget-pane.css', - ), - 'differential-changeset-view-css' => - array( - 'uri' => '/res/37f702ae/rsrc/css/application/differential/changeset-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/changeset-view.css', - ), - 'differential-core-view-css' => - array( - 'uri' => '/res/18563185/rsrc/css/application/differential/core.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/core.css', - ), - 'differential-inline-comment-editor' => - array( - 'uri' => '/res/e952d210/rsrc/js/application/differential/DifferentialInlineCommentEditor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-dom', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - 3 => 'javelin-install', - 4 => 'javelin-request', - 5 => 'javelin-workflow', - ), - 'disk' => '/rsrc/js/application/differential/DifferentialInlineCommentEditor.js', - ), - 'differential-local-commits-view-css' => - array( - 'uri' => '/res/c6e9db42/rsrc/css/application/differential/local-commits-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/local-commits-view.css', - ), - 'differential-results-table-css' => - array( - 'uri' => '/res/5e37cf75/rsrc/css/application/differential/results-table.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/results-table.css', - ), - 'differential-revision-add-comment-css' => - array( - 'uri' => '/res/849748d3/rsrc/css/application/differential/add-comment.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/add-comment.css', - ), - 'differential-revision-comment-css' => - array( - 'uri' => '/res/e2dda8b5/rsrc/css/application/differential/revision-comment.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/revision-comment.css', - ), - 'differential-revision-comment-list-css' => - array( - 'uri' => '/res/6cc4ca9b/rsrc/css/application/differential/revision-comment-list.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/revision-comment-list.css', - ), - 'differential-revision-history-css' => - array( - 'uri' => '/res/13b4c17b/rsrc/css/application/differential/revision-history.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/revision-history.css', - ), - 'differential-revision-list-css' => - array( - 'uri' => '/res/fe6c4721/rsrc/css/application/differential/revision-list.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/revision-list.css', - ), - 'differential-table-of-contents-css' => - array( - 'uri' => '/res/3bb8c01f/rsrc/css/application/differential/table-of-contents.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/differential/table-of-contents.css', - ), - 'diffusion-commit-view-css' => - array( - 'uri' => '/res/a48ea65a/rsrc/css/application/diffusion/commit-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/diffusion/commit-view.css', - ), - 'diffusion-icons-css' => - array( - 'uri' => '/res/82e77537/rsrc/css/application/diffusion/diffusion-icons.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/diffusion/diffusion-icons.css', - ), - 'diffusion-source-css' => - array( - 'uri' => '/res/5076c269/rsrc/css/application/diffusion/diffusion-source.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/diffusion/diffusion-source.css', - ), - 'diviner-shared-css' => - array( - 'uri' => '/res/2e831eea/rsrc/css/diviner/diviner-shared.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/diviner/diviner-shared.css', - ), - 'global-drag-and-drop-css' => - array( - 'uri' => '/res/4e24cb65/rsrc/css/application/files/global-drag-and-drop.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/files/global-drag-and-drop.css', - ), - 'herald-css' => - array( - 'uri' => '/res/2150a55d/rsrc/css/application/herald/herald.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/herald/herald.css', - ), - 'herald-rule-editor' => - array( - 'uri' => '/res/a561eb19/rsrc/js/application/herald/HeraldRuleEditor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'multirow-row-manager', - 1 => 'javelin-install', - 2 => 'javelin-typeahead', - 3 => 'javelin-util', - 4 => 'javelin-dom', - 5 => 'javelin-tokenizer', - 6 => 'javelin-typeahead-preloaded-source', - 7 => 'javelin-stratcom', - 8 => 'javelin-json', - 9 => 'phabricator-prefab', - ), - 'disk' => '/rsrc/js/application/herald/HeraldRuleEditor.js', - ), - 'herald-test-css' => - array( - 'uri' => '/res/51199954/rsrc/css/application/herald/herald-test.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/herald/herald-test.css', - ), - 'inline-comment-summary-css' => - array( - 'uri' => '/res/3cf1f7a7/rsrc/css/application/diff/inline-comment-summary.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/diff/inline-comment-summary.css', - ), - 'javelin-aphlict' => - array( - 'uri' => '/res/c0b9e53f/rsrc/js/application/aphlict/Aphlict.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/aphlict/Aphlict.js', - ), - 'javelin-behavior' => - array( - 'uri' => '/res/15482715/rsrc/externals/javelin/lib/behavior.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-magical-init', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/lib/behavior.js', - ), - 'javelin-behavior-aphlict-dropdown' => - array( - 'uri' => '/res/3ff0c90a/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-request', - 2 => 'javelin-stratcom', - 3 => 'javelin-vector', - 4 => 'javelin-dom', - 5 => 'javelin-uri', - ), - 'disk' => '/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js', - ), - 'javelin-behavior-aphlict-listen' => - array( - 'uri' => '/res/7487f207/rsrc/js/application/aphlict/behavior-aphlict-listen.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-aphlict', - 2 => 'javelin-stratcom', - 3 => 'javelin-request', - 4 => 'javelin-uri', - 5 => 'javelin-dom', - 6 => 'javelin-json', - 7 => 'phabricator-notification', - ), - 'disk' => '/rsrc/js/application/aphlict/behavior-aphlict-listen.js', - ), - 'javelin-behavior-aphront-basic-tokenizer' => - array( - 'uri' => '/res/c7fd9a7b/rsrc/js/core/behavior-tokenizer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'phabricator-prefab', - ), - 'disk' => '/rsrc/js/core/behavior-tokenizer.js', - ), - 'javelin-behavior-aphront-crop' => - array( - 'uri' => '/res/8c800f36/rsrc/js/core/behavior-crop.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-vector', - 3 => 'javelin-magical-init', - ), - 'disk' => '/rsrc/js/core/behavior-crop.js', - ), - 'javelin-behavior-aphront-drag-and-drop-textarea' => - array( - 'uri' => '/res/a261f6e6/rsrc/js/core/behavior-drag-and-drop-textarea.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'phabricator-drag-and-drop-file-upload', - 3 => 'phabricator-textareautils', - ), - 'disk' => '/rsrc/js/core/behavior-drag-and-drop-textarea.js', - ), - 'javelin-behavior-aphront-form-disable-on-submit' => - array( - 'uri' => '/res/a4a4ff07/rsrc/js/core/behavior-form.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-form.js', - ), - 'javelin-behavior-aphront-more' => - array( - 'uri' => '/res/fae13324/rsrc/js/core/behavior-more.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-more.js', - ), - 'javelin-behavior-audio-source' => - array( - 'uri' => '/res/21831141/rsrc/js/core/behavior-audio-source.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-vector', - 3 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-audio-source.js', - ), - 'javelin-behavior-audit-preview' => - array( - 'uri' => '/res/d8f31e46/rsrc/js/application/diffusion/behavior-audit-preview.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'phabricator-shaped-request', - ), - 'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js', - ), - 'javelin-behavior-balanced-payment-form' => - array( - 'uri' => '/res/6876492d/rsrc/js/application/phortune/behavior-balanced-payment-form.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'phortune-credit-card-form', - ), - 'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js', - ), - 'javelin-behavior-config-reorder-fields' => - array( - 'uri' => '/res/691c5c8c/rsrc/js/application/config/behavior-reorder-fields.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-json', - 4 => 'phabricator-draggable-list', - ), - 'disk' => '/rsrc/js/application/config/behavior-reorder-fields.js', - ), - 'javelin-behavior-conpherence-menu' => - array( - 'uri' => '/res/f27205d4/rsrc/js/application/conpherence/behavior-menu.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-stratcom', - 4 => 'javelin-workflow', - 5 => 'javelin-behavior-device', - 6 => 'javelin-history', - 7 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', - ), - 'javelin-behavior-conpherence-pontificate' => - array( - 'uri' => '/res/19cb581b/rsrc/js/application/conpherence/behavior-pontificate.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-workflow', - 4 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/application/conpherence/behavior-pontificate.js', - ), - 'javelin-behavior-conpherence-widget-pane' => - array( - 'uri' => '/res/562ca20e/rsrc/js/application/conpherence/behavior-widget-pane.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-workflow', - 4 => 'javelin-util', - 5 => 'phabricator-notification', - 6 => 'javelin-behavior-device', - 7 => 'phabricator-dropdown-menu', - 8 => 'phabricator-menu-item', - ), - 'disk' => '/rsrc/js/application/conpherence/behavior-widget-pane.js', - ), - 'javelin-behavior-countdown-timer' => - array( - 'uri' => '/res/13d40efa/rsrc/js/application/countdown/timer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/countdown/timer.js', - ), - 'javelin-behavior-dark-console' => - array( - 'uri' => '/res/1e2c7a5e/rsrc/js/core/behavior-dark-console.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-util', - 3 => 'javelin-dom', - 4 => 'javelin-request', - 5 => 'phabricator-keyboard-shortcut', - ), - 'disk' => '/rsrc/js/core/behavior-dark-console.js', - ), - 'javelin-behavior-device' => - array( - 'uri' => '/res/12e43f5a/rsrc/js/core/behavior-device.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - 4 => 'javelin-install', - ), - 'disk' => '/rsrc/js/core/behavior-device.js', - ), - 'javelin-behavior-differential-accept-with-errors' => - array( - 'uri' => '/res/8fea67b3/rsrc/js/application/differential/behavior-accept-with-errors.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-accept-with-errors.js', - ), - 'javelin-behavior-differential-add-reviewers-and-ccs' => - array( - 'uri' => '/res/fd9f2c1c/rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'phabricator-prefab', - ), - 'disk' => '/rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js', - ), - 'javelin-behavior-differential-comment-jump' => - array( - 'uri' => '/res/8ffb4222/rsrc/js/application/differential/behavior-comment-jump.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-comment-jump.js', - ), - 'javelin-behavior-differential-diff-radios' => - array( - 'uri' => '/res/004cb66f/rsrc/js/application/differential/behavior-diff-radios.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-diff-radios.js', - ), - 'javelin-behavior-differential-dropdown-menus' => - array( - 'uri' => '/res/722c679c/rsrc/js/application/differential/behavior-dropdown-menus.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-stratcom', - 4 => 'phabricator-dropdown-menu', - 5 => 'phabricator-menu-item', - 6 => 'phabricator-phtize', - ), - 'disk' => '/rsrc/js/application/differential/behavior-dropdown-menus.js', - ), - 'javelin-behavior-differential-edit-inline-comments' => - array( - 'uri' => '/res/935d4012/rsrc/js/application/differential/behavior-edit-inline-comments.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-util', - 4 => 'javelin-vector', - 5 => 'differential-inline-comment-editor', - ), - 'disk' => '/rsrc/js/application/differential/behavior-edit-inline-comments.js', - ), - 'javelin-behavior-differential-feedback-preview' => - array( - 'uri' => '/res/4421fac6/rsrc/js/application/differential/behavior-comment-preview.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-request', - 4 => 'javelin-util', - 5 => 'phabricator-shaped-request', - ), - 'disk' => '/rsrc/js/application/differential/behavior-comment-preview.js', - ), - 'javelin-behavior-differential-keyboard-navigation' => - array( - 'uri' => '/res/22ed93ba/rsrc/js/application/differential/behavior-keyboard-nav.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'phabricator-keyboard-shortcut', - ), - 'disk' => '/rsrc/js/application/differential/behavior-keyboard-nav.js', - ), - 'javelin-behavior-differential-populate' => - array( - 'uri' => '/res/bb9a29f4/rsrc/js/application/differential/behavior-populate.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-workflow', - 2 => 'javelin-util', - 3 => 'javelin-dom', - 4 => 'javelin-stratcom', - 5 => 'javelin-behavior-device', - 6 => 'javelin-vector', - 7 => 'phabricator-tooltip', - ), - 'disk' => '/rsrc/js/application/differential/behavior-populate.js', - ), - 'javelin-behavior-differential-show-all-comments' => - array( - 'uri' => '/res/8801848d/rsrc/js/application/differential/behavior-show-all-comments.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-show-all-comments.js', - ), - 'javelin-behavior-differential-show-field-details' => - array( - 'uri' => '/res/8d57f459/rsrc/js/application/differential/behavior-show-field-details.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-show-field-details.js', - ), - 'javelin-behavior-differential-show-more' => - array( - 'uri' => '/res/03b7bc9e/rsrc/js/application/differential/behavior-show-more.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-workflow', - 3 => 'javelin-util', - 4 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-show-more.js', - ), - 'javelin-behavior-differential-toggle-files' => - array( - 'uri' => '/res/beb89813/rsrc/js/application/differential/behavior-toggle-files.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'phabricator-phtize', - ), - 'disk' => '/rsrc/js/application/differential/behavior-toggle-files.js', - ), - 'javelin-behavior-differential-user-select' => - array( - 'uri' => '/res/23c51a5d/rsrc/js/application/differential/behavior-user-select.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/application/differential/behavior-user-select.js', - ), - 'javelin-behavior-diffusion-commit-branches' => - array( - 'uri' => '/res/1ede335a/rsrc/js/application/diffusion/behavior-commit-branches.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-request', - ), - 'disk' => '/rsrc/js/application/diffusion/behavior-commit-branches.js', - ), - 'javelin-behavior-diffusion-commit-graph' => - array( - 'uri' => '/res/536b8483/rsrc/js/application/diffusion/behavior-commit-graph.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/application/diffusion/behavior-commit-graph.js', - ), - 'javelin-behavior-diffusion-jump-to' => - array( - 'uri' => '/res/bade44bd/rsrc/js/application/diffusion/behavior-jump-to.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-vector', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/diffusion/behavior-jump-to.js', - ), - 'javelin-behavior-diffusion-pull-lastmodified' => - array( - 'uri' => '/res/29fe2790/rsrc/js/application/diffusion/behavior-pull-lastmodified.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-request', - ), - 'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js', - ), - 'javelin-behavior-doorkeeper-tag' => - array( - 'uri' => '/res/59480572/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-json', - 3 => 'javelin-workflow', - 4 => 'javelin-magical-init', - ), - 'disk' => '/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js', - ), - 'javelin-behavior-error-log' => - array( - 'uri' => '/res/acefdea7/rsrc/js/core/behavior-error-log.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-error-log.js', - ), - 'javelin-behavior-fancy-datepicker' => - array( - 'uri' => '/res/dcd7c2ca/rsrc/js/core/behavior-fancy-datepicker.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-stratcom', - 4 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/core/behavior-fancy-datepicker.js', - ), - 'javelin-behavior-global-drag-and-drop' => - array( - 'uri' => '/res/ee8e9c39/rsrc/js/core/behavior-global-drag-and-drop.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-uri', - 3 => 'javelin-mask', - 4 => 'phabricator-drag-and-drop-file-upload', - ), - 'disk' => '/rsrc/js/core/behavior-global-drag-and-drop.js', - ), - 'javelin-behavior-herald-rule-editor' => - array( - 'uri' => '/res/77a0c945/rsrc/js/application/herald/herald-rule-editor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'herald-rule-editor', - 1 => 'javelin-behavior', - ), - 'disk' => '/rsrc/js/application/herald/herald-rule-editor.js', - ), - 'javelin-behavior-history-install' => - array( - 'uri' => '/res/9099a161/rsrc/js/core/behavior-history-install.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-history', - ), - 'disk' => '/rsrc/js/core/behavior-history-install.js', - ), - 'javelin-behavior-icon-composer' => - array( - 'uri' => '/res/0be5c462/rsrc/js/application/files/behavior-icon-composer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/application/files/behavior-icon-composer.js', - ), - 'javelin-behavior-konami' => - array( - 'uri' => '/res/b7bb7c24/rsrc/js/core/behavior-konami.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/core/behavior-konami.js', - ), - 'javelin-behavior-launch-icon-composer' => - array( - 'uri' => '/res/202488ac/rsrc/js/application/files/behavior-launch-icon-composer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-workflow', - ), - 'disk' => '/rsrc/js/application/files/behavior-launch-icon-composer.js', - ), - 'javelin-behavior-lightbox-attachments' => - array( - 'uri' => '/res/72b4d3a8/rsrc/js/core/behavior-lightbox-attachments.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-mask', - 4 => 'javelin-util', - 5 => 'phabricator-busy', - ), - 'disk' => '/rsrc/js/core/behavior-lightbox-attachments.js', - ), - 'javelin-behavior-line-chart' => - array( - 'uri' => '/res/1aa5ac88/rsrc/js/application/maniphest/behavior-line-chart.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-line-chart.js', - ), - 'javelin-behavior-load-blame' => - array( - 'uri' => '/res/138e2961/rsrc/js/application/diffusion/behavior-load-blame.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-request', - ), - 'disk' => '/rsrc/js/application/diffusion/behavior-load-blame.js', - ), - 'javelin-behavior-maniphest-batch-editor' => - array( - 'uri' => '/res/81b2b86f/rsrc/js/application/maniphest/behavior-batch-editor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'phabricator-prefab', - 4 => 'multirow-row-manager', - 5 => 'javelin-json', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-batch-editor.js', - ), - 'javelin-behavior-maniphest-batch-selector' => - array( - 'uri' => '/res/423c5f1b/rsrc/js/application/maniphest/behavior-batch-selector.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-batch-selector.js', - ), - 'javelin-behavior-maniphest-list-editor' => - array( - 'uri' => '/res/a251e72f/rsrc/js/application/maniphest/behavior-list-edit.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-workflow', - 4 => 'javelin-fx', - 5 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-list-edit.js', - ), - 'javelin-behavior-maniphest-subpriority-editor' => - array( - 'uri' => '/res/1fa4961f/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-workflow', - 4 => 'phabricator-draggable-list', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', - ), - 'javelin-behavior-maniphest-transaction-controls' => - array( - 'uri' => '/res/e8498688/rsrc/js/application/maniphest/behavior-transaction-controls.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'phabricator-prefab', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js', - ), - 'javelin-behavior-maniphest-transaction-expand' => - array( - 'uri' => '/res/966410de/rsrc/js/application/maniphest/behavior-transaction-expand.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-workflow', - 3 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-expand.js', - ), - 'javelin-behavior-maniphest-transaction-preview' => - array( - 'uri' => '/res/9447a3f9/rsrc/js/application/maniphest/behavior-transaction-preview.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-json', - 4 => 'javelin-stratcom', - 5 => 'phabricator-shaped-request', - ), - 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-preview.js', - ), - 'javelin-behavior-owners-path-editor' => - array( - 'uri' => '/res/9cf78ffc/rsrc/js/application/owners/owners-path-editor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'owners-path-editor', - 1 => 'javelin-behavior', - ), - 'disk' => '/rsrc/js/application/owners/owners-path-editor.js', - ), - 'javelin-behavior-persona-login' => - array( - 'uri' => '/res/128fdf56/rsrc/js/application/auth/behavior-persona-login.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-resource', - 2 => 'javelin-stratcom', - 3 => 'javelin-workflow', - 4 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/auth/behavior-persona-login.js', - ), - 'javelin-behavior-phabricator-active-nav' => - array( - 'uri' => '/res/9c8d3df8/rsrc/js/core/behavior-active-nav.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-vector', - 3 => 'javelin-dom', - 4 => 'javelin-uri', - ), - 'disk' => '/rsrc/js/core/behavior-active-nav.js', - ), - 'javelin-behavior-phabricator-autofocus' => - array( - 'uri' => '/res/bf92b8d6/rsrc/js/core/behavior-autofocus.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-autofocus.js', - ), - 'javelin-behavior-phabricator-busy-example' => - array( - 'uri' => '/res/dbe12f2f/rsrc/js/application/uiexample/busy-example.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'phabricator-busy', - 1 => 'javelin-behavior', - ), - 'disk' => '/rsrc/js/application/uiexample/busy-example.js', - ), - 'javelin-behavior-phabricator-file-tree' => - array( - 'uri' => '/res/e5bf93df/rsrc/js/core/behavior-file-tree.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'phabricator-keyboard-shortcut', - 2 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/core/behavior-file-tree.js', - ), - 'javelin-behavior-phabricator-gesture' => - array( - 'uri' => '/res/16e1e77c/rsrc/js/core/behavior-gesture.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-behavior-device', - 2 => 'javelin-stratcom', - 3 => 'javelin-vector', - 4 => 'javelin-dom', - 5 => 'javelin-magical-init', - ), - 'disk' => '/rsrc/js/core/behavior-gesture.js', - ), - 'javelin-behavior-phabricator-gesture-example' => - array( - 'uri' => '/res/91d1e7f2/rsrc/js/application/uiexample/gesture-example.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-stratcom', - 1 => 'javelin-behavior', - 2 => 'javelin-vector', - 3 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/gesture-example.js', - ), - 'javelin-behavior-phabricator-hovercards' => - array( - 'uri' => '/res/4fe6b436/rsrc/js/core/behavior-hovercard.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-behavior-device', - 2 => 'javelin-stratcom', - 3 => 'javelin-vector', - 4 => 'phabricator-hovercard', - ), - 'disk' => '/rsrc/js/core/behavior-hovercard.js', - ), - 'javelin-behavior-phabricator-keyboard-pager' => - array( - 'uri' => '/res/6a5445b8/rsrc/js/core/behavior-keyboard-pager.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-uri', - 2 => 'phabricator-keyboard-shortcut', - ), - 'disk' => '/rsrc/js/core/behavior-keyboard-pager.js', - ), - 'javelin-behavior-phabricator-keyboard-shortcuts' => - array( - 'uri' => '/res/b971e713/rsrc/js/core/behavior-keyboard-shortcuts.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-workflow', - 2 => 'javelin-json', - 3 => 'javelin-dom', - 4 => 'phabricator-keyboard-shortcut', - ), - 'disk' => '/rsrc/js/core/behavior-keyboard-shortcuts.js', - ), - 'javelin-behavior-phabricator-line-linker' => - array( - 'uri' => '/res/1cefdb6a/rsrc/js/core/behavior-line-linker.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-history', - ), - 'disk' => '/rsrc/js/core/behavior-line-linker.js', - ), - 'javelin-behavior-phabricator-nav' => - array( - 'uri' => '/res/afabcf16/rsrc/js/core/behavior-phabricator-nav.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-behavior-device', - 2 => 'javelin-stratcom', - 3 => 'javelin-dom', - 4 => 'javelin-magical-init', - 5 => 'javelin-vector', - 6 => 'javelin-request', - 7 => 'javelin-util', - ), - 'disk' => '/rsrc/js/core/behavior-phabricator-nav.js', - ), - 'javelin-behavior-phabricator-notification-example' => - array( - 'uri' => '/res/7c50cefd/rsrc/js/application/uiexample/notification-example.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'phabricator-notification', - 1 => 'javelin-stratcom', - 2 => 'javelin-behavior', - ), - 'disk' => '/rsrc/js/application/uiexample/notification-example.js', - ), - 'javelin-behavior-phabricator-object-selector' => - array( - 'uri' => '/res/461f95f7/rsrc/js/core/behavior-object-selector.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-request', - 3 => 'javelin-util', - ), - 'disk' => '/rsrc/js/core/behavior-object-selector.js', - ), - 'javelin-behavior-phabricator-oncopy' => - array( - 'uri' => '/res/cd3a9345/rsrc/js/core/behavior-oncopy.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-oncopy.js', - ), - 'javelin-behavior-phabricator-remarkup-assist' => - array( - 'uri' => '/res/4153e95f/rsrc/js/core/behavior-phabricator-remarkup-assist.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'phabricator-phtize', - 4 => 'phabricator-textareautils', - 5 => 'javelin-workflow', - 6 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/core/behavior-phabricator-remarkup-assist.js', - ), - 'javelin-behavior-phabricator-reveal-content' => - array( - 'uri' => '/res/fef525ef/rsrc/js/core/behavior-reveal-content.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-reveal-content.js', - ), - 'javelin-behavior-phabricator-search-typeahead' => - array( - 'uri' => '/res/409d9567/rsrc/js/core/behavior-search-typeahead.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-typeahead-ondemand-source', - 2 => 'javelin-typeahead', - 3 => 'javelin-dom', - 4 => 'javelin-uri', - 5 => 'javelin-util', - 6 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/js/core/behavior-search-typeahead.js', - ), - 'javelin-behavior-phabricator-tooltips' => - array( - 'uri' => '/res/a0ac5320/rsrc/js/core/behavior-tooltip.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-behavior-device', - 2 => 'javelin-stratcom', - 3 => 'phabricator-tooltip', - ), - 'disk' => '/rsrc/js/core/behavior-tooltip.js', - ), - 'javelin-behavior-phabricator-transaction-comment-form' => - array( - 'uri' => '/res/3c8d3c10/rsrc/js/application/transactions/behavior-transaction-comment-form.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-fx', - 4 => 'javelin-request', - 5 => 'phabricator-shaped-request', - ), - 'disk' => '/rsrc/js/application/transactions/behavior-transaction-comment-form.js', - ), - 'javelin-behavior-phabricator-transaction-list' => - array( - 'uri' => '/res/f05b3c6b/rsrc/js/application/transactions/behavior-transaction-list.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-workflow', - 3 => 'javelin-dom', - 4 => 'javelin-fx', - ), - 'disk' => '/rsrc/js/application/transactions/behavior-transaction-list.js', - ), - 'javelin-behavior-phabricator-watch-anchor' => - array( - 'uri' => '/res/69a90817/rsrc/js/core/behavior-watch-anchor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/core/behavior-watch-anchor.js', - ), - 'javelin-behavior-phame-post-preview' => - array( - 'uri' => '/res/181d1cbe/rsrc/js/application/phame/phame-post-preview.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'phabricator-shaped-request', - ), - 'disk' => '/rsrc/js/application/phame/phame-post-preview.js', - ), - 'javelin-behavior-pholio-mock-edit' => - array( - 'uri' => '/res/1fd14497/rsrc/js/application/pholio/behavior-pholio-mock-edit.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-workflow', - 4 => 'phabricator-phtize', - 5 => 'phabricator-drag-and-drop-file-upload', - 6 => 'phabricator-draggable-list', - ), - 'disk' => '/rsrc/js/application/pholio/behavior-pholio-mock-edit.js', - ), - 'javelin-behavior-pholio-mock-view' => - array( - 'uri' => '/res/f9588dcf/rsrc/js/application/pholio/behavior-pholio-mock-view.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - 3 => 'javelin-dom', - 4 => 'javelin-vector', - 5 => 'javelin-magical-init', - 6 => 'javelin-request', - 7 => 'javelin-history', - 8 => 'javelin-workflow', - 9 => 'javelin-mask', - 10 => 'javelin-behavior-device', - 11 => 'phabricator-keyboard-shortcut', - ), - 'disk' => '/rsrc/js/application/pholio/behavior-pholio-mock-view.js', - ), - 'javelin-behavior-phui-object-box-tabs' => - array( - 'uri' => '/res/c2318be8/rsrc/js/phui/behavior-phui-object-box-tabs.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/phui/behavior-phui-object-box-tabs.js', - ), - 'javelin-behavior-policy-control' => - array( - 'uri' => '/res/ce9f54c8/rsrc/js/application/policy/behavior-policy-control.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'phabricator-dropdown-menu', - 4 => 'phabricator-menu-item', - 5 => 'javelin-workflow', - ), - 'disk' => '/rsrc/js/application/policy/behavior-policy-control.js', - ), - 'javelin-behavior-policy-rule-editor' => - array( - 'uri' => '/res/4665236c/rsrc/js/application/policy/behavior-policy-rule-editor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'multirow-row-manager', - 2 => 'javelin-dom', - 3 => 'javelin-util', - 4 => 'phabricator-prefab', - 5 => 'javelin-tokenizer', - 6 => 'javelin-typeahead', - 7 => 'javelin-typeahead-preloaded-source', - 8 => 'javelin-json', - ), - 'disk' => '/rsrc/js/application/policy/behavior-policy-rule-editor.js', - ), - 'javelin-behavior-ponder-votebox' => - array( - 'uri' => '/res/c28daa12/rsrc/js/application/ponder/behavior-votebox.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-stratcom', - 4 => 'javelin-request', - ), - 'disk' => '/rsrc/js/application/ponder/behavior-votebox.js', - ), - 'javelin-behavior-project-create' => - array( - 'uri' => '/res/e91f3f8f/rsrc/js/application/projects/behavior-project-create.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-workflow', - ), - 'disk' => '/rsrc/js/application/projects/behavior-project-create.js', - ), - 'javelin-behavior-refresh-csrf' => - array( - 'uri' => '/res/6c54100f/rsrc/js/core/behavior-refresh-csrf.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-request', - 1 => 'javelin-behavior', - 2 => 'javelin-dom', - 3 => 'phabricator-busy', - ), - 'disk' => '/rsrc/js/core/behavior-refresh-csrf.js', - ), - 'javelin-behavior-releeph-preview-branch' => - array( - 'uri' => '/res/f694854d/rsrc/js/application/releeph/releeph-preview-branch.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-uri', - 3 => 'javelin-request', - ), - 'disk' => '/rsrc/js/application/releeph/releeph-preview-branch.js', - ), - 'javelin-behavior-releeph-request-state-change' => - array( - 'uri' => '/res/07ecde0c/rsrc/js/application/releeph/releeph-request-state-change.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-request', - 4 => 'phabricator-keyboard-shortcut', - 5 => 'phabricator-notification', - ), - 'disk' => '/rsrc/js/application/releeph/releeph-request-state-change.js', - ), - 'javelin-behavior-releeph-request-typeahead' => - array( - 'uri' => '/res/2c2350a0/rsrc/js/application/releeph/releeph-request-typeahead.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-typeahead', - 3 => 'javelin-typeahead-ondemand-source', - 4 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/releeph/releeph-request-typeahead.js', - ), - 'javelin-behavior-remarkup-preview' => - array( - 'uri' => '/res/6ec98508/rsrc/js/core/behavior-remarkup-preview.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'phabricator-shaped-request', - ), - 'disk' => '/rsrc/js/core/behavior-remarkup-preview.js', - ), - 'javelin-behavior-repository-crossreference' => - array( - 'uri' => '/res/d3f9d50b/rsrc/js/application/repository/repository-crossreference.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-uri', - ), - 'disk' => '/rsrc/js/application/repository/repository-crossreference.js', - ), - 'javelin-behavior-search-reorder-queries' => - array( - 'uri' => '/res/9864b481/rsrc/js/application/search/behavior-reorder-queries.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-workflow', - 3 => 'javelin-dom', - 4 => 'phabricator-draggable-list', - ), - 'disk' => '/rsrc/js/application/search/behavior-reorder-queries.js', - ), - 'javelin-behavior-select-on-click' => - array( - 'uri' => '/res/f021b754/rsrc/js/core/behavior-select-on-click.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-select-on-click.js', - ), - 'javelin-behavior-slowvote-embed' => - array( - 'uri' => '/res/8e85e20d/rsrc/js/application/slowvote/behavior-slowvote-embed.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-request', - 2 => 'javelin-stratcom', - 3 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/slowvote/behavior-slowvote-embed.js', - ), - 'javelin-behavior-stripe-payment-form' => - array( - 'uri' => '/res/c1a12d77/rsrc/js/application/phortune/behavior-stripe-payment-form.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'phortune-credit-card-form', - ), - 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', - ), - 'javelin-behavior-test-payment-form' => - array( - 'uri' => '/res/a8fe8616/rsrc/js/application/phortune/behavior-test-payment-form.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'phortune-credit-card-form', - ), - 'disk' => '/rsrc/js/application/phortune/behavior-test-payment-form.js', - ), - 'javelin-behavior-toggle-class' => - array( - 'uri' => '/res/79921b7f/rsrc/js/core/behavior-toggle-class.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-toggle-class.js', - ), - 'javelin-behavior-view-placeholder' => - array( - 'uri' => '/res/6abdb85b/rsrc/externals/javelin/ext/view/ViewPlaceholder.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-view-renderer', - 3 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/ext/view/ViewPlaceholder.js', - ), - 'javelin-behavior-workflow' => - array( - 'uri' => '/res/144d3196/rsrc/js/core/behavior-workflow.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-workflow', - 3 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/behavior-workflow.js', - ), - 'javelin-color' => - array( - 'uri' => '/res/f17034de/rsrc/externals/javelin/ext/fx/Color.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/ext/fx/Color.js', - ), - 'javelin-cookie' => - array( - 'uri' => '/res/ee0d399f/rsrc/externals/javelin/lib/Cookie.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/lib/Cookie.js', - ), - 'javelin-dom' => - array( - 'uri' => '/res/580c0aeb/rsrc/externals/javelin/lib/DOM.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-magical-init', - 1 => 'javelin-install', - 2 => 'javelin-util', - 3 => 'javelin-vector', - 4 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/externals/javelin/lib/DOM.js', - ), - 'javelin-dynval' => - array( - 'uri' => '/res/ea6f2a9d/rsrc/externals/javelin/ext/reactor/core/DynVal.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-reactornode', - 2 => 'javelin-util', - 3 => 'javelin-reactor', - ), - 'disk' => '/rsrc/externals/javelin/ext/reactor/core/DynVal.js', - ), - 'javelin-event' => - array( - 'uri' => '/res/5f70f4d0/rsrc/externals/javelin/core/Event.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/core/Event.js', - ), - 'javelin-fx' => - array( - 'uri' => '/res/23fb3d44/rsrc/externals/javelin/ext/fx/FX.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-color', - 1 => 'javelin-install', - 2 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/fx/FX.js', - ), - 'javelin-history' => - array( - 'uri' => '/res/6c084b09/rsrc/externals/javelin/lib/History.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-stratcom', - 1 => 'javelin-install', - 2 => 'javelin-uri', - 3 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/lib/History.js', - ), - 'javelin-install' => - array( - 'uri' => '/res/904356c0/rsrc/externals/javelin/core/install.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-util', - 1 => 'javelin-magical-init', - ), - 'disk' => '/rsrc/externals/javelin/core/install.js', - ), - 'javelin-json' => - array( - 'uri' => '/res/cf83e72c/rsrc/externals/javelin/lib/JSON.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/lib/JSON.js', - ), - 'javelin-magical-init' => - array( - 'uri' => '/res/374d1f02/rsrc/externals/javelin/core/init.js', - 'type' => 'js', - 'requires' => - array( - ), - 'disk' => '/rsrc/externals/javelin/core/init.js', - ), - 'javelin-mask' => - array( - 'uri' => '/res/465cf513/rsrc/externals/javelin/lib/Mask.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/externals/javelin/lib/Mask.js', - ), - 'javelin-reactor' => - array( - 'uri' => '/res/c05f2658/rsrc/externals/javelin/ext/reactor/core/Reactor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/reactor/core/Reactor.js', - ), - 'javelin-reactor-dom' => - array( - 'uri' => '/res/5e03117e/rsrc/externals/javelin/ext/reactor/dom/RDOM.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-dom', - 1 => 'javelin-dynval', - 2 => 'javelin-reactor', - 3 => 'javelin-reactornode', - 4 => 'javelin-install', - 5 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/reactor/dom/RDOM.js', - ), - 'javelin-reactor-node-calmer' => - array( - 'uri' => '/res/a93dd6b6/rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-reactor', - 2 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js', - ), - 'javelin-reactornode' => - array( - 'uri' => '/res/4eac475b/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-reactor', - 2 => 'javelin-util', - 3 => 'javelin-reactor-node-calmer', - ), - 'disk' => '/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js', - ), - 'javelin-request' => - array( - 'uri' => '/res/687bdcfc/rsrc/externals/javelin/lib/Request.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-stratcom', - 2 => 'javelin-util', - 3 => 'javelin-behavior', - 4 => 'javelin-json', - 5 => 'javelin-dom', - 6 => 'javelin-resource', - ), - 'disk' => '/rsrc/externals/javelin/lib/Request.js', - ), - 'javelin-resource' => - array( - 'uri' => '/res/33a3bb57/rsrc/externals/javelin/lib/Resource.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-util', - 1 => 'javelin-uri', - 2 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/lib/Resource.js', - ), - 'javelin-stratcom' => - array( - 'uri' => '/res/714946e7/rsrc/externals/javelin/core/Stratcom.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-event', - 2 => 'javelin-util', - 3 => 'javelin-magical-init', - ), - 'disk' => '/rsrc/externals/javelin/core/Stratcom.js', - ), - 'javelin-tokenizer' => - array( - 'uri' => '/res/cddb70f3/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-dom', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - 3 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js', - ), - 'javelin-typeahead' => - array( - 'uri' => '/res/fd79f758/rsrc/externals/javelin/lib/control/typeahead/Typeahead.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-vector', - 3 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/Typeahead.js', - ), - 'javelin-typeahead-composite-source' => - array( - 'uri' => '/res/487b3da2/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-typeahead-source', - 2 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js', - ), - 'javelin-typeahead-normalizer' => - array( - 'uri' => '/res/5a4bd979/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js', - ), - 'javelin-typeahead-ondemand-source' => - array( - 'uri' => '/res/92286a21/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-request', - 3 => 'javelin-typeahead-source', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js', - ), - 'javelin-typeahead-preloaded-source' => - array( - 'uri' => '/res/147900c7/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-request', - 3 => 'javelin-typeahead-source', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js', - ), - 'javelin-typeahead-source' => - array( - 'uri' => '/res/13289259/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-typeahead-normalizer', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js', - ), - 'javelin-typeahead-static-source' => - array( - 'uri' => '/res/bb0a5173/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-typeahead-source', - ), - 'disk' => '/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js', - ), - 'javelin-uri' => - array( - 'uri' => '/res/75aa4597/rsrc/externals/javelin/lib/URI.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - ), - 'disk' => '/rsrc/externals/javelin/lib/URI.js', - ), - 'javelin-util' => - array( - 'uri' => '/res/90222113/rsrc/externals/javelin/core/util.js', - 'type' => 'js', - 'requires' => - array( - ), - 'disk' => '/rsrc/externals/javelin/core/util.js', - ), - 'javelin-vector' => - array( - 'uri' => '/res/58ea3dd7/rsrc/externals/javelin/lib/Vector.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-event', - ), - 'disk' => '/rsrc/externals/javelin/lib/Vector.js', - ), - 'javelin-view' => - array( - 'uri' => '/res/38daaec0/rsrc/externals/javelin/ext/view/View.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/view/View.js', - ), - 'javelin-view-html' => - array( - 'uri' => '/res/0d225e8c/rsrc/externals/javelin/ext/view/HTMLView.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-view-visitor', - 3 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/view/HTMLView.js', - ), - 'javelin-view-interpreter' => - array( - 'uri' => '/res/b0c07f96/rsrc/externals/javelin/ext/view/ViewInterpreter.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-view', - 1 => 'javelin-install', - 2 => 'javelin-dom', - ), - 'disk' => '/rsrc/externals/javelin/ext/view/ViewInterpreter.js', - ), - 'javelin-view-renderer' => - array( - 'uri' => '/res/fe0d2f60/rsrc/externals/javelin/ext/view/ViewRenderer.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/view/ViewRenderer.js', - ), - 'javelin-view-visitor' => - array( - 'uri' => '/res/b1606cec/rsrc/externals/javelin/ext/view/ViewVisitor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - ), - 'disk' => '/rsrc/externals/javelin/ext/view/ViewVisitor.js', - ), - 'javelin-workflow' => - array( - 'uri' => '/res/09a97dda/rsrc/externals/javelin/lib/Workflow.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-stratcom', - 1 => 'javelin-request', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - 4 => 'javelin-install', - 5 => 'javelin-util', - 6 => 'javelin-mask', - 7 => 'javelin-uri', - ), - 'disk' => '/rsrc/externals/javelin/lib/Workflow.js', - ), - 'lightbox-attachment-css' => - array( - 'uri' => '/res/4657e15d/rsrc/css/aphront/lightbox-attachment.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/lightbox-attachment.css', - ), - 'maniphest-batch-editor' => - array( - 'uri' => '/res/fb15d744/rsrc/css/application/maniphest/batch-editor.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/maniphest/batch-editor.css', - ), - 'maniphest-report-css' => - array( - 'uri' => '/res/2e633fcf/rsrc/css/application/maniphest/report.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/maniphest/report.css', - ), - 'maniphest-task-edit-css' => - array( - 'uri' => '/res/f5926f5a/rsrc/css/application/maniphest/task-edit.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/maniphest/task-edit.css', - ), - 'maniphest-task-summary-css' => - array( - 'uri' => '/res/5de3b188/rsrc/css/application/maniphest/task-summary.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/maniphest/task-summary.css', - ), - 'multirow-row-manager' => - array( - 'uri' => '/res/408fae4f/rsrc/js/core/MultirowRowManager.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'javelin-util', - ), - 'disk' => '/rsrc/js/core/MultirowRowManager.js', - ), - 'owners-path-editor' => - array( - 'uri' => '/res/29b68354/rsrc/js/application/owners/OwnersPathEditor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'multirow-row-manager', - 1 => 'javelin-install', - 2 => 'path-typeahead', - 3 => 'javelin-dom', - 4 => 'javelin-util', - 5 => 'phabricator-prefab', - ), - 'disk' => '/rsrc/js/application/owners/OwnersPathEditor.js', - ), - 'owners-path-editor-css' => - array( - 'uri' => '/res/c91cc4a8/rsrc/css/application/owners/owners-path-editor.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/owners/owners-path-editor.css', - ), - 'paste-css' => - array( - 'uri' => '/res/216fbfe9/rsrc/css/application/paste/paste.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/paste/paste.css', - ), - 'path-typeahead' => - array( - 'uri' => '/res/50246fb6/rsrc/js/application/herald/PathTypeahead.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-typeahead', - 2 => 'javelin-dom', - 3 => 'javelin-request', - 4 => 'javelin-typeahead-ondemand-source', - 5 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/herald/PathTypeahead.js', - ), - 'people-profile-css' => - array( - 'uri' => '/res/f1da102e/rsrc/css/application/people/people-profile.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/people/people-profile.css', - ), - 'phabricator-action-header-view-css' => - array( - 'uri' => '/res/cd8b4a61/rsrc/css/layout/phabricator-action-header-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-action-header-view.css', - ), - 'phabricator-action-list-view-css' => - array( - 'uri' => '/res/2dce4556/rsrc/css/layout/phabricator-action-list-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-action-list-view.css', - ), - 'phabricator-application-launch-view-css' => - array( - 'uri' => '/res/21a67228/rsrc/css/application/base/phabricator-application-launch-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/base/phabricator-application-launch-view.css', - ), - 'phabricator-busy' => - array( - 'uri' => '/res/083c11d2/rsrc/js/core/Busy.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-fx', - ), - 'disk' => '/rsrc/js/core/Busy.js', - ), - 'phabricator-chatlog-css' => - array( - 'uri' => '/res/cf9b0aa7/rsrc/css/application/chatlog/chatlog.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/chatlog/chatlog.css', - ), - 'phabricator-content-source-view-css' => - array( - 'uri' => '/res/f15a9527/rsrc/css/application/contentsource/content-source-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/contentsource/content-source-view.css', - ), - 'phabricator-core-css' => - array( - 'uri' => '/res/9e767fb1/rsrc/css/core/core.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/core/core.css', - ), - 'phabricator-countdown-css' => - array( - 'uri' => '/res/d85bdfd5/rsrc/css/application/countdown/timer.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/countdown/timer.css', - ), - 'phabricator-crumbs-view-css' => - array( - 'uri' => '/res/f3c7068b/rsrc/css/layout/phabricator-crumbs-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-crumbs-view.css', - ), - 'phabricator-drag-and-drop-file-upload' => - array( - 'uri' => '/res/396d3b3b/rsrc/js/core/DragAndDropFileUpload.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-request', - 3 => 'javelin-dom', - 4 => 'javelin-uri', - 5 => 'phabricator-file-upload', - ), - 'disk' => '/rsrc/js/core/DragAndDropFileUpload.js', - ), - 'phabricator-draggable-list' => - array( - 'uri' => '/res/75c556db/rsrc/js/core/DraggableList.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-util', - 4 => 'javelin-vector', - 5 => 'javelin-magical-init', - ), - 'disk' => '/rsrc/js/core/DraggableList.js', - ), - 'phabricator-dropdown-menu' => - array( - 'uri' => '/res/147ca011/rsrc/js/core/DropdownMenu.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - 4 => 'javelin-stratcom', - 5 => 'phabricator-menu-item', - ), - 'disk' => '/rsrc/js/core/DropdownMenu.js', - ), - 'phabricator-fatal-config-template-css' => - array( - 'uri' => '/res/6e1a8d22/rsrc/css/application/config/config-template.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/config/config-template.css', - ), - 'phabricator-feed-css' => - array( - 'uri' => '/res/e19633ed/rsrc/css/application/feed/feed.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/feed/feed.css', - ), - 'phabricator-file-upload' => - array( - 'uri' => '/res/c9605008/rsrc/js/core/FileUpload.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'phabricator-notification', - ), - 'disk' => '/rsrc/js/core/FileUpload.js', - ), - 'phabricator-filetree-view-css' => - array( - 'uri' => '/res/c912ed91/rsrc/css/layout/phabricator-filetree-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-filetree-view.css', - ), - 'phabricator-flag-css' => - array( - 'uri' => '/res/cdb5cb1b/rsrc/css/application/flag/flag.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/flag/flag.css', - ), - 'phabricator-hovercard' => - array( - 'uri' => '/res/7fb94260/rsrc/js/core/Hovercard.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-vector', - 3 => 'javelin-request', - 4 => 'javelin-uri', - ), - 'disk' => '/rsrc/js/core/Hovercard.js', - ), - 'phabricator-hovercard-view-css' => - array( - 'uri' => '/res/79c61f0e/rsrc/css/layout/phabricator-hovercard-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-hovercard-view.css', - ), - 'phabricator-jump-nav' => - array( - 'uri' => '/res/7db8cead/rsrc/css/application/directory/phabricator-jump-nav.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/directory/phabricator-jump-nav.css', - ), - 'phabricator-keyboard-shortcut' => - array( - 'uri' => '/res/44747afd/rsrc/js/core/KeyboardShortcut.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'phabricator-keyboard-shortcut-manager', - ), - 'disk' => '/rsrc/js/core/KeyboardShortcut.js', - ), - 'phabricator-keyboard-shortcut-manager' => - array( - 'uri' => '/res/bf9bc02a/rsrc/js/core/KeyboardShortcutManager.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - 3 => 'javelin-dom', - 4 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/core/KeyboardShortcutManager.js', - ), - 'phabricator-main-menu-view' => - array( - 'uri' => '/res/f45115e5/rsrc/css/application/base/main-menu-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/base/main-menu-view.css', - ), - 'phabricator-menu-item' => - array( - 'uri' => '/res/e810b0a1/rsrc/js/core/DropdownMenuItem.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/core/DropdownMenuItem.js', - ), - 'phabricator-nav-view-css' => - array( - 'uri' => '/res/37955b6a/rsrc/css/aphront/phabricator-nav-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/phabricator-nav-view.css', - ), - 'phabricator-notification' => - array( - 'uri' => '/res/0764da14/rsrc/js/core/Notification.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-util', - 4 => 'phabricator-notification-css', - ), - 'disk' => '/rsrc/js/core/Notification.js', - ), - 'phabricator-notification-css' => - array( - 'uri' => '/res/2b9cdac0/rsrc/css/aphront/notification.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/notification.css', - ), - 'phabricator-notification-menu-css' => - array( - 'uri' => '/res/c6b17cfb/rsrc/css/application/base/notification-menu.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/base/notification-menu.css', - ), - 'phabricator-object-list-view-css' => - array( - 'uri' => '/res/4f183668/rsrc/css/application/projects/phabricator-object-list-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/projects/phabricator-object-list-view.css', - ), - 'phabricator-object-selector-css' => - array( - 'uri' => '/res/20c94e28/rsrc/css/application/objectselector/object-selector.css', - 'type' => 'css', - 'requires' => - array( - 0 => 'aphront-dialog-view-css', - ), - 'disk' => '/rsrc/css/application/objectselector/object-selector.css', - ), - 'phabricator-phtize' => - array( - 'uri' => '/res/dc655a81/rsrc/js/core/phtize.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-util', - ), - 'disk' => '/rsrc/js/core/phtize.js', - ), - 'phabricator-prefab' => - array( - 'uri' => '/res/511859ca/rsrc/js/core/Prefab.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-typeahead', - 4 => 'javelin-tokenizer', - 5 => 'javelin-typeahead-preloaded-source', - 6 => 'javelin-typeahead-ondemand-source', - 7 => 'javelin-dom', - 8 => 'javelin-stratcom', - 9 => 'javelin-util', - ), - 'disk' => '/rsrc/js/core/Prefab.js', - ), - 'phabricator-profile-css' => - array( - 'uri' => '/res/c1f72695/rsrc/css/application/profile/profile-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/profile/profile-view.css', - ), - 'phabricator-project-tag-css' => - array( - 'uri' => '/res/535b8b7a/rsrc/css/application/projects/project-tag.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/projects/project-tag.css', - ), - 'phabricator-remarkup-css' => - array( - 'uri' => '/res/4c313572/rsrc/css/core/remarkup.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/core/remarkup.css', - ), - 'phabricator-search-results-css' => - array( - 'uri' => '/res/5407f3ea/rsrc/css/application/search/search-results.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/search/search-results.css', - ), - 'phabricator-settings-css' => - array( - 'uri' => '/res/fb9d017f/rsrc/css/application/settings/settings.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/settings/settings.css', - ), - 'phabricator-shaped-request' => - array( - 'uri' => '/res/d173af85/rsrc/js/core/ShapedRequest.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-request', - ), - 'disk' => '/rsrc/js/core/ShapedRequest.js', - ), - 'phabricator-side-menu-view-css' => - array( - 'uri' => '/res/e83fbe58/rsrc/css/layout/phabricator-side-menu-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-side-menu-view.css', - ), - 'phabricator-slowvote-css' => - array( - 'uri' => '/res/e61a54eb/rsrc/css/application/slowvote/slowvote.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/slowvote/slowvote.css', - ), - 'phabricator-source-code-view-css' => - array( - 'uri' => '/res/aee63670/rsrc/css/layout/phabricator-source-code-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-source-code-view.css', - ), - 'phabricator-standard-page-view' => - array( - 'uri' => '/res/eebd59cd/rsrc/css/application/base/standard-page-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/base/standard-page-view.css', - ), - 'phabricator-tag-view-css' => - array( - 'uri' => '/res/65ad2dc3/rsrc/css/layout/phabricator-tag-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-tag-view.css', - ), - 'phabricator-textareautils' => - array( - 'uri' => '/res/03c03e8b/rsrc/js/core/TextAreaUtils.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - ), - 'disk' => '/rsrc/js/core/TextAreaUtils.js', - ), - 'phabricator-timeline-view-css' => - array( - 'uri' => '/res/d139291d/rsrc/css/layout/phabricator-timeline-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/layout/phabricator-timeline-view.css', - ), - 'phabricator-tooltip' => - array( - 'uri' => '/res/a23bc887/rsrc/js/core/ToolTip.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - ), - 'disk' => '/rsrc/js/core/ToolTip.js', - ), - 'phabricator-transaction-view-css' => - array( - 'uri' => '/res/5e6237c6/rsrc/css/aphront/transaction.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/aphront/transaction.css', - ), - 'phabricator-ui-example-css' => - array( - 'uri' => '/res/376ab671/rsrc/css/application/uiexample/example.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/uiexample/example.css', - ), - 'phabricator-uiexample-javelin-view' => - array( - 'uri' => '/res/d42834b6/rsrc/js/application/uiexample/JavelinViewExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-view', - ), - 'disk' => '/rsrc/js/application/uiexample/JavelinViewExample.js', - ), - 'phabricator-uiexample-reactor-button' => - array( - 'uri' => '/res/6bfe4f05/rsrc/js/application/uiexample/ReactorButtonExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-dynval', - 4 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorButtonExample.js', - ), - 'phabricator-uiexample-reactor-checkbox' => - array( - 'uri' => '/res/3e8b30ac/rsrc/js/application/uiexample/ReactorCheckboxExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorCheckboxExample.js', - ), - 'phabricator-uiexample-reactor-focus' => - array( - 'uri' => '/res/d8f3b56e/rsrc/js/application/uiexample/ReactorFocusExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorFocusExample.js', - ), - 'phabricator-uiexample-reactor-input' => - array( - 'uri' => '/res/936352d9/rsrc/js/application/uiexample/ReactorInputExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-reactor-dom', - 2 => 'javelin-view-html', - 3 => 'javelin-view-interpreter', - 4 => 'javelin-view-renderer', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorInputExample.js', - ), - 'phabricator-uiexample-reactor-mouseover' => - array( - 'uri' => '/res/031a9f4f/rsrc/js/application/uiexample/ReactorMouseoverExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorMouseoverExample.js', - ), - 'phabricator-uiexample-reactor-radio' => - array( - 'uri' => '/res/208c58e3/rsrc/js/application/uiexample/ReactorRadioExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorRadioExample.js', - ), - 'phabricator-uiexample-reactor-select' => - array( - 'uri' => '/res/1b68a6db/rsrc/js/application/uiexample/ReactorSelectExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorSelectExample.js', - ), - 'phabricator-uiexample-reactor-sendclass' => - array( - 'uri' => '/res/00cb3131/rsrc/js/application/uiexample/ReactorSendClassExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorSendClassExample.js', - ), - 'phabricator-uiexample-reactor-sendproperties' => - array( - 'uri' => '/res/392f1e02/rsrc/js/application/uiexample/ReactorSendPropertiesExample.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-reactor-dom', - ), - 'disk' => '/rsrc/js/application/uiexample/ReactorSendPropertiesExample.js', - ), - 'phabricator-welcome-page' => - array( - 'uri' => '/res/c7e1ddad/rsrc/custom/css/phabricator-welcome-page.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/custom/css/phabricator-welcome-page.css', - ), - 'phabricator-zindex-css' => - array( - 'uri' => '/res/b443d508/rsrc/css/core/z-index.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/core/z-index.css', - ), - 'phame-css' => - array( - 'uri' => '/res/405e77b8/rsrc/css/application/phame/phame.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/phame/phame.css', - ), - 'pholio-css' => - array( - 'uri' => '/res/e64264c9/rsrc/css/application/pholio/pholio.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/pholio/pholio.css', - ), - 'pholio-edit-css' => - array( - 'uri' => '/res/04013652/rsrc/css/application/pholio/pholio-edit.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/pholio/pholio-edit.css', - ), - 'pholio-inline-comments-css' => - array( - 'uri' => '/res/67e2f18c/rsrc/css/application/pholio/pholio-inline-comments.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/pholio/pholio-inline-comments.css', - ), - 'phortune-credit-card-form' => - array( - 'uri' => '/res/bc948778/rsrc/js/application/phortune/phortune-credit-card-form.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-json', - 3 => 'javelin-workflow', - 4 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/phortune/phortune-credit-card-form.js', - ), - 'phortune-credit-card-form-css' => - array( - 'uri' => '/res/563c8c6d/rsrc/css/application/phortune/phortune-credit-card-form.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/phortune/phortune-credit-card-form.css', - ), - 'phrequent-css' => - array( - 'uri' => '/res/e2f09149/rsrc/css/application/phrequent/phrequent.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/phrequent/phrequent.css', - ), - 'phriction-document-css' => - array( - 'uri' => '/res/97cbd7c6/rsrc/css/application/phriction/phriction-document-css.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/phriction/phriction-document-css.css', - ), - 'phui-box-css' => - array( - 'uri' => '/res/cd1b04cf/rsrc/css/phui/phui-box.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-box.css', - ), - 'phui-button-css' => - array( - 'uri' => '/res/abf52ae9/rsrc/css/phui/phui-button.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-button.css', - ), - 'phui-document-view-css' => - array( - 'uri' => '/res/cac7a825/rsrc/css/phui/phui-document.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-document.css', - ), - 'phui-feed-story-css' => - array( - 'uri' => '/res/8f28c686/rsrc/css/phui/phui-feed-story.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-feed-story.css', - ), - 'phui-form-css' => - array( - 'uri' => '/res/29b48d06/rsrc/css/phui/phui-form.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-form.css', - ), - 'phui-form-view-css' => - array( - 'uri' => '/res/18392610/rsrc/css/phui/phui-form-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-form-view.css', - ), - 'phui-header-view-css' => - array( - 'uri' => '/res/d6ca0939/rsrc/css/phui/phui-header-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-header-view.css', - ), - 'phui-icon-view-css' => - array( - 'uri' => '/res/28fb5ae5/rsrc/css/phui/phui-icon.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-icon.css', - ), - 'phui-info-panel-css' => - array( - 'uri' => '/res/e0ba8d04/rsrc/css/phui/phui-info-panel.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-info-panel.css', - ), - 'phui-list-view-css' => - array( - 'uri' => '/res/fbf42225/rsrc/css/phui/phui-list.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-list.css', - ), - 'phui-object-box-css' => - array( - 'uri' => '/res/8504279f/rsrc/css/phui/phui-object-box.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-object-box.css', - ), - 'phui-object-item-list-view-css' => - array( - 'uri' => '/res/c3a0ea74/rsrc/css/phui/phui-object-item-list-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-object-item-list-view.css', - ), - 'phui-pinboard-view-css' => - array( - 'uri' => '/res/f791ea99/rsrc/css/phui/phui-pinboard-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-pinboard-view.css', - ), - 'phui-property-list-view-css' => - array( - 'uri' => '/res/e1e6674b/rsrc/css/phui/phui-property-list-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-property-list-view.css', - ), - 'phui-remarkup-preview-css' => - array( - 'uri' => '/res/50fa4178/rsrc/css/phui/phui-remarkup-preview.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-remarkup-preview.css', - ), - 'phui-spacing-css' => - array( - 'uri' => '/res/28891fd3/rsrc/css/phui/phui-spacing.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-spacing.css', - ), - 'phui-status-list-view-css' => - array( - 'uri' => '/res/868f8a95/rsrc/css/phui/phui-status.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-status.css', - ), - 'phui-text-css' => - array( - 'uri' => '/res/63e53cac/rsrc/css/phui/phui-text.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-text.css', - ), - 'phui-workboard-view-css' => - array( - 'uri' => '/res/908b64b3/rsrc/css/phui/phui-workboard-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-workboard-view.css', - ), - 'phui-workpanel-view-css' => - array( - 'uri' => '/res/0b9a41d8/rsrc/css/phui/phui-workpanel-view.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/phui/phui-workpanel-view.css', - ), - 'policy-css' => - array( - 'uri' => '/res/51325bff/rsrc/css/application/policy/policy.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/policy/policy.css', - ), - 'policy-edit-css' => - array( - 'uri' => '/res/1e2a2b5e/rsrc/css/application/policy/policy-edit.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/policy/policy-edit.css', - ), - 'ponder-comment-table-css' => - array( - 'uri' => '/res/4aa4b865/rsrc/css/application/ponder/comments.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/ponder/comments.css', - ), - 'ponder-feed-view-css' => - array( - 'uri' => '/res/cab09075/rsrc/css/application/ponder/feed.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/ponder/feed.css', - ), - 'ponder-post-css' => - array( - 'uri' => '/res/013b9e2c/rsrc/css/application/ponder/post.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/ponder/post.css', - ), - 'ponder-vote-css' => - array( - 'uri' => '/res/6bbe8538/rsrc/css/application/ponder/vote.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/ponder/vote.css', - ), - 'raphael-core' => - array( - 'uri' => '/res/5dc5e17c/rsrc/externals/raphael/raphael.js', - 'type' => 'js', - 'requires' => - array( - ), - 'disk' => '/rsrc/externals/raphael/raphael.js', - ), - 'raphael-g' => - array( - 'uri' => '/res/229b89a1/rsrc/externals/raphael/g.raphael.js', - 'type' => 'js', - 'requires' => - array( - ), - 'disk' => '/rsrc/externals/raphael/g.raphael.js', - ), - 'raphael-g-line' => - array( - 'uri' => '/res/96da30f7/rsrc/externals/raphael/g.raphael.line.js', - 'type' => 'js', - 'requires' => - array( - ), - 'disk' => '/rsrc/externals/raphael/g.raphael.line.js', - ), - 'releeph-branch' => - array( - 'uri' => '/res/6ad6420d/rsrc/css/application/releeph/releeph-branch.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-branch.css', - ), - 'releeph-colors' => - array( - 'uri' => '/res/dff4b26a/rsrc/css/application/releeph/releeph-colors.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-colors.css', - ), - 'releeph-core' => - array( - 'uri' => '/res/dad04eff/rsrc/css/application/releeph/releeph-core.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-core.css', - ), - 'releeph-intents' => - array( - 'uri' => '/res/4e73e9dd/rsrc/css/application/releeph/releeph-intents.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-intents.css', - ), - 'releeph-preview-branch' => - array( - 'uri' => '/res/65e5dece/rsrc/css/application/releeph/releeph-preview-branch.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-preview-branch.css', - ), - 'releeph-project' => - array( - 'uri' => '/res/b9376e59/rsrc/css/application/releeph/releeph-project.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-project.css', - ), - 'releeph-request-differential-create-dialog' => - array( - 'uri' => '/res/4df30ce1/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-request-differential-create-dialog.css', - ), - 'releeph-request-typeahead-css' => - array( - 'uri' => '/res/9c9a1acf/rsrc/css/application/releeph/releeph-request-typeahead.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-request-typeahead.css', - ), - 'releeph-status' => - array( - 'uri' => '/res/588529df/rsrc/css/application/releeph/releeph-status.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/releeph/releeph-status.css', - ), - 'setup-issue-css' => - array( - 'uri' => '/res/135c19ed/rsrc/css/application/config/setup-issue.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/config/setup-issue.css', - ), - 'sprite-actions-css' => - array( - 'uri' => '/res/bd43efa8/rsrc/css/sprite-actions.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-actions.css', - ), - 'sprite-apps-css' => - array( - 'uri' => '/res/774f4bad/rsrc/css/sprite-apps.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-apps.css', - ), - 'sprite-apps-large-css' => - array( - 'uri' => '/res/b547fab1/rsrc/css/sprite-apps-large.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-apps-large.css', - ), - 'sprite-apps-xlarge-css' => - array( - 'uri' => '/res/33a8e644/rsrc/css/sprite-apps-xlarge.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-apps-xlarge.css', - ), - 'sprite-conpherence-css' => - array( - 'uri' => '/res/f6793453/rsrc/css/sprite-conpherence.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-conpherence.css', - ), - 'sprite-docs-css' => - array( - 'uri' => '/res/b32f93bc/rsrc/css/sprite-docs.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-docs.css', - ), - 'sprite-gradient-css' => - array( - 'uri' => '/res/e31d9063/rsrc/css/sprite-gradient.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-gradient.css', - ), - 'sprite-icons-css' => - array( - 'uri' => '/res/cb634e79/rsrc/css/sprite-icons.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-icons.css', - ), - 'sprite-login-css' => - array( - 'uri' => '/res/48dc427d/rsrc/css/sprite-login.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-login.css', - ), - 'sprite-menu-css' => - array( - 'uri' => '/res/05b9ad87/rsrc/css/sprite-menu.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-menu.css', - ), - 'sprite-minicons-css' => - array( - 'uri' => '/res/2dba70cd/rsrc/css/sprite-minicons.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-minicons.css', - ), - 'sprite-payments-css' => - array( - 'uri' => '/res/876697b6/rsrc/css/sprite-payments.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-payments.css', - ), - 'sprite-projects-css' => - array( - 'uri' => '/res/40eacbfb/rsrc/css/sprite-projects.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-projects.css', - ), - 'sprite-status-css' => - array( - 'uri' => '/res/f08fd1e1/rsrc/css/sprite-status.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-status.css', - ), - 'sprite-tokens-css' => - array( - 'uri' => '/res/eeca7cf1/rsrc/css/sprite-tokens.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/sprite-tokens.css', - ), - 'syntax-highlighting-css' => - array( - 'uri' => '/res/db7c0e13/rsrc/css/core/syntax.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/core/syntax.css', - ), - 'tokens-css' => - array( - 'uri' => '/res/bbddf548/rsrc/css/application/tokens/tokens.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/tokens/tokens.css', - ), -), array( - 'packages' => - array( - '650e555b' => - array( - 'name' => 'core.pkg.css', - 'symbols' => - array( - 0 => 'phabricator-core-css', - 1 => 'phabricator-zindex-css', - 2 => 'phui-button-css', - 3 => 'phabricator-standard-page-view', - 4 => 'aphront-dialog-view-css', - 5 => 'phui-form-view-css', - 6 => 'aphront-panel-view-css', - 7 => 'aphront-table-view-css', - 8 => 'aphront-tokenizer-control-css', - 9 => 'aphront-typeahead-control-css', - 10 => 'aphront-list-filter-view-css', - 11 => 'phabricator-jump-nav', - 12 => 'phabricator-welcome-page', - 13 => 'phabricator-remarkup-css', - 14 => 'syntax-highlighting-css', - 15 => 'aphront-pager-view-css', - 16 => 'phabricator-transaction-view-css', - 17 => 'aphront-tooltip-css', - 18 => 'phabricator-flag-css', - 19 => 'aphront-error-view-css', - 20 => 'sprite-icons-css', - 21 => 'sprite-gradient-css', - 22 => 'sprite-menu-css', - 23 => 'sprite-apps-large-css', - 24 => 'sprite-status-css', - 25 => 'phabricator-main-menu-view', - 26 => 'phabricator-notification-css', - 27 => 'phabricator-notification-menu-css', - 28 => 'lightbox-attachment-css', - 29 => 'phui-header-view-css', - 30 => 'phabricator-filetree-view-css', - 31 => 'phabricator-nav-view-css', - 32 => 'phabricator-side-menu-view-css', - 33 => 'phabricator-crumbs-view-css', - 34 => 'phui-object-item-list-view-css', - 35 => 'global-drag-and-drop-css', - 36 => 'phui-spacing-css', - 37 => 'phui-form-css', - 38 => 'phui-icon-view-css', - 39 => 'phabricator-application-launch-view-css', - 40 => 'phabricator-action-list-view-css', - 41 => 'phui-property-list-view-css', - 42 => 'phabricator-tag-view-css', - 43 => 'phui-list-view-css', - ), - 'uri' => '/res/pkg/650e555b/core.pkg.css', - 'type' => 'css', - ), - '2c1dba03' => - array( - 'name' => 'core.pkg.js', - 'symbols' => - array( - 0 => 'javelin-behavior-aphront-basic-tokenizer', - 1 => 'javelin-behavior-workflow', - 2 => 'javelin-behavior-aphront-form-disable-on-submit', - 3 => 'phabricator-keyboard-shortcut-manager', - 4 => 'phabricator-keyboard-shortcut', - 5 => 'javelin-behavior-phabricator-keyboard-shortcuts', - 6 => 'javelin-behavior-refresh-csrf', - 7 => 'javelin-behavior-phabricator-watch-anchor', - 8 => 'javelin-behavior-phabricator-autofocus', - 9 => 'phabricator-menu-item', - 10 => 'phabricator-dropdown-menu', - 11 => 'phabricator-phtize', - 12 => 'javelin-behavior-phabricator-oncopy', - 13 => 'phabricator-tooltip', - 14 => 'javelin-behavior-phabricator-tooltips', - 15 => 'phabricator-prefab', - 16 => 'javelin-behavior-device', - 17 => 'javelin-behavior-toggle-class', - 18 => 'javelin-behavior-lightbox-attachments', - 19 => 'phabricator-busy', - 20 => 'javelin-aphlict', - 21 => 'phabricator-notification', - 22 => 'javelin-behavior-aphlict-listen', - 23 => 'javelin-behavior-phabricator-search-typeahead', - 24 => 'javelin-behavior-konami', - 25 => 'javelin-behavior-aphlict-dropdown', - 26 => 'javelin-behavior-history-install', - 27 => 'javelin-behavior-phabricator-gesture', - 28 => 'javelin-behavior-phabricator-active-nav', - 29 => 'javelin-behavior-phabricator-nav', - 30 => 'javelin-behavior-phabricator-remarkup-assist', - 31 => 'phabricator-textareautils', - 32 => 'phabricator-file-upload', - 33 => 'javelin-behavior-global-drag-and-drop', - 34 => 'javelin-behavior-phabricator-reveal-content', - 35 => 'phabricator-hovercard', - 36 => 'javelin-behavior-phabricator-hovercards', - 37 => 'javelin-color', - 38 => 'javelin-fx', - ), - 'uri' => '/res/pkg/2c1dba03/core.pkg.js', - 'type' => 'js', - ), - '4ccfeb47' => - array( - 'name' => 'darkconsole.pkg.js', - 'symbols' => - array( - 0 => 'javelin-behavior-dark-console', - 1 => 'javelin-behavior-error-log', - ), - 'uri' => '/res/pkg/4ccfeb47/darkconsole.pkg.js', - 'type' => 'js', - ), - '1084b12b' => - array( - 'name' => 'differential.pkg.css', - 'symbols' => - array( - 0 => 'differential-core-view-css', - 1 => 'differential-changeset-view-css', - 2 => 'differential-results-table-css', - 3 => 'differential-revision-history-css', - 4 => 'differential-revision-list-css', - 5 => 'differential-table-of-contents-css', - 6 => 'differential-revision-comment-css', - 7 => 'differential-revision-add-comment-css', - 8 => 'differential-revision-comment-list-css', - 9 => 'phabricator-object-selector-css', - 10 => 'phabricator-content-source-view-css', - 11 => 'differential-local-commits-view-css', - 12 => 'inline-comment-summary-css', - ), - 'uri' => '/res/pkg/1084b12b/differential.pkg.css', - 'type' => 'css', - ), - '5e9e5c4e' => - array( - 'name' => 'differential.pkg.js', - 'symbols' => - array( - 0 => 'phabricator-drag-and-drop-file-upload', - 1 => 'phabricator-shaped-request', - 2 => 'javelin-behavior-differential-feedback-preview', - 3 => 'javelin-behavior-differential-edit-inline-comments', - 4 => 'javelin-behavior-differential-populate', - 5 => 'javelin-behavior-differential-show-more', - 6 => 'javelin-behavior-differential-diff-radios', - 7 => 'javelin-behavior-differential-accept-with-errors', - 8 => 'javelin-behavior-differential-comment-jump', - 9 => 'javelin-behavior-differential-add-reviewers-and-ccs', - 10 => 'javelin-behavior-differential-keyboard-navigation', - 11 => 'javelin-behavior-aphront-drag-and-drop-textarea', - 12 => 'javelin-behavior-phabricator-object-selector', - 13 => 'javelin-behavior-repository-crossreference', - 14 => 'javelin-behavior-load-blame', - 15 => 'differential-inline-comment-editor', - 16 => 'javelin-behavior-differential-dropdown-menus', - 17 => 'javelin-behavior-differential-toggle-files', - 18 => 'javelin-behavior-differential-user-select', - ), - 'uri' => '/res/pkg/5e9e5c4e/differential.pkg.js', - 'type' => 'js', - ), - '7aa115b4' => - array( - 'name' => 'diffusion.pkg.css', - 'symbols' => - array( - 0 => 'diffusion-commit-view-css', - 1 => 'diffusion-icons-css', - ), - 'uri' => '/res/pkg/7aa115b4/diffusion.pkg.css', - 'type' => 'css', - ), - 96909266 => - array( - 'name' => 'diffusion.pkg.js', - 'symbols' => - array( - 0 => 'javelin-behavior-diffusion-pull-lastmodified', - 1 => 'javelin-behavior-diffusion-commit-graph', - 2 => 'javelin-behavior-audit-preview', - ), - 'uri' => '/res/pkg/96909266/diffusion.pkg.js', - 'type' => 'js', - ), - '3e3be199' => - array( - 'name' => 'javelin.pkg.js', - 'symbols' => - array( - 0 => 'javelin-util', - 1 => 'javelin-install', - 2 => 'javelin-event', - 3 => 'javelin-stratcom', - 4 => 'javelin-behavior', - 5 => 'javelin-resource', - 6 => 'javelin-request', - 7 => 'javelin-vector', - 8 => 'javelin-dom', - 9 => 'javelin-json', - 10 => 'javelin-uri', - 11 => 'javelin-workflow', - 12 => 'javelin-mask', - 13 => 'javelin-typeahead', - 14 => 'javelin-typeahead-normalizer', - 15 => 'javelin-typeahead-source', - 16 => 'javelin-typeahead-preloaded-source', - 17 => 'javelin-typeahead-ondemand-source', - 18 => 'javelin-tokenizer', - 19 => 'javelin-history', - ), - 'uri' => '/res/pkg/3e3be199/javelin.pkg.js', - 'type' => 'js', - ), - 49898640 => - array( - 'name' => 'maniphest.pkg.css', - 'symbols' => - array( - 0 => 'maniphest-task-summary-css', - 1 => 'phabricator-project-tag-css', - ), - 'uri' => '/res/pkg/49898640/maniphest.pkg.css', - 'type' => 'css', - ), - '0a694954' => - array( - 'name' => 'maniphest.pkg.js', - 'symbols' => - array( - 0 => 'javelin-behavior-maniphest-batch-selector', - 1 => 'javelin-behavior-maniphest-transaction-controls', - 2 => 'javelin-behavior-maniphest-transaction-preview', - 3 => 'javelin-behavior-maniphest-transaction-expand', - 4 => 'javelin-behavior-maniphest-subpriority-editor', - ), - 'uri' => '/res/pkg/0a694954/maniphest.pkg.js', - 'type' => 'js', - ), - ), - 'reverse' => - array( - 'aphront-dialog-view-css' => '650e555b', - 'aphront-error-view-css' => '650e555b', - 'aphront-list-filter-view-css' => '650e555b', - 'aphront-pager-view-css' => '650e555b', - 'aphront-panel-view-css' => '650e555b', - 'aphront-table-view-css' => '650e555b', - 'aphront-tokenizer-control-css' => '650e555b', - 'aphront-tooltip-css' => '650e555b', - 'aphront-typeahead-control-css' => '650e555b', - 'differential-changeset-view-css' => '1084b12b', - 'differential-core-view-css' => '1084b12b', - 'differential-inline-comment-editor' => '5e9e5c4e', - 'differential-local-commits-view-css' => '1084b12b', - 'differential-results-table-css' => '1084b12b', - 'differential-revision-add-comment-css' => '1084b12b', - 'differential-revision-comment-css' => '1084b12b', - 'differential-revision-comment-list-css' => '1084b12b', - 'differential-revision-history-css' => '1084b12b', - 'differential-revision-list-css' => '1084b12b', - 'differential-table-of-contents-css' => '1084b12b', - 'diffusion-commit-view-css' => '7aa115b4', - 'diffusion-icons-css' => '7aa115b4', - 'global-drag-and-drop-css' => '650e555b', - 'inline-comment-summary-css' => '1084b12b', - 'javelin-aphlict' => '2c1dba03', - 'javelin-behavior' => '3e3be199', - 'javelin-behavior-aphlict-dropdown' => '2c1dba03', - 'javelin-behavior-aphlict-listen' => '2c1dba03', - 'javelin-behavior-aphront-basic-tokenizer' => '2c1dba03', - 'javelin-behavior-aphront-drag-and-drop-textarea' => '5e9e5c4e', - 'javelin-behavior-aphront-form-disable-on-submit' => '2c1dba03', - 'javelin-behavior-audit-preview' => '96909266', - 'javelin-behavior-dark-console' => '4ccfeb47', - 'javelin-behavior-device' => '2c1dba03', - 'javelin-behavior-differential-accept-with-errors' => '5e9e5c4e', - 'javelin-behavior-differential-add-reviewers-and-ccs' => '5e9e5c4e', - 'javelin-behavior-differential-comment-jump' => '5e9e5c4e', - 'javelin-behavior-differential-diff-radios' => '5e9e5c4e', - 'javelin-behavior-differential-dropdown-menus' => '5e9e5c4e', - 'javelin-behavior-differential-edit-inline-comments' => '5e9e5c4e', - 'javelin-behavior-differential-feedback-preview' => '5e9e5c4e', - 'javelin-behavior-differential-keyboard-navigation' => '5e9e5c4e', - 'javelin-behavior-differential-populate' => '5e9e5c4e', - 'javelin-behavior-differential-show-more' => '5e9e5c4e', - 'javelin-behavior-differential-toggle-files' => '5e9e5c4e', - 'javelin-behavior-differential-user-select' => '5e9e5c4e', - 'javelin-behavior-diffusion-commit-graph' => '96909266', - 'javelin-behavior-diffusion-pull-lastmodified' => '96909266', - 'javelin-behavior-error-log' => '4ccfeb47', - 'javelin-behavior-global-drag-and-drop' => '2c1dba03', - 'javelin-behavior-history-install' => '2c1dba03', - 'javelin-behavior-konami' => '2c1dba03', - 'javelin-behavior-lightbox-attachments' => '2c1dba03', - 'javelin-behavior-load-blame' => '5e9e5c4e', - 'javelin-behavior-maniphest-batch-selector' => '0a694954', - 'javelin-behavior-maniphest-subpriority-editor' => '0a694954', - 'javelin-behavior-maniphest-transaction-controls' => '0a694954', - 'javelin-behavior-maniphest-transaction-expand' => '0a694954', - 'javelin-behavior-maniphest-transaction-preview' => '0a694954', - 'javelin-behavior-phabricator-active-nav' => '2c1dba03', - 'javelin-behavior-phabricator-autofocus' => '2c1dba03', - 'javelin-behavior-phabricator-gesture' => '2c1dba03', - 'javelin-behavior-phabricator-hovercards' => '2c1dba03', - 'javelin-behavior-phabricator-keyboard-shortcuts' => '2c1dba03', - 'javelin-behavior-phabricator-nav' => '2c1dba03', - 'javelin-behavior-phabricator-object-selector' => '5e9e5c4e', - 'javelin-behavior-phabricator-oncopy' => '2c1dba03', - 'javelin-behavior-phabricator-remarkup-assist' => '2c1dba03', - 'javelin-behavior-phabricator-reveal-content' => '2c1dba03', - 'javelin-behavior-phabricator-search-typeahead' => '2c1dba03', - 'javelin-behavior-phabricator-tooltips' => '2c1dba03', - 'javelin-behavior-phabricator-watch-anchor' => '2c1dba03', - 'javelin-behavior-refresh-csrf' => '2c1dba03', - 'javelin-behavior-repository-crossreference' => '5e9e5c4e', - 'javelin-behavior-toggle-class' => '2c1dba03', - 'javelin-behavior-workflow' => '2c1dba03', - 'javelin-color' => '2c1dba03', - 'javelin-dom' => '3e3be199', - 'javelin-event' => '3e3be199', - 'javelin-fx' => '2c1dba03', - 'javelin-history' => '3e3be199', - 'javelin-install' => '3e3be199', - 'javelin-json' => '3e3be199', - 'javelin-mask' => '3e3be199', - 'javelin-request' => '3e3be199', - 'javelin-resource' => '3e3be199', - 'javelin-stratcom' => '3e3be199', - 'javelin-tokenizer' => '3e3be199', - 'javelin-typeahead' => '3e3be199', - 'javelin-typeahead-normalizer' => '3e3be199', - 'javelin-typeahead-ondemand-source' => '3e3be199', - 'javelin-typeahead-preloaded-source' => '3e3be199', - 'javelin-typeahead-source' => '3e3be199', - 'javelin-uri' => '3e3be199', - 'javelin-util' => '3e3be199', - 'javelin-vector' => '3e3be199', - 'javelin-workflow' => '3e3be199', - 'lightbox-attachment-css' => '650e555b', - 'maniphest-task-summary-css' => '49898640', - 'phabricator-action-list-view-css' => '650e555b', - 'phabricator-application-launch-view-css' => '650e555b', - 'phabricator-busy' => '2c1dba03', - 'phabricator-content-source-view-css' => '1084b12b', - 'phabricator-core-css' => '650e555b', - 'phabricator-crumbs-view-css' => '650e555b', - 'phabricator-drag-and-drop-file-upload' => '5e9e5c4e', - 'phabricator-dropdown-menu' => '2c1dba03', - 'phabricator-file-upload' => '2c1dba03', - 'phabricator-filetree-view-css' => '650e555b', - 'phabricator-flag-css' => '650e555b', - 'phabricator-hovercard' => '2c1dba03', - 'phabricator-jump-nav' => '650e555b', - 'phabricator-keyboard-shortcut' => '2c1dba03', - 'phabricator-keyboard-shortcut-manager' => '2c1dba03', - 'phabricator-main-menu-view' => '650e555b', - 'phabricator-menu-item' => '2c1dba03', - 'phabricator-nav-view-css' => '650e555b', - 'phabricator-notification' => '2c1dba03', - 'phabricator-notification-css' => '650e555b', - 'phabricator-notification-menu-css' => '650e555b', - 'phabricator-object-selector-css' => '1084b12b', - 'phabricator-phtize' => '2c1dba03', - 'phabricator-prefab' => '2c1dba03', - 'phabricator-project-tag-css' => '49898640', - 'phabricator-remarkup-css' => '650e555b', - 'phabricator-shaped-request' => '5e9e5c4e', - 'phabricator-side-menu-view-css' => '650e555b', - 'phabricator-standard-page-view' => '650e555b', - 'phabricator-tag-view-css' => '650e555b', - 'phabricator-textareautils' => '2c1dba03', - 'phabricator-tooltip' => '2c1dba03', - 'phabricator-transaction-view-css' => '650e555b', - 'phabricator-welcome-page' => '650e555b', - 'phabricator-zindex-css' => '650e555b', - 'phui-button-css' => '650e555b', - 'phui-form-css' => '650e555b', - 'phui-form-view-css' => '650e555b', - 'phui-header-view-css' => '650e555b', - 'phui-icon-view-css' => '650e555b', - 'phui-list-view-css' => '650e555b', - 'phui-object-item-list-view-css' => '650e555b', - 'phui-property-list-view-css' => '650e555b', - 'phui-spacing-css' => '650e555b', - 'sprite-apps-large-css' => '650e555b', - 'sprite-gradient-css' => '650e555b', - 'sprite-icons-css' => '650e555b', - 'sprite-menu-css' => '650e555b', - 'sprite-status-css' => '650e555b', - 'syntax-highlighting-css' => '650e555b', - ), -)); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1c4e18a025..2310f80235 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -48,6 +48,7 @@ phutil_register_library_map(array( 'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php', 'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php', 'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php', + 'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php', 'AphrontFormToggleButtonsControl' => 'view/form/control/AphrontFormToggleButtonsControl.php', 'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php', 'AphrontFormView' => 'view/form/AphrontFormView.php', @@ -81,6 +82,7 @@ phutil_register_library_map(array( 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', + 'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php', 'AphrontTableView' => 'view/control/AphrontTableView.php', 'AphrontTagView' => 'view/AphrontTagView.php', 'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php', @@ -93,14 +95,21 @@ phutil_register_library_map(array( 'AuditActionMenuEventListener' => 'applications/audit/events/AuditActionMenuEventListener.php', 'BuildStepImplementation' => 'applications/harbormaster/step/BuildStepImplementation.php', 'CelerityAPI' => 'infrastructure/celerity/CelerityAPI.php', + 'CelerityManagementMapWorkflow' => 'infrastructure/celerity/management/CelerityManagementMapWorkflow.php', + 'CelerityManagementWorkflow' => 'infrastructure/celerity/management/CelerityManagementWorkflow.php', 'CelerityPhabricatorResourceController' => 'infrastructure/celerity/CelerityPhabricatorResourceController.php', + 'CelerityPhabricatorResources' => 'infrastructure/celerity/resources/CelerityPhabricatorResources.php', + 'CelerityPhysicalResources' => 'infrastructure/celerity/resources/CelerityPhysicalResources.php', 'CelerityResourceController' => 'infrastructure/celerity/CelerityResourceController.php', 'CelerityResourceGraph' => 'infrastructure/celerity/CelerityResourceGraph.php', 'CelerityResourceMap' => 'infrastructure/celerity/CelerityResourceMap.php', 'CelerityResourceTransformer' => 'infrastructure/celerity/CelerityResourceTransformer.php', 'CelerityResourceTransformerTestCase' => 'infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php', + 'CelerityResources' => 'infrastructure/celerity/resources/CelerityResources.php', + 'CelerityResourcesOnDisk' => 'infrastructure/celerity/resources/CelerityResourcesOnDisk.php', 'CeleritySpriteGenerator' => 'infrastructure/celerity/CeleritySpriteGenerator.php', 'CelerityStaticResourceResponse' => 'infrastructure/celerity/CelerityStaticResourceResponse.php', + 'CommandBuildStepImplementation' => 'applications/harbormaster/step/CommandBuildStepImplementation.php', 'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php', 'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php', 'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php', @@ -147,7 +156,6 @@ phutil_register_library_map(array( 'ConduitAPI_diffusion_abstractquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_abstractquery_Method.php', 'ConduitAPI_diffusion_branchquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_branchquery_Method.php', 'ConduitAPI_diffusion_browsequery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_browsequery_Method.php', - 'ConduitAPI_diffusion_commitbranchesquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_commitbranchesquery_Method.php', 'ConduitAPI_diffusion_commitparentsquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_commitparentsquery_Method.php', 'ConduitAPI_diffusion_createcomment_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_createcomment_Method.php', 'ConduitAPI_diffusion_diffquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_diffquery_Method.php', @@ -161,6 +169,8 @@ phutil_register_library_map(array( 'ConduitAPI_diffusion_lastmodifiedquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_lastmodifiedquery_Method.php', 'ConduitAPI_diffusion_looksoon_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_looksoon_Method.php', 'ConduitAPI_diffusion_mergedcommitsquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_mergedcommitsquery_Method.php', + 'ConduitAPI_diffusion_querycommits_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_querycommits_Method.php', + 'ConduitAPI_diffusion_querypaths_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php', 'ConduitAPI_diffusion_rawdiffquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_rawdiffquery_Method.php', 'ConduitAPI_diffusion_readmequery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_readmequery_Method.php', 'ConduitAPI_diffusion_refsquery_Method' => 'applications/diffusion/conduit/ConduitAPI_diffusion_refsquery_Method.php', @@ -189,6 +199,8 @@ phutil_register_library_map(array( 'ConduitAPI_maniphest_info_Method' => 'applications/maniphest/conduit/ConduitAPI_maniphest_info_Method.php', 'ConduitAPI_maniphest_query_Method' => 'applications/maniphest/conduit/ConduitAPI_maniphest_query_Method.php', 'ConduitAPI_maniphest_update_Method' => 'applications/maniphest/conduit/ConduitAPI_maniphest_update_Method.php', + 'ConduitAPI_nuance_Method' => 'applications/nuance/conduit/ConduitAPI_nuance_Method.php', + 'ConduitAPI_nuance_createitem_Method' => 'applications/nuance/conduit/ConduitAPI_nuance_createitem_Method.php', 'ConduitAPI_owners_Method' => 'applications/owners/conduit/ConduitAPI_owners_Method.php', 'ConduitAPI_owners_query_Method' => 'applications/owners/conduit/ConduitAPI_owners_query_Method.php', 'ConduitAPI_paste_Method' => 'applications/paste/conduit/ConduitAPI_paste_Method.php', @@ -205,6 +217,9 @@ phutil_register_library_map(array( 'ConduitAPI_phpast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_Method.php', 'ConduitAPI_phpast_getast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_getast_Method.php', 'ConduitAPI_phpast_version_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_version_Method.php', + 'ConduitAPI_phragment_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_Method.php', + 'ConduitAPI_phragment_getpatch_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php', + 'ConduitAPI_phragment_queryfragments_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php', 'ConduitAPI_phriction_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_Method.php', 'ConduitAPI_phriction_edit_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php', 'ConduitAPI_phriction_history_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_history_Method.php', @@ -246,7 +261,9 @@ phutil_register_library_map(array( 'ConduitAPI_user_whoami_Method' => 'applications/people/conduit/ConduitAPI_user_whoami_Method.php', 'ConduitCall' => 'applications/conduit/call/ConduitCall.php', 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', + 'ConduitConnectionGarbageCollector' => 'applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php', 'ConduitException' => 'applications/conduit/protocol/ConduitException.php', + 'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php', 'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php', 'ConpherenceActionMenuEventListener' => 'applications/conpherence/events/ConpherenceActionMenuEventListener.php', 'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php', @@ -387,7 +404,9 @@ phutil_register_library_map(array( 'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php', 'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php', 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php', + 'DifferentialLandingToGitHub' => 'applications/differential/landing/DifferentialLandingToGitHub.php', 'DifferentialLandingToHostedGit' => 'applications/differential/landing/DifferentialLandingToHostedGit.php', + 'DifferentialLandingToHostedMercurial' => 'applications/differential/landing/DifferentialLandingToHostedMercurial.php', 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', 'DifferentialLintFieldSpecification' => 'applications/differential/field/specification/DifferentialLintFieldSpecification.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', @@ -398,6 +417,7 @@ phutil_register_library_map(array( 'DifferentialNewDiffMail' => 'applications/differential/mail/DifferentialNewDiffMail.php', 'DifferentialPHIDTypeDiff' => 'applications/differential/phid/DifferentialPHIDTypeDiff.php', 'DifferentialPHIDTypeRevision' => 'applications/differential/phid/DifferentialPHIDTypeRevision.php', + 'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php', 'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php', 'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php', 'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php', @@ -449,7 +469,6 @@ phutil_register_library_map(array( 'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php', 'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php', 'DifferentialViewPolicyFieldSpecification' => 'applications/differential/field/specification/DifferentialViewPolicyFieldSpecification.php', - 'DiffusionBranchInformation' => 'applications/diffusion/data/DiffusionBranchInformation.php', 'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php', 'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php', 'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php', @@ -471,8 +490,12 @@ phutil_register_library_map(array( 'DiffusionCommitChangeTableView' => 'applications/diffusion/view/DiffusionCommitChangeTableView.php', 'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php', 'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php', - 'DiffusionCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionCommitParentsQuery.php', + 'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php', + 'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php', + 'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php', 'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php', + 'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php', + 'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', 'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php', 'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php', @@ -483,7 +506,6 @@ phutil_register_library_map(array( 'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php', 'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php', 'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php', - 'DiffusionGitCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php', 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php', 'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php', @@ -497,16 +519,20 @@ phutil_register_library_map(array( 'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php', 'DiffusionLintDetailsController' => 'applications/diffusion/controller/DiffusionLintDetailsController.php', 'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php', + 'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php', + 'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php', 'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php', 'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php', + 'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php', 'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php', 'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php', - 'DiffusionMercurialCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php', 'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php', 'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php', 'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php', 'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php', 'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php', + 'DiffusionMirrorDeleteController' => 'applications/diffusion/controller/DiffusionMirrorDeleteController.php', + 'DiffusionMirrorEditController' => 'applications/diffusion/controller/DiffusionMirrorEditController.php', 'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php', 'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php', 'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php', @@ -514,9 +540,9 @@ phutil_register_library_map(array( 'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php', 'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php', 'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php', + 'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php', 'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php', 'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php', - 'DiffusionRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRemarkupRule.php', 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php', @@ -526,27 +552,36 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php', 'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php', 'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php', + 'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php', 'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php', 'DiffusionRepositoryEditEncodingController' => 'applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php', 'DiffusionRepositoryEditHostingController' => 'applications/diffusion/controller/DiffusionRepositoryEditHostingController.php', 'DiffusionRepositoryEditLocalController' => 'applications/diffusion/controller/DiffusionRepositoryEditLocalController.php', 'DiffusionRepositoryEditMainController' => 'applications/diffusion/controller/DiffusionRepositoryEditMainController.php', - 'DiffusionRepositoryEditPolicyController' => 'applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php', 'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', 'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php', + 'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php', 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', + 'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php', 'DiffusionSSHGitReceivePackWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php', 'DiffusionSSHGitUploadPackWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php', 'DiffusionSSHGitWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitWorkflow.php', + 'DiffusionSSHMercurialServeWorkflow' => 'applications/diffusion/ssh/DiffusionSSHMercurialServeWorkflow.php', + 'DiffusionSSHMercurialWireClientProtocolChannel' => 'applications/diffusion/ssh/DiffusionSSHMercurialWireClientProtocolChannel.php', + 'DiffusionSSHMercurialWireTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionSSHMercurialWireTestCase.php', + 'DiffusionSSHMercurialWorkflow' => 'applications/diffusion/ssh/DiffusionSSHMercurialWorkflow.php', + 'DiffusionSSHSubversionServeWorkflow' => 'applications/diffusion/ssh/DiffusionSSHSubversionServeWorkflow.php', + 'DiffusionSSHSubversionWorkflow' => 'applications/diffusion/ssh/DiffusionSSHSubversionWorkflow.php', 'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', 'DiffusionSetPasswordPanel' => 'applications/diffusion/panel/DiffusionSetPasswordPanel.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', - 'DiffusionSvnCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php', + 'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php', + 'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php', 'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php', 'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php', 'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php', @@ -608,41 +643,70 @@ phutil_register_library_map(array( 'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php', 'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php', 'DoorkeeperRemarkupRuleJIRA' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php', + 'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php', 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', - 'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php', + 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', + 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', + 'DrydockBlueprintCreateController' => 'applications/drydock/controller/DrydockBlueprintCreateController.php', + 'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php', + 'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php', + 'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php', + 'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php', + 'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php', 'DrydockBlueprintScopeGuard' => 'applications/drydock/util/DrydockBlueprintScopeGuard.php', + 'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php', + 'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php', + 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', + 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', + 'DrydockCapabilityCreateBlueprints' => 'applications/drydock/capability/DrydockCapabilityCreateBlueprints.php', + 'DrydockCapabilityDefaultEdit' => 'applications/drydock/capability/DrydockCapabilityDefaultEdit.php', + 'DrydockCapabilityDefaultView' => 'applications/drydock/capability/DrydockCapabilityDefaultView.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', + 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', 'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php', + 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', + 'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', 'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php', 'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php', + 'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', 'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php', 'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php', - 'DrydockLocalHostBlueprint' => 'applications/drydock/blueprint/DrydockLocalHostBlueprint.php', + 'DrydockLocalHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', 'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php', + 'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php', 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', + 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', + 'DrydockManagementCreateResourceWorkflow' => 'applications/drydock/management/DrydockManagementCreateResourceWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', - 'DrydockManagementWaitForLeaseWorkflow' => 'applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', + 'DrydockPHIDTypeBlueprint' => 'applications/drydock/phid/DrydockPHIDTypeBlueprint.php', + 'DrydockPHIDTypeLease' => 'applications/drydock/phid/DrydockPHIDTypeLease.php', + 'DrydockPHIDTypeResource' => 'applications/drydock/phid/DrydockPHIDTypeResource.php', + 'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php', + 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php', + 'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php', 'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php', 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', + 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', + 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', - 'DrydockWorkingCopyBlueprint' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprint.php', + 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php', 'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php', 'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php', @@ -650,10 +714,15 @@ phutil_register_library_map(array( 'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php', 'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php', 'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php', + 'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php', 'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php', + 'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php', + 'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php', + 'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php', 'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php', 'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php', 'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php', + 'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php', 'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', @@ -666,11 +735,11 @@ phutil_register_library_map(array( 'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php', 'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php', 'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php', + 'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php', 'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php', 'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php', - 'HarbormasterBuildableApplyController' => 'applications/harbormaster/controller/HarbormasterBuildableApplyController.php', - 'HarbormasterBuildableArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildableArtifactQuery.php', - 'HarbormasterBuildableEditController' => 'applications/harbormaster/controller/HarbormasterBuildableEditController.php', + 'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php', + 'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php', 'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php', 'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php', 'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php', @@ -678,22 +747,33 @@ phutil_register_library_map(array( 'HarbormasterCapabilityManagePlans' => 'applications/harbormaster/capability/HarbormasterCapabilityManagePlans.php', 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php', + 'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php', + 'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php', + 'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterPHIDTypeBuild' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuild.php', 'HarbormasterPHIDTypeBuildItem' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildItem.php', + 'HarbormasterPHIDTypeBuildLog' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php', 'HarbormasterPHIDTypeBuildPlan' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php', 'HarbormasterPHIDTypeBuildStep' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildStep.php', 'HarbormasterPHIDTypeBuildTarget' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildTarget.php', 'HarbormasterPHIDTypeBuildable' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildable.php', 'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php', + 'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php', 'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php', 'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php', + 'HarbormasterPlanOrderController' => 'applications/harbormaster/controller/HarbormasterPlanOrderController.php', + 'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php', 'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php', 'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php', 'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php', 'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php', 'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php', 'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php', + 'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php', + 'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php', + 'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php', + 'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php', 'HeraldAction' => 'applications/herald/storage/HeraldAction.php', 'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php', 'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php', @@ -716,7 +796,11 @@ phutil_register_library_map(array( 'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php', 'HeraldPHIDTypeRule' => 'applications/herald/phid/HeraldPHIDTypeRule.php', 'HeraldPholioMockAdapter' => 'applications/herald/adapter/HeraldPholioMockAdapter.php', + 'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php', + 'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php', + 'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php', 'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php', + 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', 'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', @@ -736,13 +820,18 @@ phutil_register_library_map(array( 'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php', 'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php', 'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php', + 'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php', 'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php', 'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php', + 'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php', 'Javelin' => 'infrastructure/javelin/Javelin.php', 'JavelinReactorExample' => 'applications/uiexample/examples/JavelinReactorExample.php', 'JavelinUIExample' => 'applications/uiexample/examples/JavelinUIExample.php', 'JavelinViewExample' => 'applications/uiexample/examples/JavelinViewExample.php', 'JavelinViewExampleServerView' => 'applications/uiexample/examples/JavelinViewExampleServerView.php', + 'LeaseHostBuildStepImplementation' => 'applications/harbormaster/step/LeaseHostBuildStepImplementation.php', + 'LegalpadCapabilityDefaultEdit' => 'applications/legalpad/capability/LegalpadCapabilityDefaultEdit.php', + 'LegalpadCapabilityDefaultView' => 'applications/legalpad/capability/LegalpadCapabilityDefaultView.php', 'LegalpadConstants' => 'applications/legalpad/constants/LegalpadConstants.php', 'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php', 'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php', @@ -756,6 +845,9 @@ phutil_register_library_map(array( 'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php', 'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php', 'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php', + 'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php', + 'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php', + 'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php', 'LegalpadDocumentViewController' => 'applications/legalpad/controller/LegalpadDocumentViewController.php', 'LegalpadMockMailReceiver' => 'applications/legalpad/mail/LegalpadMockMailReceiver.php', 'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php', @@ -834,6 +926,10 @@ phutil_register_library_map(array( 'MetaMTAConstants' => 'applications/metamta/constants/MetaMTAConstants.php', 'MetaMTANotificationType' => 'applications/metamta/constants/MetaMTANotificationType.php', 'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php', + 'NuanceCapabilitySourceDefaultEdit' => 'applications/nuance/capability/NuanceCapabilitySourceDefaultEdit.php', + 'NuanceCapabilitySourceDefaultView' => 'applications/nuance/capability/NuanceCapabilitySourceDefaultView.php', + 'NuanceCapabilitySourceManage' => 'applications/nuance/capability/NuanceCapabilitySourceManage.php', + 'NuanceConstants' => 'applications/nuance/constants/NuanceConstants.php', 'NuanceController' => 'applications/nuance/controller/NuanceController.php', 'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php', 'NuanceItem' => 'applications/nuance/storage/NuanceItem.php', @@ -848,6 +944,7 @@ phutil_register_library_map(array( 'NuancePHIDTypeQueue' => 'applications/nuance/phid/NuancePHIDTypeQueue.php', 'NuancePHIDTypeRequestor' => 'applications/nuance/phid/NuancePHIDTypeRequestor.php', 'NuancePHIDTypeSource' => 'applications/nuance/phid/NuancePHIDTypeSource.php', + 'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php', 'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php', 'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php', 'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php', @@ -868,6 +965,7 @@ phutil_register_library_map(array( 'NuanceRequestorTransactionQuery' => 'applications/nuance/query/NuanceRequestorTransactionQuery.php', 'NuanceRequestorViewController' => 'applications/nuance/controller/NuanceRequestorViewController.php', 'NuanceSource' => 'applications/nuance/storage/NuanceSource.php', + 'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php', 'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php', 'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php', 'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php', @@ -914,6 +1012,8 @@ phutil_register_library_map(array( 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', + 'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php', + 'PHUITagView' => 'view/phui/PHUITagView.php', 'PHUITextExample' => 'applications/uiexample/examples/PHUITextExample.php', 'PHUITextView' => 'view/phui/PHUITextView.php', 'PHUIWorkboardExample' => 'applications/uiexample/examples/PHUIWorkboardExample.php', @@ -923,15 +1023,41 @@ phutil_register_library_map(array( 'PackageDeleteMail' => 'applications/owners/mail/PackageDeleteMail.php', 'PackageMail' => 'applications/owners/mail/PackageMail.php', 'PackageModifyMail' => 'applications/owners/mail/PackageModifyMail.php', + 'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php', + 'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php', + 'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php', + 'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php', + 'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php', + 'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php', + 'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php', + 'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php', + 'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php', + 'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php', + 'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php', + 'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php', + 'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php', + 'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php', + 'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php', + 'PassphraseCredentialTypePassword' => 'applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php', + 'PassphraseCredentialTypeSSHPrivateKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php', + 'PassphraseCredentialTypeSSHPrivateKeyFile' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php', + 'PassphraseCredentialTypeSSHPrivateKeyText' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php', + 'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php', + 'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php', + 'PassphrasePHIDTypeCredential' => 'applications/passphrase/phid/PassphrasePHIDTypeCredential.php', + 'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php', + 'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php', + 'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php', 'PasteCapabilityDefaultView' => 'applications/paste/capability/PasteCapabilityDefaultView.php', 'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php', 'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', 'PasteMockMailReceiver' => 'applications/paste/mail/PasteMockMailReceiver.php', 'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php', + 'PeopleCapabilityBrowseUserDirectory' => 'applications/people/capability/PeopleCapabilityBrowseUserDirectory.php', 'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', 'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php', 'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php', - 'PhabricatorAccessLog' => 'infrastructure/PhabricatorAccessLog.php', + 'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php', 'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php', 'PhabricatorActionHeaderExample' => 'applications/uiexample/examples/PhabricatorActionHeaderExample.php', 'PhabricatorActionHeaderView' => 'view/layout/PhabricatorActionHeaderView.php', @@ -956,6 +1082,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationConpherence' => 'applications/conpherence/application/PhabricatorApplicationConpherence.php', 'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php', 'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php', + 'PhabricatorApplicationDashboard' => 'applications/dashboard/application/PhabricatorApplicationDashboard.php', 'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php', 'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php', 'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php', @@ -970,6 +1097,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationFlags' => 'applications/flag/application/PhabricatorApplicationFlags.php', 'PhabricatorApplicationHarbormaster' => 'applications/harbormaster/application/PhabricatorApplicationHarbormaster.php', 'PhabricatorApplicationHerald' => 'applications/herald/application/PhabricatorApplicationHerald.php', + 'PhabricatorApplicationHome' => 'applications/home/application/PhabricatorApplicationHome.php', 'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php', 'PhabricatorApplicationLegalpad' => 'applications/legalpad/application/PhabricatorApplicationLegalpad.php', 'PhabricatorApplicationMacro' => 'applications/macro/application/PhabricatorApplicationMacro.php', @@ -980,12 +1108,14 @@ phutil_register_library_map(array( 'PhabricatorApplicationOwners' => 'applications/owners/application/PhabricatorApplicationOwners.php', 'PhabricatorApplicationPHIDTypeApplication' => 'applications/meta/phid/PhabricatorApplicationPHIDTypeApplication.php', 'PhabricatorApplicationPHPAST' => 'applications/phpast/application/PhabricatorApplicationPHPAST.php', + 'PhabricatorApplicationPassphrase' => 'applications/passphrase/application/PhabricatorApplicationPassphrase.php', 'PhabricatorApplicationPaste' => 'applications/paste/application/PhabricatorApplicationPaste.php', 'PhabricatorApplicationPeople' => 'applications/people/application/PhabricatorApplicationPeople.php', 'PhabricatorApplicationPhame' => 'applications/phame/application/PhabricatorApplicationPhame.php', 'PhabricatorApplicationPhlux' => 'applications/phlux/application/PhabricatorApplicationPhlux.php', 'PhabricatorApplicationPholio' => 'applications/pholio/application/PhabricatorApplicationPholio.php', 'PhabricatorApplicationPhortune' => 'applications/phortune/application/PhabricatorApplicationPhortune.php', + 'PhabricatorApplicationPhragment' => 'applications/phragment/application/PhabricatorApplicationPhragment.php', 'PhabricatorApplicationPhrequent' => 'applications/phrequent/application/PhabricatorApplicationPhrequent.php', 'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php', 'PhabricatorApplicationPolicy' => 'applications/policy/application/PhabricatorApplicationPolicy.php', @@ -1065,6 +1195,7 @@ phutil_register_library_map(array( 'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php', 'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php', 'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php', + 'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php', 'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php', 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', @@ -1089,6 +1220,10 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderPassword' => 'applications/auth/provider/PhabricatorAuthProviderPassword.php', 'PhabricatorAuthProviderPersona' => 'applications/auth/provider/PhabricatorAuthProviderPersona.php', 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', + 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', + 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', + 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', + 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', @@ -1117,8 +1252,11 @@ phutil_register_library_map(array( 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', 'PhabricatorBusyExample' => 'applications/uiexample/examples/PhabricatorBusyExample.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', + 'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php', 'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php', 'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php', + 'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php', + 'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php', 'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php', 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php', 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', @@ -1129,6 +1267,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php', 'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php', + 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', @@ -1140,6 +1279,7 @@ phutil_register_library_map(array( 'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php', 'PhabricatorChatLogEventType' => 'applications/chatlog/constants/PhabricatorChatLogEventType.php', 'PhabricatorChatLogQuery' => 'applications/chatlog/PhabricatorChatLogQuery.php', + 'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', 'PhabricatorConduitConfigOptions' => 'applications/conduit/config/PhabricatorConduitConfigOptions.php', @@ -1191,6 +1331,7 @@ phutil_register_library_map(array( 'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php', 'PhabricatorContentSourceView' => 'applications/metamta/contentsource/PhabricatorContentSourceView.php', 'PhabricatorController' => 'applications/base/controller/PhabricatorController.php', + 'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php', 'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php', 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 'PhabricatorCountdownCapabilityDefaultView' => 'applications/countdown/capability/PhabricatorCountdownCapabilityDefaultView.php', @@ -1231,6 +1372,7 @@ phutil_register_library_map(array( 'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php', 'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php', 'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php', + 'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php', 'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php', 'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php', 'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php', @@ -1245,6 +1387,19 @@ phutil_register_library_map(array( 'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php', 'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php', 'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php', + 'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php', + 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', + 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', + 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', + 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', + 'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php', + 'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php', + 'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php', + 'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php', + 'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php', + 'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php', + 'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php', + 'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php', 'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php', 'PhabricatorDebugController' => 'applications/system/PhabricatorDebugController.php', 'PhabricatorDefaultFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php', @@ -1254,8 +1409,6 @@ phutil_register_library_map(array( 'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php', 'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php', - 'PhabricatorDirectoryController' => 'applications/directory/controller/PhabricatorDirectoryController.php', - 'PhabricatorDirectoryMainController' => 'applications/directory/controller/PhabricatorDirectoryMainController.php', 'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php', 'PhabricatorDisqusConfigOptions' => 'applications/config/option/PhabricatorDisqusConfigOptions.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', @@ -1353,6 +1506,7 @@ phutil_register_library_map(array( 'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php', 'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php', 'PhabricatorFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorFileStorageEngineSelector.php', + 'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php', 'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php', 'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php', 'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php', @@ -1382,18 +1536,23 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', 'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php', 'PhabricatorFormExample' => 'applications/uiexample/examples/PhabricatorFormExample.php', + 'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php', 'PhabricatorGarbageCollectorConfigOptions' => 'applications/config/option/PhabricatorGarbageCollectorConfigOptions.php', - 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php', + 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php', 'PhabricatorGestureExample' => 'applications/uiexample/examples/PhabricatorGestureExample.php', 'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php', 'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php', 'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', 'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php', + 'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php', 'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php', 'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php', 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', + 'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php', + 'PhabricatorHomeMainController' => 'applications/home/controller/PhabricatorHomeMainController.php', + 'PhabricatorHomeQuickCreateController' => 'applications/home/controller/PhabricatorHomeQuickCreateController.php', 'PhabricatorHovercardExample' => 'applications/uiexample/examples/PhabricatorHovercardExample.php', 'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php', 'PhabricatorIRCBot' => 'infrastructure/daemon/bot/PhabricatorIRCBot.php', @@ -1443,6 +1602,7 @@ phutil_register_library_map(array( 'PhabricatorMail' => 'applications/metamta/PhabricatorMail.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', + 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', @@ -1458,6 +1618,7 @@ phutil_register_library_map(array( 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', + 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 'PhabricatorMailingListPHIDTypeList' => 'applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php', 'PhabricatorMailingListQuery' => 'applications/mailinglists/query/PhabricatorMailingListQuery.php', 'PhabricatorMailingListSearchEngine' => 'applications/mailinglists/query/PhabricatorMailingListSearchEngine.php', @@ -1468,6 +1629,7 @@ phutil_register_library_map(array( 'PhabricatorMainMenuIconView' => 'view/page/menu/PhabricatorMainMenuIconView.php', 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', 'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php', + 'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php', 'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php', 'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php', 'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php', @@ -1488,7 +1650,9 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php', 'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php', 'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php', + 'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php', 'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php', + 'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php', 'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php', 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php', @@ -1583,7 +1747,9 @@ phutil_register_library_map(array( 'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php', 'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php', 'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php', + 'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', + 'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php', 'PhabricatorPeopleEditController' => 'applications/people/controller/PhabricatorPeopleEditController.php', 'PhabricatorPeopleHovercardEventListener' => 'applications/people/event/PhabricatorPeopleHovercardEventListener.php', 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', @@ -1630,6 +1796,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', 'PhabricatorPolicyRuleAdministrators' => 'applications/policy/rule/PhabricatorPolicyRuleAdministrators.php', + 'PhabricatorPolicyRuleLegalpadSignature' => 'applications/policy/rule/PhabricatorPolicyRuleLegalpadSignature.php', 'PhabricatorPolicyRuleLunarPhase' => 'applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php', 'PhabricatorPolicyRuleProjects' => 'applications/policy/rule/PhabricatorPolicyRuleProjects.php', 'PhabricatorPolicyRuleUsers' => 'applications/policy/rule/PhabricatorPolicyRuleUsers.php', @@ -1638,6 +1805,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', + 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', @@ -1652,6 +1820,7 @@ phutil_register_library_map(array( 'PhabricatorProjectHistoryController' => 'applications/project/controller/PhabricatorProjectHistoryController.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', + 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 'PhabricatorProjectNameCollisionException' => 'applications/project/exception/PhabricatorProjectNameCollisionException.php', 'PhabricatorProjectPHIDTypeColumn' => 'applications/project/phid/PhabricatorProjectPHIDTypeColumn.php', 'PhabricatorProjectPHIDTypeProject' => 'applications/project/phid/PhabricatorProjectPHIDTypeProject.php', @@ -1708,24 +1877,40 @@ phutil_register_library_map(array( 'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php', + 'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php', 'PhabricatorRepositoryListController' => 'applications/repository/controller/PhabricatorRepositoryListController.php', 'PhabricatorRepositoryManagementDeleteWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', + 'PhabricatorRepositoryManagementEditWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php', 'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php', 'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php', + 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php', + 'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php', 'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php', + 'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php', 'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php', + 'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php', + 'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php', + 'PhabricatorRepositoryMirrorQuery' => 'applications/repository/query/PhabricatorRepositoryMirrorQuery.php', 'PhabricatorRepositoryPHIDTypeArcanistProject' => 'applications/repository/phid/PhabricatorRepositoryPHIDTypeArcanistProject.php', 'PhabricatorRepositoryPHIDTypeCommit' => 'applications/repository/phid/PhabricatorRepositoryPHIDTypeCommit.php', + 'PhabricatorRepositoryPHIDTypeMirror' => 'applications/repository/phid/PhabricatorRepositoryPHIDTypeMirror.php', + 'PhabricatorRepositoryPHIDTypePushLog' => 'applications/repository/phid/PhabricatorRepositoryPHIDTypePushLog.php', 'PhabricatorRepositoryPHIDTypeRepository' => 'applications/repository/phid/PhabricatorRepositoryPHIDTypeRepository.php', + 'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php', 'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php', 'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php', + 'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php', + 'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php', + 'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php', 'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php', + 'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php', + 'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php', + 'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php', 'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php', - 'PhabricatorRepositoryShortcut' => 'applications/repository/storage/PhabricatorRepositoryShortcut.php', 'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php', @@ -1734,9 +1919,14 @@ phutil_register_library_map(array( 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', + 'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php', + 'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php', + 'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php', 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', + 'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php', + 'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php', 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', 'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php', 'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php', @@ -1768,6 +1958,7 @@ phutil_register_library_map(array( 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 'PhabricatorSearchScope' => 'applications/search/constants/PhabricatorSearchScope.php', 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', + 'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php', 'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php', 'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php', 'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php', @@ -1787,11 +1978,13 @@ phutil_register_library_map(array( 'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php', 'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php', 'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php', + 'PhabricatorSettingsPanelSessions' => 'applications/settings/panel/PhabricatorSettingsPanelSessions.php', 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 'PhabricatorSetupCheckAPC' => 'applications/config/check/PhabricatorSetupCheckAPC.php', 'PhabricatorSetupCheckAuth' => 'applications/config/check/PhabricatorSetupCheckAuth.php', 'PhabricatorSetupCheckBaseURI' => 'applications/config/check/PhabricatorSetupCheckBaseURI.php', 'PhabricatorSetupCheckBinaries' => 'applications/config/check/PhabricatorSetupCheckBinaries.php', + 'PhabricatorSetupCheckDaemons' => 'applications/config/check/PhabricatorSetupCheckDaemons.php', 'PhabricatorSetupCheckDatabase' => 'applications/config/check/PhabricatorSetupCheckDatabase.php', 'PhabricatorSetupCheckExtensions' => 'applications/config/check/PhabricatorSetupCheckExtensions.php', 'PhabricatorSetupCheckExtraConfig' => 'applications/config/check/PhabricatorSetupCheckExtraConfig.php', @@ -1864,8 +2057,6 @@ phutil_register_library_map(array( 'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php', 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php', - 'PhabricatorTagExample' => 'applications/uiexample/examples/PhabricatorTagExample.php', - 'PhabricatorTagView' => 'view/layout/PhabricatorTagView.php', 'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', 'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', 'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php', @@ -1905,6 +2096,7 @@ phutil_register_library_map(array( 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', + 'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php', 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIListFilterExample' => 'applications/uiexample/examples/PhabricatorUIListFilterExample.php', @@ -1984,6 +2176,7 @@ phutil_register_library_map(array( 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', 'PhameBlogSkin' => 'applications/phame/skins/PhameBlogSkin.php', 'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php', + 'PhameCelerityResources' => 'applications/phame/celerity/PhameCelerityResources.php', 'PhameController' => 'applications/phame/controller/PhameController.php', 'PhameDAO' => 'applications/phame/storage/PhameDAO.php', 'PhamePost' => 'applications/phame/storage/PhamePost.php', @@ -2013,6 +2206,7 @@ phutil_register_library_map(array( 'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php', 'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php', 'PholioActionMenuEventListener' => 'applications/pholio/event/PholioActionMenuEventListener.php', + 'PholioCapabilityDefaultView' => 'applications/pholio/capability/PholioCapabilityDefaultView.php', 'PholioConstants' => 'applications/pholio/constants/PholioConstants.php', 'PholioController' => 'applications/pholio/controller/PholioController.php', 'PholioDAO' => 'applications/pholio/storage/PholioDAO.php', @@ -2095,6 +2289,34 @@ phutil_register_library_map(array( 'PhortuneTestExtraPaymentProvider' => 'applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php', 'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php', 'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php', + 'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php', + 'PhragmentCapabilityCanCreate' => 'applications/phragment/capability/PhragmentCapabilityCanCreate.php', + 'PhragmentController' => 'applications/phragment/controller/PhragmentController.php', + 'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php', + 'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php', + 'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php', + 'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php', + 'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php', + 'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php', + 'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php', + 'PhragmentPHIDTypeFragment' => 'applications/phragment/phid/PhragmentPHIDTypeFragment.php', + 'PhragmentPHIDTypeFragmentVersion' => 'applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php', + 'PhragmentPHIDTypeSnapshot' => 'applications/phragment/phid/PhragmentPHIDTypeSnapshot.php', + 'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php', + 'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php', + 'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php', + 'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php', + 'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php', + 'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php', + 'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php', + 'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php', + 'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php', + 'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php', + 'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php', + 'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php', + 'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php', + 'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php', + 'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php', 'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php', 'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php', 'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php', @@ -2176,6 +2398,7 @@ phutil_register_library_map(array( 'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php', 'ProjectCapabilityCreateProjects' => 'applications/project/capability/ProjectCapabilityCreateProjects.php', 'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php', + 'PublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/PublishFragmentBuildStepImplementation.php', 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', 'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php', 'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php', @@ -2200,6 +2423,7 @@ phutil_register_library_map(array( 'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php', 'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php', 'ReleephDefaultUserView' => 'applications/releeph/view/user/ReleephDefaultUserView.php', + 'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php', 'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php', 'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php', 'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php', @@ -2253,9 +2477,13 @@ phutil_register_library_map(array( 'ReleephStatusFieldSpecification' => 'applications/releeph/field/specification/ReleephStatusFieldSpecification.php', 'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php', 'ReleephUserView' => 'applications/releeph/view/user/ReleephUserView.php', + 'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php', 'SleepBuildStepImplementation' => 'applications/harbormaster/step/SleepBuildStepImplementation.php', 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', + 'UploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/UploadArtifactBuildStepImplementation.php', + 'VariableBuildStepImplementation' => 'applications/harbormaster/step/VariableBuildStepImplementation.php', + 'WaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php', ), 'function' => array( @@ -2263,7 +2491,6 @@ phutil_register_library_map(array( '_phabricator_time_format' => 'view/viewutils.php', 'celerity_generate_unique_node_id' => 'infrastructure/celerity/api.php', 'celerity_get_resource_uri' => 'infrastructure/celerity/api.php', - 'celerity_register_resource_map' => 'infrastructure/celerity/map.php', 'implode_selected_handle_links' => 'applications/phid/handle/view/render.php', 'javelin_tag' => 'infrastructure/javelin/markup.php', 'phabricator_date' => 'view/viewutils.php', @@ -2322,6 +2549,7 @@ phutil_register_library_map(array( 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', + 'AphrontFormTextWithSubmitControl' => 'AphrontFormControl', 'AphrontFormToggleButtonsControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', @@ -2352,6 +2580,7 @@ phutil_register_library_map(array( 'AphrontRequestFailureView' => 'AphrontView', 'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontSideNavFilterView' => 'AphrontView', + 'AphrontStackTraceView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontTagView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView', @@ -2365,10 +2594,16 @@ phutil_register_library_map(array( ), 'AphrontWebpageResponse' => 'AphrontHTMLResponse', 'AuditActionMenuEventListener' => 'PhabricatorEventListener', + 'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow', + 'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow', 'CelerityPhabricatorResourceController' => 'CelerityResourceController', + 'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk', + 'CelerityPhysicalResources' => 'CelerityResources', 'CelerityResourceController' => 'PhabricatorController', 'CelerityResourceGraph' => 'AbstractDirectedGraph', 'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase', + 'CelerityResourcesOnDisk' => 'CelerityPhysicalResources', + 'CommandBuildStepImplementation' => 'VariableBuildStepImplementation', 'ConduitAPIMethod' => array( 0 => 'Phobject', @@ -2417,7 +2652,6 @@ phutil_register_library_map(array( 'ConduitAPI_diffusion_abstractquery_Method' => 'ConduitAPI_diffusion_Method', 'ConduitAPI_diffusion_branchquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_browsequery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', - 'ConduitAPI_diffusion_commitbranchesquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_commitparentsquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_createcomment_Method' => 'ConduitAPI_diffusion_Method', 'ConduitAPI_diffusion_diffquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', @@ -2431,6 +2665,8 @@ phutil_register_library_map(array( 'ConduitAPI_diffusion_lastmodifiedquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_looksoon_Method' => 'ConduitAPI_diffusion_Method', 'ConduitAPI_diffusion_mergedcommitsquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', + 'ConduitAPI_diffusion_querycommits_Method' => 'ConduitAPI_diffusion_Method', + 'ConduitAPI_diffusion_querypaths_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_rawdiffquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_readmequery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', 'ConduitAPI_diffusion_refsquery_Method' => 'ConduitAPI_diffusion_abstractquery_Method', @@ -2459,6 +2695,8 @@ phutil_register_library_map(array( 'ConduitAPI_maniphest_info_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_maniphest_query_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_maniphest_update_Method' => 'ConduitAPI_maniphest_Method', + 'ConduitAPI_nuance_Method' => 'ConduitAPIMethod', + 'ConduitAPI_nuance_createitem_Method' => 'ConduitAPI_nuance_Method', 'ConduitAPI_owners_Method' => 'ConduitAPIMethod', 'ConduitAPI_owners_query_Method' => 'ConduitAPI_owners_Method', 'ConduitAPI_paste_Method' => 'ConduitAPIMethod', @@ -2475,6 +2713,9 @@ phutil_register_library_map(array( 'ConduitAPI_phpast_Method' => 'ConduitAPIMethod', 'ConduitAPI_phpast_getast_Method' => 'ConduitAPI_phpast_Method', 'ConduitAPI_phpast_version_Method' => 'ConduitAPI_phpast_Method', + 'ConduitAPI_phragment_Method' => 'ConduitAPIMethod', + 'ConduitAPI_phragment_getpatch_Method' => 'ConduitAPI_phragment_Method', + 'ConduitAPI_phragment_queryfragments_Method' => 'ConduitAPI_phragment_Method', 'ConduitAPI_phriction_Method' => 'ConduitAPIMethod', 'ConduitAPI_phriction_edit_Method' => 'ConduitAPI_phriction_Method', 'ConduitAPI_phriction_history_Method' => 'ConduitAPI_phriction_Method', @@ -2515,7 +2756,9 @@ phutil_register_library_map(array( 'ConduitAPI_user_removestatus_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_whoami_Method' => 'ConduitAPI_user_Method', 'ConduitCallTestCase' => 'PhabricatorTestCase', + 'ConduitConnectionGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitException' => 'Exception', + 'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow', 'ConpherenceActionMenuEventListener' => 'PhabricatorEventListener', 'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -2617,6 +2860,7 @@ phutil_register_library_map(array( array( 0 => 'DifferentialDAO', 1 => 'PhabricatorPolicyInterface', + 2 => 'HarbormasterBuildableInterface', ), 'DifferentialDiffContentMail' => 'DifferentialMail', 'DifferentialDiffCreateController' => 'DifferentialController', @@ -2651,7 +2895,9 @@ phutil_register_library_map(array( 'DifferentialInlineCommentView' => 'AphrontView', 'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener', + 'DifferentialLandingToGitHub' => 'DifferentialLandingStrategy', 'DifferentialLandingToHostedGit' => 'DifferentialLandingStrategy', + 'DifferentialLandingToHostedMercurial' => 'DifferentialLandingStrategy', 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLintFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLocalCommitsView' => 'AphrontView', @@ -2660,6 +2906,7 @@ phutil_register_library_map(array( 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialPHIDTypeDiff' => 'PhabricatorPHIDType', 'DifferentialPHIDTypeRevision' => 'PhabricatorPHIDType', + 'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector', 'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', 'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialPrimaryPaneView' => 'AphrontView', @@ -2682,6 +2929,7 @@ phutil_register_library_map(array( 2 => 'PhabricatorPolicyInterface', 3 => 'PhabricatorFlaggableInterface', 4 => 'PhrequentTrackableInterface', + 5 => 'HarbormasterBuildableInterface', ), 'DifferentialRevisionCommentListView' => 'AphrontView', 'DifferentialRevisionCommentView' => 'AphrontView', @@ -2733,8 +2981,12 @@ phutil_register_library_map(array( 'DiffusionCommitChangeTableView' => 'DiffusionView', 'DiffusionCommitController' => 'DiffusionController', 'DiffusionCommitEditController' => 'DiffusionController', - 'DiffusionCommitParentsQuery' => 'DiffusionQuery', + 'DiffusionCommitHash' => 'Phobject', + 'DiffusionCommitHookEngine' => 'Phobject', + 'DiffusionCommitHookRejectException' => 'Exception', 'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'DiffusionCommitRef' => 'Phobject', + 'DiffusionCommitRemarkupRule' => 'PhabricatorRemarkupRuleObject', 'DiffusionCommitTagsController' => 'DiffusionController', 'DiffusionController' => 'PhabricatorController', 'DiffusionDiffController' => 'DiffusionController', @@ -2743,7 +2995,6 @@ phutil_register_library_map(array( 'DiffusionExternalController' => 'DiffusionController', 'DiffusionFileContentQuery' => 'DiffusionQuery', 'DiffusionGitBranchTestCase' => 'PhabricatorTestCase', - 'DiffusionGitCommitParentsQuery' => 'DiffusionCommitParentsQuery', 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionGitRequest' => 'DiffusionRequest', @@ -2756,21 +3007,29 @@ phutil_register_library_map(array( 'DiffusionLastModifiedController' => 'DiffusionController', 'DiffusionLintController' => 'DiffusionController', 'DiffusionLintDetailsController' => 'DiffusionController', + 'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery', + 'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery', + 'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelQuery' => 'Phobject', 'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery', - 'DiffusionMercurialCommitParentsQuery' => 'DiffusionCommitParentsQuery', 'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionMercurialRequest' => 'DiffusionRequest', 'DiffusionMercurialResponse' => 'AphrontResponse', + 'DiffusionMirrorDeleteController' => 'DiffusionController', + 'DiffusionMirrorEditController' => 'DiffusionController', 'DiffusionPathCompleteController' => 'DiffusionController', 'DiffusionPathQueryTestCase' => 'PhabricatorTestCase', 'DiffusionPathValidateController' => 'DiffusionController', + 'DiffusionPushLogListController' => + array( + 0 => 'DiffusionController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), 'DiffusionQuery' => 'PhabricatorQuery', 'DiffusionRawDiffQuery' => 'DiffusionQuery', - 'DiffusionRemarkupRule' => 'PhabricatorRemarkupRuleObject', 'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryDefaultController' => 'DiffusionController', @@ -2779,12 +3038,12 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditController' => 'DiffusionController', + 'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditEncodingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditHostingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditLocalController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditMainController' => 'DiffusionRepositoryEditController', - 'DiffusionRepositoryEditPolicyController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryListController' => array( @@ -2792,14 +3051,24 @@ phutil_register_library_map(array( 1 => 'PhabricatorApplicationSearchResultsControllerInterface', ), 'DiffusionRepositoryNewController' => 'DiffusionController', + 'DiffusionRepositoryRef' => 'Phobject', + 'DiffusionRepositoryRemarkupRule' => 'PhabricatorRemarkupRuleObject', + 'DiffusionResolveUserQuery' => 'Phobject', 'DiffusionSSHGitReceivePackWorkflow' => 'DiffusionSSHGitWorkflow', 'DiffusionSSHGitUploadPackWorkflow' => 'DiffusionSSHGitWorkflow', 'DiffusionSSHGitWorkflow' => 'DiffusionSSHWorkflow', + 'DiffusionSSHMercurialServeWorkflow' => 'DiffusionSSHMercurialWorkflow', + 'DiffusionSSHMercurialWireClientProtocolChannel' => 'PhutilProtocolChannel', + 'DiffusionSSHMercurialWireTestCase' => 'PhabricatorTestCase', + 'DiffusionSSHMercurialWorkflow' => 'DiffusionSSHWorkflow', + 'DiffusionSSHSubversionServeWorkflow' => 'DiffusionSSHSubversionWorkflow', + 'DiffusionSSHSubversionWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DiffusionServeController' => 'DiffusionController', 'DiffusionSetPasswordPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'AphrontUsageException', - 'DiffusionSvnCommitParentsQuery' => 'DiffusionCommitParentsQuery', + 'DiffusionSubversionWireProtocol' => 'Phobject', + 'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase', 'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionSvnRequest' => 'DiffusionRequest', @@ -2852,7 +3121,7 @@ phutil_register_library_map(array( 'DivinerReturnTableView' => 'AphrontTagView', 'DivinerSectionView' => 'AphrontTagView', 'DivinerStaticPublisher' => 'DivinerPublisher', - 'DivinerWorkflow' => 'PhutilArgumentWorkflow', + 'DivinerWorkflow' => 'PhabricatorManagementWorkflow', 'DoorkeeperBridge' => 'Phobject', 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', @@ -2871,37 +3140,98 @@ phutil_register_library_map(array( 'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule', 'DoorkeeperRemarkupRuleAsana' => 'DoorkeeperRemarkupRule', 'DoorkeeperRemarkupRuleJIRA' => 'DoorkeeperRemarkupRule', + 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAllocatorWorker' => 'PhabricatorWorker', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', + 'DrydockBlueprint' => + array( + 0 => 'DrydockDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'DrydockBlueprintController' => 'DrydockController', + 'DrydockBlueprintCreateController' => 'DrydockBlueprintController', + 'DrydockBlueprintEditController' => 'DrydockBlueprintController', + 'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor', + 'DrydockBlueprintListController' => + array( + 0 => 'DrydockBlueprintController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'DrydockBlueprintQuery' => 'DrydockQuery', + 'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction', + 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'DrydockBlueprintViewController' => 'DrydockBlueprintController', + 'DrydockCapabilityCreateBlueprints' => 'PhabricatorPolicyCapability', + 'DrydockCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', + 'DrydockCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'DrydockCommandInterface' => 'DrydockInterface', + 'DrydockConsoleController' => 'DrydockController', 'DrydockController' => 'PhabricatorController', 'DrydockDAO' => 'PhabricatorLiskDAO', - 'DrydockLease' => 'DrydockDAO', - 'DrydockLeaseListController' => 'DrydockController', - 'DrydockLeaseQuery' => 'PhabricatorOffsetPagedQuery', - 'DrydockLeaseReleaseController' => 'DrydockController', + 'DrydockFilesystemInterface' => 'DrydockInterface', + 'DrydockLease' => + array( + 0 => 'DrydockDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'DrydockLeaseController' => 'DrydockController', + 'DrydockLeaseListController' => + array( + 0 => 'DrydockLeaseController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'DrydockLeaseQuery' => 'DrydockQuery', + 'DrydockLeaseReleaseController' => 'DrydockLeaseController', + 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'DrydockConstants', - 'DrydockLeaseViewController' => 'DrydockController', + 'DrydockLeaseViewController' => 'DrydockLeaseController', 'DrydockLocalCommandInterface' => 'DrydockCommandInterface', - 'DrydockLocalHostBlueprint' => 'DrydockBlueprint', - 'DrydockLog' => 'DrydockDAO', + 'DrydockLocalHostBlueprintImplementation' => 'DrydockBlueprintImplementation', + 'DrydockLog' => + array( + 0 => 'DrydockDAO', + 1 => 'PhabricatorPolicyInterface', + ), 'DrydockLogController' => 'DrydockController', - 'DrydockLogQuery' => 'PhabricatorOffsetPagedQuery', + 'DrydockLogListController' => + array( + 0 => 'DrydockLogController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'DrydockLogQuery' => 'DrydockQuery', + 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementCreateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementWaitForLeaseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementWorkflow' => 'PhutilArgumentWorkflow', - 'DrydockResource' => 'DrydockDAO', - 'DrydockResourceCloseController' => 'DrydockController', - 'DrydockResourceListController' => 'DrydockController', - 'DrydockResourceQuery' => 'PhabricatorOffsetPagedQuery', + 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'DrydockPHIDTypeBlueprint' => 'PhabricatorPHIDType', + 'DrydockPHIDTypeLease' => 'PhabricatorPHIDType', + 'DrydockPHIDTypeResource' => 'PhabricatorPHIDType', + 'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation', + 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'DrydockResource' => + array( + 0 => 'DrydockDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'DrydockResourceCloseController' => 'DrydockResourceController', + 'DrydockResourceController' => 'DrydockController', + 'DrydockResourceListController' => + array( + 0 => 'DrydockResourceController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'DrydockResourceQuery' => 'DrydockQuery', + 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'DrydockConstants', - 'DrydockResourceViewController' => 'DrydockController', + 'DrydockResourceViewController' => 'DrydockResourceController', + 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'DrydockWebrootInterface' => 'DrydockInterface', - 'DrydockWorkingCopyBlueprint' => 'DrydockBlueprint', + 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'FeedPublisherHTTPWorker' => 'FeedPushWorker', 'FeedPublisherWorker' => 'FeedPushWorker', 'FeedPushWorker' => 'PhabricatorWorker', @@ -2913,14 +3243,23 @@ phutil_register_library_map(array( 0 => 'HarbormasterDAO', 1 => 'PhabricatorPolicyInterface', ), + 'HarbormasterBuildActionController' => 'HarbormasterController', 'HarbormasterBuildArtifact' => array( 0 => 'HarbormasterDAO', 1 => 'PhabricatorPolicyInterface', ), + 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildCommand' => 'HarbormasterDAO', + 'HarbormasterBuildEngine' => 'Phobject', 'HarbormasterBuildItem' => 'HarbormasterDAO', 'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'HarbormasterBuildLog' => 'HarbormasterDAO', + 'HarbormasterBuildLog' => + array( + 0 => 'HarbormasterDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlan' => array( 0 => 'HarbormasterDAO', @@ -2940,17 +3279,21 @@ phutil_register_library_map(array( 1 => 'PhabricatorPolicyInterface', ), 'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'HarbormasterBuildTarget' => 'HarbormasterDAO', - 'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'HarbormasterBuildWorker' => 'PhabricatorWorker', - 'HarbormasterBuildable' => + 'HarbormasterBuildTarget' => array( 0 => 'HarbormasterDAO', 1 => 'PhabricatorPolicyInterface', ), - 'HarbormasterBuildableApplyController' => 'HarbormasterController', - 'HarbormasterBuildableArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'HarbormasterBuildableEditController' => 'HarbormasterController', + 'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildViewController' => 'HarbormasterController', + 'HarbormasterBuildWorker' => 'HarbormasterWorker', + 'HarbormasterBuildable' => + array( + 0 => 'HarbormasterDAO', + 1 => 'PhabricatorPolicyInterface', + 2 => 'HarbormasterBuildableInterface', + ), + 'HarbormasterBuildableActionController' => 'HarbormasterController', 'HarbormasterBuildableListController' => array( 0 => 'HarbormasterController', @@ -2962,26 +3305,37 @@ phutil_register_library_map(array( 'HarbormasterCapabilityManagePlans' => 'PhabricatorPolicyCapability', 'HarbormasterController' => 'PhabricatorController', 'HarbormasterDAO' => 'PhabricatorLiskDAO', + 'HarbormasterHTTPRequestBuildStepImplementation' => 'VariableBuildStepImplementation', + 'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow', + 'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterPHIDTypeBuild' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildItem' => 'PhabricatorPHIDType', + 'HarbormasterPHIDTypeBuildLog' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildPlan' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildStep' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildTarget' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildable' => 'PhabricatorPHIDType', - 'HarbormasterPlanController' => 'PhabricatorController', + 'HarbormasterPlanController' => 'HarbormasterController', + 'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController', 'HarbormasterPlanListController' => array( 0 => 'HarbormasterPlanController', 1 => 'PhabricatorApplicationSearchResultsControllerInterface', ), + 'HarbormasterPlanOrderController' => 'HarbormasterController', + 'HarbormasterPlanRunController' => 'HarbormasterController', 'HarbormasterPlanViewController' => 'HarbormasterPlanController', 'HarbormasterRemarkupRule' => 'PhabricatorRemarkupRuleObject', 'HarbormasterScratchTable' => 'HarbormasterDAO', 'HarbormasterStepAddController' => 'HarbormasterController', 'HarbormasterStepDeleteController' => 'HarbormasterController', 'HarbormasterStepEditController' => 'HarbormasterController', + 'HarbormasterTargetWorker' => 'HarbormasterWorker', + 'HarbormasterThrowExceptionBuildStep' => 'BuildStepImplementation', + 'HarbormasterUIEventListener' => 'PhabricatorEventListener', + 'HarbormasterWorker' => 'PhabricatorWorker', 'HeraldAction' => 'HeraldDAO', 'HeraldApplyTranscript' => 'HeraldDAO', 'HeraldCapabilityManageGlobalRules' => 'PhabricatorPolicyCapability', @@ -2999,7 +3353,11 @@ phutil_register_library_map(array( 'HeraldNewController' => 'HeraldController', 'HeraldPHIDTypeRule' => 'PhabricatorPHIDType', 'HeraldPholioMockAdapter' => 'HeraldAdapter', + 'HeraldPreCommitAdapter' => 'HeraldAdapter', + 'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter', + 'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter', 'HeraldRecursiveConditionsException' => 'Exception', + 'HeraldRemarkupRule' => 'PhabricatorRemarkupRuleObject', 'HeraldRule' => array( 0 => 'HeraldDAO', @@ -3029,12 +3387,17 @@ phutil_register_library_map(array( 1 => 'PhabricatorPolicyInterface', ), 'HeraldTranscriptController' => 'HeraldController', + 'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector', 'HeraldTranscriptListController' => 'HeraldController', 'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HeraldTranscriptTestCase' => 'PhabricatorTestCase', 'JavelinReactorExample' => 'PhabricatorUIExample', 'JavelinUIExample' => 'PhabricatorUIExample', 'JavelinViewExample' => 'PhabricatorUIExample', 'JavelinViewExampleServerView' => 'AphrontView', + 'LeaseHostBuildStepImplementation' => 'BuildStepImplementation', + 'LegalpadCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', + 'LegalpadCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'LegalpadController' => 'PhabricatorController', 'LegalpadDAO' => 'PhabricatorLiskDAO', 'LegalpadDocument' => @@ -3060,7 +3423,14 @@ phutil_register_library_map(array( 'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'LegalpadDocumentSignController' => 'LegalpadController', - 'LegalpadDocumentSignature' => 'LegalpadDAO', + 'LegalpadDocumentSignature' => + array( + 0 => 'LegalpadDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'LegalpadDocumentSignatureListController' => 'LegalpadController', + 'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'LegalpadDocumentSignatureVerificationController' => 'LegalpadController', 'LegalpadDocumentViewController' => 'LegalpadController', 'LegalpadMockMailReceiver' => 'PhabricatorObjectMailReceiver', 'LegalpadReplyHandler' => 'PhabricatorMailReplyHandler', @@ -3151,6 +3521,9 @@ phutil_register_library_map(array( 'ManiphestView' => 'AphrontView', 'MetaMTANotificationType' => 'MetaMTAConstants', 'MetaMTAReceivedMailStatus' => 'MetaMTAConstants', + 'NuanceCapabilitySourceDefaultEdit' => 'PhabricatorPolicyCapability', + 'NuanceCapabilitySourceDefaultView' => 'PhabricatorPolicyCapability', + 'NuanceCapabilitySourceManage' => 'PhabricatorPolicyCapability', 'NuanceController' => 'PhabricatorController', 'NuanceDAO' => 'PhabricatorLiskDAO', 'NuanceItem' => @@ -3169,6 +3542,7 @@ phutil_register_library_map(array( 'NuancePHIDTypeQueue' => 'PhabricatorPHIDType', 'NuancePHIDTypeRequestor' => 'PhabricatorPHIDType', 'NuancePHIDTypeSource' => 'PhabricatorPHIDType', + 'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition', 'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'NuanceQueue' => array( @@ -3197,6 +3571,7 @@ phutil_register_library_map(array( 0 => 'NuanceDAO', 1 => 'PhabricatorPolicyInterface', ), + 'NuanceSourceDefinition' => 'Phobject', 'NuanceSourceEditController' => 'NuanceController', 'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceSourceQuery' => 'NuanceQuery', @@ -3242,20 +3617,56 @@ phutil_register_library_map(array( 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', + 'PHUITagExample' => 'PhabricatorUIExample', + 'PHUITagView' => 'AphrontView', 'PHUITextExample' => 'PhabricatorUIExample', 'PHUITextView' => 'AphrontTagView', 'PHUIWorkboardExample' => 'PhabricatorUIExample', - 'PHUIWorkboardView' => 'AphrontView', - 'PHUIWorkpanelView' => 'AphrontView', + 'PHUIWorkboardView' => 'AphrontTagView', + 'PHUIWorkpanelView' => 'AphrontTagView', 'PackageCreateMail' => 'PackageMail', 'PackageDeleteMail' => 'PackageMail', 'PackageMail' => 'PhabricatorMail', 'PackageModifyMail' => 'PackageMail', + 'PassphraseAbstractKey' => 'Phobject', + 'PassphraseController' => 'PhabricatorController', + 'PassphraseCredential' => + array( + 0 => 'PassphraseDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PassphraseCredentialControl' => 'AphrontFormControl', + 'PassphraseCredentialCreateController' => 'PassphraseController', + 'PassphraseCredentialDestroyController' => 'PassphraseController', + 'PassphraseCredentialEditController' => 'PassphraseController', + 'PassphraseCredentialListController' => + array( + 0 => 'PassphraseController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PassphraseCredentialRevealController' => 'PassphraseController', + 'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PassphraseCredentialTransaction' => 'PhabricatorApplicationTransaction', + 'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PassphraseCredentialType' => 'Phobject', + 'PassphraseCredentialTypePassword' => 'PassphraseCredentialType', + 'PassphraseCredentialTypeSSHPrivateKey' => 'PassphraseCredentialType', + 'PassphraseCredentialTypeSSHPrivateKeyFile' => 'PassphraseCredentialTypeSSHPrivateKey', + 'PassphraseCredentialTypeSSHPrivateKeyText' => 'PassphraseCredentialTypeSSHPrivateKey', + 'PassphraseCredentialViewController' => 'PassphraseController', + 'PassphraseDAO' => 'PhabricatorLiskDAO', + 'PassphrasePHIDTypeCredential' => 'PhabricatorPHIDType', + 'PassphrasePasswordKey' => 'PassphraseAbstractKey', + 'PassphraseSSHKey' => 'PassphraseAbstractKey', + 'PassphraseSecret' => 'PassphraseDAO', 'PasteCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', 'PasteEmbedView' => 'AphrontView', 'PasteMockMailReceiver' => 'PhabricatorObjectMailReceiver', 'PasteReplyHandler' => 'PhabricatorMailReplyHandler', + 'PeopleCapabilityBrowseUserDirectory' => 'PhabricatorPolicyCapability', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase', @@ -3283,6 +3694,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationConpherence' => 'PhabricatorApplication', 'PhabricatorApplicationCountdown' => 'PhabricatorApplication', 'PhabricatorApplicationDaemons' => 'PhabricatorApplication', + 'PhabricatorApplicationDashboard' => 'PhabricatorApplication', 'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationDifferential' => 'PhabricatorApplication', 'PhabricatorApplicationDiffusion' => 'PhabricatorApplication', @@ -3297,6 +3709,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationFlags' => 'PhabricatorApplication', 'PhabricatorApplicationHarbormaster' => 'PhabricatorApplication', 'PhabricatorApplicationHerald' => 'PhabricatorApplication', + 'PhabricatorApplicationHome' => 'PhabricatorApplication', 'PhabricatorApplicationLaunchView' => 'AphrontView', 'PhabricatorApplicationLegalpad' => 'PhabricatorApplication', 'PhabricatorApplicationMacro' => 'PhabricatorApplication', @@ -3307,12 +3720,14 @@ phutil_register_library_map(array( 'PhabricatorApplicationOwners' => 'PhabricatorApplication', 'PhabricatorApplicationPHIDTypeApplication' => 'PhabricatorPHIDType', 'PhabricatorApplicationPHPAST' => 'PhabricatorApplication', + 'PhabricatorApplicationPassphrase' => 'PhabricatorApplication', 'PhabricatorApplicationPaste' => 'PhabricatorApplication', 'PhabricatorApplicationPeople' => 'PhabricatorApplication', 'PhabricatorApplicationPhame' => 'PhabricatorApplication', 'PhabricatorApplicationPhlux' => 'PhabricatorApplication', 'PhabricatorApplicationPholio' => 'PhabricatorApplication', 'PhabricatorApplicationPhortune' => 'PhabricatorApplication', + 'PhabricatorApplicationPhragment' => 'PhabricatorApplication', 'PhabricatorApplicationPhrequent' => 'PhabricatorApplication', 'PhabricatorApplicationPhriction' => 'PhabricatorApplication', 'PhabricatorApplicationPolicy' => 'PhabricatorApplication', @@ -3389,7 +3804,7 @@ phutil_register_library_map(array( 'PhabricatorAuditListView' => 'AphrontView', 'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow', - 'PhabricatorAuditManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAuditPreviewController' => 'PhabricatorAuditController', 'PhabricatorAuditReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorAuthAccountView' => 'AphrontView', @@ -3404,7 +3819,8 @@ phutil_register_library_map(array( 'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow', - 'PhabricatorAuthManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 'PhabricatorAuthProviderConfig' => @@ -3432,6 +3848,14 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderPassword' => 'PhabricatorAuthProvider', 'PhabricatorAuthProviderPersona' => 'PhabricatorAuthProvider', 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController', + 'PhabricatorAuthSession' => + array( + 0 => 'PhabricatorAuthDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhabricatorAuthSessionEngine' => 'Phobject', + 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', + 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', @@ -3456,8 +3880,11 @@ phutil_register_library_map(array( 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', 'PhabricatorBusyExample' => 'PhabricatorUIExample', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', - 'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorCacheManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector', + 'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow', + 'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector', + 'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', @@ -3467,6 +3894,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController', 'PhabricatorCampfireProtocolAdapter' => 'PhabricatorBotBaseStreamingProtocolAdapter', + 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChatLogChannel' => array( @@ -3485,6 +3913,7 @@ phutil_register_library_map(array( ), 'PhabricatorChatLogEventType' => 'PhabricatorChatLogConstants', 'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorCommonPasswords' => 'Phobject', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', 'PhabricatorConduitConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -3532,7 +3961,7 @@ phutil_register_library_map(array( 'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow', - 'PhabricatorConfigManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorConfigOption' => array( 0 => 'Phobject', @@ -3548,6 +3977,7 @@ phutil_register_library_map(array( 'PhabricatorConpherencePHIDTypeThread' => 'PhabricatorPHIDType', 'PhabricatorContentSourceView' => 'AphrontView', 'PhabricatorController' => 'AphrontController', + 'PhabricatorCookies' => 'Phobject', 'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorCountdown' => array( @@ -3597,6 +4027,7 @@ phutil_register_library_map(array( 'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO', 'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogEventsView' => 'AphrontView', + 'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogListView' => 'AphrontView', 'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -3609,7 +4040,32 @@ phutil_register_library_map(array( 'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow', 'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow', - 'PhabricatorDaemonManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector', + 'PhabricatorDashboard' => + array( + 0 => 'PhabricatorDashboardDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhabricatorDashboardController' => 'PhabricatorController', + 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', + 'PhabricatorDashboardListController' => + array( + 0 => 'PhabricatorDashboardController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'PhabricatorDashboardPHIDTypeDashboard' => 'PhabricatorPHIDType', + 'PhabricatorDashboardPHIDTypePanel' => 'PhabricatorPHIDType', + 'PhabricatorDashboardPanel' => 'PhabricatorDashboardDAO', + 'PhabricatorDashboardPanelListController' => + array( + 0 => 'PhabricatorDashboardController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDataNotAttachedException' => 'Exception', 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDefaultFileStorageEngineSelector' => 'PhabricatorFileStorageEngineSelector', @@ -3618,8 +4074,6 @@ phutil_register_library_map(array( 'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions', - 'PhabricatorDirectoryController' => 'PhabricatorController', - 'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController', 'PhabricatorDisabledUserController' => 'PhabricatorAuthController', 'PhabricatorDisqusConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDraft' => 'PhabricatorDraftDAO', @@ -3663,7 +4117,7 @@ phutil_register_library_map(array( 'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow', - 'PhabricatorFactManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', @@ -3677,7 +4131,7 @@ phutil_register_library_map(array( 1 => 'PhabricatorApplicationSearchResultsControllerInterface', ), 'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow', - 'PhabricatorFeedManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFeedPublicStreamController' => 'PhabricatorFeedController', 'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -3732,6 +4186,7 @@ phutil_register_library_map(array( 'PhabricatorFileShortcutController' => 'PhabricatorFileController', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileStorageConfigurationException' => 'Exception', + 'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorFileTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction', @@ -3746,7 +4201,7 @@ phutil_register_library_map(array( 'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow', - 'PhabricatorFilesManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFlag' => array( 0 => 'PhabricatorFlagDAO', @@ -3768,15 +4223,21 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', 'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorFormExample' => 'PhabricatorUIExample', + 'PhabricatorGarbageCollector' => 'Phobject', 'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', 'PhabricatorGestureExample' => 'PhabricatorUIExample', + 'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream', 'PhabricatorGlobalLock' => 'PhutilLock', 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorHashTestCase' => 'PhabricatorTestCase', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', + 'PhabricatorHomeController' => 'PhabricatorController', + 'PhabricatorHomeMainController' => 'PhabricatorHomeController', + 'PhabricatorHomeQuickCreateController' => 'PhabricatorHomeController', 'PhabricatorHovercardExample' => 'PhabricatorUIExample', 'PhabricatorHovercardView' => 'AphrontView', 'PhabricatorIRCBot' => 'PhabricatorDaemon', @@ -3793,7 +4254,7 @@ phutil_register_library_map(array( 'PhabricatorLegalpadPHIDTypeDocument' => 'PhabricatorPHIDType', 'PhabricatorLintEngine' => 'PhutilLintEngine', 'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow', - 'PhabricatorLipsumManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', @@ -3824,19 +4285,21 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', + 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', - 'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementResendWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorMailManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', + 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMailingListPHIDTypeList' => 'PhabricatorPHIDType', 'PhabricatorMailingListQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMailingListSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -3851,11 +4314,13 @@ phutil_register_library_map(array( 'PhabricatorMainMenuIconView' => 'AphrontView', 'PhabricatorMainMenuSearchView' => 'AphrontView', 'PhabricatorMainMenuView' => 'AphrontView', + 'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 'PhabricatorMarkupOneOff' => 'PhabricatorMarkupInterface', 'PhabricatorMarkupPreviewController' => 'PhabricatorController', + 'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream', 'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMetaMTAController' => 'PhabricatorController', @@ -3864,11 +4329,13 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase', + 'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingList' => array( 0 => 'PhabricatorMetaMTADAO', 1 => 'PhabricatorPolicyInterface', ), + 'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAPermanentFailureException' => 'Exception', 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception', @@ -3971,7 +4438,9 @@ phutil_register_library_map(array( 'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPasteViewController' => 'PhabricatorPasteController', + 'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController', 'PhabricatorPeopleController' => 'PhabricatorController', + 'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController', 'PhabricatorPeopleEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleHovercardEventListener' => 'PhabricatorEventListener', 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', @@ -4019,10 +4488,11 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface', 'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow', - 'PhabricatorPolicyManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', 'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPolicyRuleAdministrators' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleLegalpadSignature' => 'PhabricatorPolicyRule', 'PhabricatorPolicyRuleLunarPhase' => 'PhabricatorPolicyRule', 'PhabricatorPolicyRuleProjects' => 'PhabricatorPolicyRule', 'PhabricatorPolicyRuleUsers' => 'PhabricatorPolicyRule', @@ -4036,6 +4506,7 @@ phutil_register_library_map(array( 2 => 'PhabricatorPolicyInterface', ), 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', + 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectController', 'PhabricatorProjectColumn' => array( 0 => 'PhabricatorProjectDAO', @@ -4057,6 +4528,7 @@ phutil_register_library_map(array( 1 => 'PhabricatorApplicationSearchResultsControllerInterface', ), 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', + 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 'PhabricatorProjectNameCollisionException' => 'Exception', 'PhabricatorProjectPHIDTypeColumn' => 'PhabricatorPHIDType', 'PhabricatorProjectPHIDTypeProject' => 'PhabricatorPHIDType', @@ -4110,6 +4582,7 @@ phutil_register_library_map(array( 1 => 'PhabricatorPolicyInterface', 2 => 'PhabricatorFlaggableInterface', 3 => 'PhabricatorTokenReceiverInterface', + 4 => 'HarbormasterBuildableInterface', ), 'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO', @@ -4125,24 +4598,52 @@ phutil_register_library_map(array( 'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', + 'PhabricatorRepositoryGraphStream' => 'Phobject', 'PhabricatorRepositoryListController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryManagementDeleteWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementEditWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow', - 'PhabricatorRepositoryManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', + 'PhabricatorRepositoryMirror' => + array( + 0 => 'PhabricatorRepositoryDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine', + 'PhabricatorRepositoryMirrorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPHIDTypeArcanistProject' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPHIDTypeCommit' => 'PhabricatorPHIDType', + 'PhabricatorRepositoryPHIDTypeMirror' => 'PhabricatorPHIDType', + 'PhabricatorRepositoryPHIDTypePushLog' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPHIDTypeRepository' => 'PhabricatorPHIDType', + 'PhabricatorRepositoryParsedChange' => 'Phobject', 'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon', + 'PhabricatorRepositoryPushLog' => + array( + 0 => 'PhabricatorRepositoryDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRepositoryRefCursor' => + array( + 0 => 'PhabricatorRepositoryDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine', - 'PhabricatorRepositoryShortcut' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', @@ -4150,9 +4651,14 @@ phutil_register_library_map(array( 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRepositoryURINormalizer' => 'Phobject', + 'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase', + 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', - 'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorSSHLog' => 'Phobject', + 'PhabricatorSSHPassthruCommand' => 'Phobject', + 'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSavedQuery' => array( 0 => 'PhabricatorSearchDAO', @@ -4173,11 +4679,12 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineMySQL' => 'PhabricatorSearchEngine', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow', - 'PhabricatorSearchManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchQuery' => 'PhabricatorSearchDAO', 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', + 'PhabricatorSearchWorker' => 'PhabricatorWorker', 'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSettingsAdjustController' => 'PhabricatorController', @@ -4196,10 +4703,12 @@ phutil_register_library_map(array( 'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel', + 'PhabricatorSettingsPanelSessions' => 'PhabricatorSettingsPanel', 'PhabricatorSetupCheckAPC' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckAuth' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckBaseURI' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckBinaries' => 'PhabricatorSetupCheck', + 'PhabricatorSetupCheckDaemons' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckDatabase' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckExtensions' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckExtraConfig' => 'PhabricatorSetupCheck', @@ -4268,15 +4777,13 @@ phutil_register_library_map(array( 'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', - 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook', 'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions', - 'PhabricatorTagExample' => 'PhabricatorUIExample', - 'PhabricatorTagView' => 'AphrontView', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 'PhabricatorTestCase' => 'ArcanistPhutilTestCase', 'PhabricatorTestController' => 'PhabricatorController', @@ -4317,6 +4824,7 @@ phutil_register_library_map(array( 'PhabricatorTwoColumnExample' => 'PhabricatorUIExample', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', + 'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIListFilterExample' => 'PhabricatorUIExample', 'PhabricatorUINotificationExample' => 'PhabricatorUIExample', @@ -4409,6 +4917,7 @@ phutil_register_library_map(array( 'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhameBlogSkin' => 'PhabricatorController', 'PhameBlogViewController' => 'PhameController', + 'PhameCelerityResources' => 'CelerityResources', 'PhameController' => 'PhabricatorController', 'PhameDAO' => 'PhabricatorLiskDAO', 'PhamePost' => @@ -4448,6 +4957,7 @@ phutil_register_library_map(array( 'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhluxViewController' => 'PhluxController', 'PholioActionMenuEventListener' => 'PhabricatorEventListener', + 'PholioCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'PholioController' => 'PhabricatorController', 'PholioDAO' => 'PhabricatorLiskDAO', 'PholioImage' => @@ -4555,6 +5065,50 @@ phutil_register_library_map(array( 'PhortuneTestExtraPaymentProvider' => 'PhortunePaymentProvider', 'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider', 'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider', + 'PhragmentBrowseController' => 'PhragmentController', + 'PhragmentCapabilityCanCreate' => 'PhabricatorPolicyCapability', + 'PhragmentController' => 'PhabricatorController', + 'PhragmentCreateController' => 'PhragmentController', + 'PhragmentDAO' => 'PhabricatorLiskDAO', + 'PhragmentFragment' => + array( + 0 => 'PhragmentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhragmentFragmentVersion' => + array( + 0 => 'PhragmentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhragmentHistoryController' => 'PhragmentController', + 'PhragmentPHIDTypeFragment' => 'PhabricatorPHIDType', + 'PhragmentPHIDTypeFragmentVersion' => 'PhabricatorPHIDType', + 'PhragmentPHIDTypeSnapshot' => 'PhabricatorPHIDType', + 'PhragmentPatchController' => 'PhragmentController', + 'PhragmentPatchUtil' => 'Phobject', + 'PhragmentPolicyController' => 'PhragmentController', + 'PhragmentRevertController' => 'PhragmentController', + 'PhragmentSnapshot' => + array( + 0 => 'PhragmentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhragmentSnapshotChild' => + array( + 0 => 'PhragmentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhragmentSnapshotCreateController' => 'PhragmentController', + 'PhragmentSnapshotDeleteController' => 'PhragmentController', + 'PhragmentSnapshotPromoteController' => 'PhragmentController', + 'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhragmentSnapshotViewController' => 'PhragmentController', + 'PhragmentUpdateController' => 'PhragmentController', + 'PhragmentVersionController' => 'PhragmentController', + 'PhragmentZIPController' => 'PhragmentController', 'PhrequentController' => 'PhabricatorController', 'PhrequentDAO' => 'PhabricatorLiskDAO', 'PhrequentListController' => @@ -4681,6 +5235,7 @@ phutil_register_library_map(array( 'PonderVoteSaveController' => 'PonderController', 'ProjectCapabilityCreateProjects' => 'PhabricatorPolicyCapability', 'ProjectRemarkupRule' => 'PhabricatorRemarkupRuleObject', + 'PublishFragmentBuildStepImplementation' => 'VariableBuildStepImplementation', 'QueryFormattingTestCase' => 'PhabricatorTestCase', 'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification', 'ReleephBranch' => @@ -4711,6 +5266,7 @@ phutil_register_library_map(array( 'ReleephDAO' => 'PhabricatorLiskDAO', 'ReleephDefaultFieldSelector' => 'ReleephFieldSelector', 'ReleephDefaultUserView' => 'ReleephUserView', + 'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification', 'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification', 'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification', 'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification', @@ -4782,8 +5338,12 @@ phutil_register_library_map(array( 'ReleephStatusFieldSpecification' => 'ReleephFieldSpecification', 'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification', 'ReleephUserView' => 'AphrontView', + 'ShellLogView' => 'AphrontView', 'SleepBuildStepImplementation' => 'BuildStepImplementation', 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteRemarkupRule' => 'PhabricatorRemarkupRuleObject', + 'UploadArtifactBuildStepImplementation' => 'VariableBuildStepImplementation', + 'VariableBuildStepImplementation' => 'BuildStepImplementation', + 'WaitForPreviousBuildStepImplementation' => 'BuildStepImplementation', ), )); diff --git a/src/aphront/AphrontController.php b/src/aphront/AphrontController.php index 6d84c46195..a591271270 100644 --- a/src/aphront/AphrontController.php +++ b/src/aphront/AphrontController.php @@ -9,7 +9,6 @@ abstract class AphrontController extends Phobject { private $currentApplication; private $delegatingController; - public function setDelegatingController( AphrontController $delegating_controller) { $this->delegatingController = $delegating_controller; @@ -64,4 +63,24 @@ abstract class AphrontController extends Phobject { return $this->currentApplication; } + public function getDefaultResourceSource() { + throw new Exception( + pht( + 'A Controller must implement getDefaultResourceSource() before you '. + 'can invoke requireResource() or initBehavior().')); + } + + public function requireResource($symbol) { + $response = CelerityAPI::getStaticResourceResponse(); + $response->requireResource($symbol, $this->getDefaultResourceSource()); + return $this; + } + + public function initBehavior($name, $config = array()) { + Javelin::initBehavior( + $name, + $config, + $this->getDefaultResourceSource()); + } + } diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index d4d055f8ec..407221d0d3 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -1,10 +1,9 @@ isAjax()) { - $more_info = "(This was an Ajax request, {$token_info}.)"; + $more_info[] = pht('This was an Ajax request.'); } else { - $more_info = "(This was a web request, {$token_info}.)"; + $more_info[] = pht('This was a Web request.'); + } + + if ($token) { + $more_info[] = pht('This request had an invalid CSRF token.'); + } else { + $more_info[] = pht('This request had no CSRF token.'); } // Give a more detailed explanation of how to avoid the exception // in developer mode. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { - $more_info = $more_info . + // TODO: Clean this up, see T1921. + $more_info[] = "To avoid this error, use phabricator_form() to construct forms. " . "If you are already using phabricator_form(), make sure the form " . "'action' uses a relative URI (i.e., begins with a '/'). Forms " . @@ -249,14 +251,11 @@ final class AphrontRequest { // but give the user some indication of what happened since the workflow // is incredibly confusing otherwise. throw new AphrontCSRFException( - "The form you just submitted did not include a valid CSRF token. ". - "This token is a technical security measure which prevents a ". - "certain type of login hijacking attack. However, the token can ". - "become invalid if you leave a page open for more than six hours ". - "without a connection to the internet. To fix this problem: reload ". - "the page, and then resubmit it. All data inserted to the form will ". - "be lost in some browsers so copy them somewhere before reloading.\n\n". - $more_info); + pht( + "You are trying to save some data to Phabricator, but the request ". + "your browser made included an incorrect token. Reload the page ". + "and try again. You may need to clear your cookies.\n\n%s", + implode("\n", $more_info))); } return true; @@ -273,75 +272,105 @@ final class AphrontRequest { return $this->validateCSRF(); } + final public function setCookiePrefix($prefix) { + $this->cookiePrefix = $prefix; + return $this; + } + + final private function getPrefixedCookieName($name) { + if (strlen($this->cookiePrefix)) { + return $this->cookiePrefix.'_'.$name; + } else { + return $name; + } + } + final public function getCookie($name, $default = null) { + $name = $this->getPrefixedCookieName($name); return idx($_COOKIE, $name, $default); } final public function clearCookie($name) { + $name = $this->getPrefixedCookieName($name); $this->setCookie($name, '', time() - (60 * 60 * 24 * 30)); unset($_COOKIE[$name]); } + /** + * Get the domain which cookies should be set on for this request, or null + * if the request does not correspond to a valid cookie domain. + * + * @return PhutilURI|null Domain URI, or null if no valid domain exists. + * + * @task cookie + */ + private function getCookieDomainURI() { + $host = $this->getHost(); + + // If there's no base domain configured, just use whatever the request + // domain is. This makes setup easier, and we'll tell administrators to + // configure a base domain during the setup process. + $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); + if (!strlen($base_uri)) { + return new PhutilURI('http://'.$host.'/'); + } + + $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); + $allowed_uris = array_merge( + array($base_uri), + $alternates); + + foreach ($allowed_uris as $allowed_uri) { + $uri = new PhutilURI($allowed_uri); + if ($uri->getDomain() == $host) { + return $uri; + } + } + + return null; + } + + /** + * Determine if security policy rules will allow cookies to be set when + * responding to the request. + * + * @return bool True if setCookie() will succeed. If this method returns + * false, setCookie() will throw. + * + * @task cookie + */ + final public function canSetCookies() { + return (bool)$this->getCookieDomainURI(); + } + final public function setCookie($name, $value, $expire = null) { $is_secure = false; - // If a base URI has been configured, ensure cookies are only set on that - // domain. Also, use the URI protocol to control SSL-only cookies. - $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); - if ($base_uri) { - $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); - $allowed_uris = array_merge( - array($base_uri), - $alternates); + $base_domain_uri = $this->getCookieDomainURI(); + if (!$base_domain_uri) { + $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); + $accessed_as = $this->getHost(); - $host = $this->getHost(); - - $match = null; - foreach ($allowed_uris as $allowed_uri) { - $uri = new PhutilURI($allowed_uri); - $domain = $uri->getDomain(); - if ($host == $domain) { - $match = $uri; - break; - } - } - - if ($match === null) { - if (count($allowed_uris) > 1) { - throw new Exception( - pht( - 'This Phabricator install is configured as "%s", but you are '. - 'accessing it via "%s". Access Phabricator via the primary '. - 'configured domain, or one of the permitted alternate '. - 'domains: %s. Phabricator will not set cookies on other domains '. - 'for security reasons.', - $base_uri, - $host, - implode(', ', $alternates))); - } else { - throw new Exception( - pht( - 'This Phabricator install is configured as "%s", but you are '. - 'accessing it via "%s". Acccess Phabricator via the primary '. - 'configured domain. Phabricator will not set cookies on other '. - 'domains for security reasons.', - $base_uri, - $host)); - } - } - - $base_domain = $match->getDomain(); - $is_secure = ($match->getProtocol() == 'https'); - } else { - $base_uri = new PhutilURI(PhabricatorEnv::getRequestBaseURI()); - $base_domain = $base_uri->getDomain(); + throw new Exception( + pht( + 'This Phabricator install is configured as "%s", but you are '. + 'using the domain name "%s" to access a page which is trying to '. + 'set a cookie. Acccess Phabricator on the configured primary '. + 'domain or a configured alternate domain. Phabricator will not '. + 'set cookies on other domains for security reasons.', + $configured_as, + $accessed_as)); } + $base_domain = $base_domain_uri->getDomain(); + $is_secure = ($base_domain_uri->getProtocol() == 'https'); + if ($expire === null) { $expire = time() + (60 * 60 * 24 * 365 * 5); } + $name = $this->getPrefixedCookieName($name); if (php_sapi_name() == 'cli') { // Do nothing, to avoid triggering "Cannot modify header information" @@ -466,14 +495,44 @@ final class AphrontRequest { } - public static function getHTTPHeader($name, $default = null) { + /** + * Read the value of an HTTP header from `$_SERVER`, or a similar datasource. + * + * This function accepts a canonical header name, like `"Accept-Encoding"`, + * and looks up the appropriate value in `$_SERVER` (in this case, + * `"HTTP_ACCEPT_ENCODING"`). + * + * @param string Canonical header name, like `"Accept-Encoding"`. + * @param wild Default value to return if header is not present. + * @param array? Read this instead of `$_SERVER`. + * @return string|wild Header value if present, or `$default` if not. + */ + public static function getHTTPHeader($name, $default = null, $data = null) { // PHP mangles HTTP headers by uppercasing them and replacing hyphens with // underscores, then prepending 'HTTP_'. $php_index = strtoupper($name); $php_index = str_replace('-', '_', $php_index); - $php_index = 'HTTP_'.$php_index; - return idx($_SERVER, $php_index, $default); + $try_names = array(); + + $try_names[] = 'HTTP_'.$php_index; + if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') { + // These headers may be available under alternate names. See + // http://www.php.net/manual/en/reserved.variables.server.php#110763 + $try_names[] = $php_index; + } + + if ($data === null) { + $data = $_SERVER; + } + + foreach ($try_names as $try_name) { + if (array_key_exists($try_name, $data)) { + return $data[$try_name]; + } + } + + return $default; } } diff --git a/src/aphront/__tests__/AphrontRequestTestCase.php b/src/aphront/__tests__/AphrontRequestTestCase.php index 95bc224c11..151343ef0b 100644 --- a/src/aphront/__tests__/AphrontRequestTestCase.php +++ b/src/aphront/__tests__/AphrontRequestTestCase.php @@ -131,4 +131,21 @@ final class AphrontRequestTestCase extends PhabricatorTestCase { } } + public function testGetHTTPHeader() { + $server_data = array( + 'HTTP_ACCEPT_ENCODING' => 'duck/quack', + 'CONTENT_TYPE' => 'cow/moo', + ); + + $this->assertEqual( + 'duck/quack', + AphrontRequest::getHTTPHeader('AcCePt-EncOdING', null, $server_data)); + $this->assertEqual( + 'cow/moo', + AphrontRequest::getHTTPHeader('cONTent-TyPE', null, $server_data)); + $this->assertEqual( + null, + AphrontRequest::getHTTPHeader('Pie-Flavor', null, $server_data)); + } + } diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 0b296962f7..26b9e9fc28 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -16,9 +16,6 @@ class AphrontDefaultApplicationConfiguration public function getURIMap() { return $this->getResourceURIMapRules() + array( - '/(?:(?P(?:jump))/)?' => - 'PhabricatorDirectoryMainController', - '/typeahead/' => array( 'common/(?P\w+)/' => 'PhabricatorTypeaheadCommonDatasourceController', @@ -73,7 +70,7 @@ class AphrontDefaultApplicationConfiguration return array( '/res/' => array( '(?:(?P[0-9]+)T/)?'. - '(?Ppkg/)?'. + '(?P[^/]+)/'. '(?P[a-f0-9]{8})/'. '(?P.+\.(?:css|js|jpg|png|swf|gif))' => 'CelerityPhabricatorResourceController', @@ -107,9 +104,12 @@ class AphrontDefaultApplicationConfiguration $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); + $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); + $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($data); $request->setApplicationConfiguration($this); + $request->setCookiePrefix($cookie_prefix); return $request; } @@ -243,18 +243,20 @@ class AphrontDefaultApplicationConfiguration } if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { - $trace = $this->renderStackTrace($ex->getTrace(), $user); + $trace = id(new AphrontStackTraceView()) + ->setUser($user) + ->setTrace($ex->getTrace()); } else { $trace = null; } - $content = hsprintf( - '
'. - '
%s
'. - '%s'. - '
', - $message, - $trace); + $content = phutil_tag( + 'div', + array('class' => 'aphront-unhandled-exception'), + array( + phutil_tag('div', array('class' => 'exception-message'), $message), + $trace, + )); $dialog = new AphrontDialogView(); $dialog @@ -290,106 +292,4 @@ class AphrontDefaultApplicationConfiguration )); } - private function renderStackTrace($trace, PhabricatorUser $user) { - - $libraries = PhutilBootloader::getInstance()->getAllLibraries(); - - // TODO: Make this configurable? - $path = 'https://secure.phabricator.com/diffusion/%s/browse/master/src/'; - - $callsigns = array( - 'arcanist' => 'ARC', - 'phutil' => 'PHU', - 'phabricator' => 'P', - ); - - $rows = array(); - $depth = count($trace); - foreach ($trace as $part) { - $lib = null; - $file = idx($part, 'file'); - $relative = $file; - foreach ($libraries as $library) { - $root = phutil_get_library_root($library); - if (Filesystem::isDescendant($file, $root)) { - $lib = $library; - $relative = Filesystem::readablePath($file, $root); - break; - } - } - - $where = ''; - if (isset($part['class'])) { - $where .= $part['class'].'::'; - } - if (isset($part['function'])) { - $where .= $part['function'].'()'; - } - - if ($file) { - if (isset($callsigns[$lib])) { - $attrs = array('title' => $file); - try { - $attrs['href'] = $user->loadEditorLink( - '/src/'.$relative, - $part['line'], - $callsigns[$lib]); - } catch (Exception $ex) { - // The database can be inaccessible. - } - if (empty($attrs['href'])) { - $attrs['href'] = sprintf($path, $callsigns[$lib]). - str_replace(DIRECTORY_SEPARATOR, '/', $relative). - '$'.$part['line']; - $attrs['target'] = '_blank'; - } - $file_name = phutil_tag( - 'a', - $attrs, - $relative); - } else { - $file_name = phutil_tag( - 'span', - array( - 'title' => $file, - ), - $relative); - } - $file_name = hsprintf('%s : %d', $file_name, $part['line']); - } else { - $file_name = phutil_tag('em', array(), '(Internal)'); - } - - - $rows[] = array( - $depth--, - $lib, - $file_name, - $where, - ); - } - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - 'Depth', - 'Library', - 'File', - 'Where', - )); - $table->setColumnClasses( - array( - 'n', - '', - '', - 'wide', - )); - - return hsprintf( - '
'. - '
Stack Trace
'. - '%s'. - '
', - $table->render()); - } - } diff --git a/src/aphront/console/DarkConsoleDataController.php b/src/aphront/console/DarkConsoleDataController.php index b860b310d7..3bf9f902db 100644 --- a/src/aphront/console/DarkConsoleDataController.php +++ b/src/aphront/console/DarkConsoleDataController.php @@ -58,12 +58,17 @@ final class DarkConsoleDataController extends PhabricatorController { $panel = $obj->renderPanel(); - if (!empty($_COOKIE['phsid'])) { - $panel = PhutilSafeHTML::applyFunction( - 'str_replace', - $_COOKIE['phsid'], - '(session-key)', - $panel); + // Because cookie names can now be prefixed, wipe out any cookie value + // with the session cookie name anywhere in its name. + $pattern = '('.preg_quote(PhabricatorCookies::COOKIE_SESSION).')'; + foreach ($_COOKIE as $cookie_name => $cookie_value) { + if (preg_match($pattern, $cookie_name)) { + $panel = PhutilSafeHTML::applyFunction( + 'str_replace', + $cookie_value, + '(session-key)', + $panel); + } } $output['panel'][$class] = $panel; diff --git a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php index e0d3cd5f8e..7c96cafe6f 100644 --- a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php @@ -90,13 +90,13 @@ final class DarkConsoleErrorLogPlugin extends DarkConsolePlugin { $table->setHeaders(array('Error')); $table->setNoDataString('No errors.'); - return hsprintf( - '
'. - '
%s
'. - '
%s
'. - '
', - $table->render(), - phutil_implode_html('', $details)); + return phutil_tag( + 'div', + array(), + array( + phutil_tag('div', array(), $table->render()), + phutil_tag('pre', array('class' => 'PhabricatorMonospaced'), $details), + )); } } diff --git a/src/aphront/console/plugin/DarkConsoleEventPlugin.php b/src/aphront/console/plugin/DarkConsoleEventPlugin.php index 3b9236b4a2..bc191e00ea 100644 --- a/src/aphront/console/plugin/DarkConsoleEventPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleEventPlugin.php @@ -42,10 +42,10 @@ final class DarkConsoleEventPlugin extends DarkConsolePlugin { $out = array(); - $out[] = hsprintf( - '
'. - '

Registered Event Listeners

'. - '
'); + $out[] = phutil_tag( + 'div', + array('class' => 'dark-console-panel-header'), + phutil_tag('h1', array(), pht('Registered Event Listeners'))); $rows = array(); foreach ($data['listeners'] as $listener) { @@ -66,10 +66,10 @@ final class DarkConsoleEventPlugin extends DarkConsolePlugin { $out[] = $table->render(); - $out[] = hsprintf( - '
'. - '

Event Log

'. - '
'); + $out[] = phutil_tag( + 'div', + array('class' => 'dark-console-panel-header'), + phutil_tag('h1', array(), pht('Event Log'))); $rows = array(); foreach ($data['events'] as $event) { diff --git a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php index a43a83c785..38538f0564 100644 --- a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php @@ -15,17 +15,36 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { return 'Information about services.'; } + public static function getQueryAnalyzerHeader() { + return 'X-Phabricator-QueryAnalyzer'; + } + + public static function isQueryAnalyzerRequested() { + if (!empty($_REQUEST['__analyze__'])) { + return true; + } + + $header = AphrontRequest::getHTTPHeader(self::getQueryAnalyzerHeader()); + if ($header) { + return true; + } + + return false; + } + /** * @phutil-external-symbol class PhabricatorStartup */ public function generateData() { + $should_analyze = self::isQueryAnalyzerRequested(); + $log = PhutilServiceProfiler::getInstance()->getServiceCallLog(); foreach ($log as $key => $entry) { $config = idx($entry, 'config', array()); unset($log[$key]['config']); - if (empty($_REQUEST['__analyze__'])) { + if (!$should_analyze) { $log[$key]['explain'] = array( 'sev' => 7, 'size' => null, @@ -139,7 +158,7 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { 'analyzeURI' => (string)$this ->getRequestURI() ->alter('__analyze__', true), - 'didAnalyze' => isset($_REQUEST['__analyze__']), + 'didAnalyze' => $should_analyze, ); } @@ -149,21 +168,20 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { $log = $data['log']; $results = array(); - $results[] = hsprintf( - '
'. - '%s'. - '

Calls to External Services

'. - '
'. - '
', - phutil_tag( - 'a', - array( - 'href' => $data['analyzeURI'], - 'class' => $data['didAnalyze'] - ? 'disabled button' - : 'green button', - ), - 'Analyze Query Plans')); + $results[] = phutil_tag( + 'div', + array('class' => 'dark-console-panel-header'), + array( + phutil_tag( + 'a', + array( + 'href' => $data['analyzeURI'], + 'class' => $data['didAnalyze'] ? 'disabled button' : 'green button', + ), + pht('Analyze Query Plans')), + phutil_tag('h1', array(), pht('Calls to External Services')), + phutil_tag('div', array('style' => 'clear: both;')), + )); $page_total = $data['end'] - $data['start']; $totals = array(); diff --git a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php index db40e92e9d..45cf8bcd9d 100644 --- a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php @@ -62,38 +62,41 @@ final class DarkConsoleXHProfPlugin extends DarkConsolePlugin { $result = array(); - $header = hsprintf( - '
'. - '%s'. - '

XHProf Profiler

'. - '
', - phutil_tag( - 'a', - array( - 'href' => $profile_uri, - 'class' => $run - ? 'disabled button' - : 'green button', - ), - 'Profile Page')); + $header = phutil_tag( + 'div', + array('class' => 'dark-console-panel-header'), + array( + phutil_tag( + 'a', + array( + 'href' => $profile_uri, + 'class' => $run ? 'disabled button' : 'green button', + ), + pht('Profile Page')), + phutil_tag('h1', array(), pht('XHProf Profiler')), + )); $result[] = $header; if ($run) { - $result[] = hsprintf( - 'Profile Permalink'. - '', - $run, - $run); + $result[] = phutil_tag( + 'a', + array( + 'href' => "/xhprof/profile/$run/", + 'class' => 'bright-link', + 'style' => 'float: right; margin: 1em 2em 0 0; font-weight: bold;', + 'target' => '_blank', + ), + pht('Profile Permalink')); + $result[] = phutil_tag( + 'iframe', + array('src' => "/xhprof/profile/$run/?frame=true")); } else { - $result[] = hsprintf( - '
'. + $result[] = phutil_tag( + 'div', + array('class' => 'dark-console-no-content'), + pht( 'Profiling was not enabled for this page. Use the button above '. - 'to enable it.'. - '
'); + 'to enable it.')); } return phutil_implode_html("\n", $result); diff --git a/src/aphront/response/AphrontRedirectResponse.php b/src/aphront/response/AphrontRedirectResponse.php index b01cc3644d..bab7b7f261 100644 --- a/src/aphront/response/AphrontRedirectResponse.php +++ b/src/aphront/response/AphrontRedirectResponse.php @@ -8,6 +8,14 @@ class AphrontRedirectResponse extends AphrontResponse { private $uri; + private $stackWhenCreated; + + public function __construct() { + if ($this->shouldStopForDebugging()) { + // If we're going to stop, capture the stack so we can print it out. + $this->stackWhenCreated = id(new Exception())->getTrace(); + } + } public function setURI($uri) { $this->uri = $uri; @@ -33,29 +41,44 @@ class AphrontRedirectResponse extends AphrontResponse { public function buildResponseString() { if ($this->shouldStopForDebugging()) { + $user = new PhabricatorUser(); + $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->setApplicationName('Debug'); $view->setTitle('Stopped on Redirect'); - $error = new AphrontErrorView(); - $error->setSeverity(AphrontErrorView::SEVERITY_NOTICE); - $error->setTitle('Stopped on Redirect'); + $dialog = new AphrontDialogView(); + $dialog->setUser($user); + $dialog->setTitle('Stopped on Redirect'); - $link = phutil_tag( - 'a', - array( - 'href' => $this->getURI(), - ), - 'Continue to: '.$this->getURI()); + $dialog->appendParagraph( + pht( + 'You were stopped here because %s is set in your configuration.', + phutil_tag('tt', array(), 'debug.stop-on-redirect'))); - $error->appendChild(hsprintf( - '

You were stopped here because debug.stop-on-redirect '. - 'is set in your configuration.

'. - '

%s

', - $link)); + $dialog->appendParagraph( + pht( + 'You are being redirected to: %s', + phutil_tag('tt', array(), $this->getURI()))); - $view->appendChild($error); + $dialog->addCancelButton($this->getURI(), pht('Continue')); + + $dialog->appendChild(phutil_tag('br')); + + $dialog->appendChild( + id(new AphrontStackTraceView()) + ->setUser($user) + ->setTrace($this->stackWhenCreated)); + + $dialog->setIsStandalone(true); + $dialog->setWidth(AphrontDialogView::WIDTH_FULL); + + $box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($dialog); + + $view->appendChild($box); return $view->render(); } diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php index 1f6164733a..aef3c7e224 100644 --- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php +++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php @@ -31,4 +31,23 @@ final class PhabricatorAuditCommitStatusConstants { ); } + public static function getStatusColor($code) { + switch ($code) { + case self::CONCERN_RAISED: + $color = 'red'; + break; + case self::NEEDS_AUDIT: + case self::PARTIALLY_AUDITED: + $color = 'orange'; + break; + case self::FULLY_AUDITED: + $color = 'green'; + break; + default: + $color = null; + break; + } + return $color; + } + } diff --git a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php index 8d8ce45e68..f80760d614 100644 --- a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php +++ b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php @@ -40,6 +40,9 @@ final class PhabricatorAuditStatusConstants { case self::AUDIT_REQUIRED: $color = 'orange'; break; + case self::ACCEPTED: + $color = 'green'; + break; default: $color = null; break; diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php index 01b178c1e0..ff72c79de3 100644 --- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php @@ -8,6 +8,8 @@ final class PhabricatorAuditCommentEditor extends PhabricatorEditor { private $auditors = array(); private $ccs = array(); + private $noEmail; + public function __construct(PhabricatorRepositoryCommit $commit) { $this->commit = $commit; return $this; @@ -28,6 +30,11 @@ final class PhabricatorAuditCommentEditor extends PhabricatorEditor { return $this; } + public function setNoEmail($no_email) { + $this->noEmail = $no_email; + return $this; + } + public function addComment(PhabricatorAuditComment $comment) { $commit = $this->commit; @@ -295,9 +302,11 @@ final class PhabricatorAuditCommentEditor extends PhabricatorEditor { $this->publishFeedStory($comment, $feed_phids); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($commit->getPHID()); + ->queueDocumentForIndexing($commit->getPHID()); - $this->sendMail($comment, $other_comments, $inline_comments, $requests); + if (!$this->noEmail) { + $this->sendMail($comment, $other_comments, $inline_comments, $requests); + } } diff --git a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php index a06deea931..001e1ffe48 100644 --- a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php @@ -178,10 +178,6 @@ final class PhabricatorAuditManagementDeleteWorkflow return 0; } - private function getViewer() { - return PhabricatorUser::getOmnipotentUser(); - } - private function loadUsers($users) { $users = $this->parseList($users); if (!$users) { diff --git a/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php index 99ad58fc5e..27bc7702d0 100644 --- a/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php @@ -1,10 +1,6 @@ getEpoch(), $this->user); + $audit_status = $commit->getAuditStatus(); $commit_status = PhabricatorAuditCommitStatusConstants::getStatusName( - $commit->getAuditStatus()); + $audit_status); + $status_color = PhabricatorAuditCommitStatusConstants::getStatusColor( + $audit_status); $item = id(new PHUIObjectItemView()) + ->setBarColor($status_color) ->setObjectName($commit_name) ->setHeader($commit_desc) ->setHref($commit_link) diff --git a/src/applications/auth/application/PhabricatorApplicationAuth.php b/src/applications/auth/application/PhabricatorApplicationAuth.php index da318e7992..3a47a732f0 100644 --- a/src/applications/auth/application/PhabricatorApplicationAuth.php +++ b/src/applications/auth/application/PhabricatorApplicationAuth.php @@ -36,20 +36,25 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication { $item = id(new PHUIListItemView()) ->addClass('core-menu-item') ->setName(pht('Log Out')) - ->setIcon('power') + ->setIcon('logout-sm') ->setWorkflow(true) ->setHref('/logout/') - ->setSelected(($controller instanceof PhabricatorLogoutController)); - $items[] = $item; - } - else { - $item = id(new PHUIListItemView()) - ->addClass('core-menu-item') - ->setName(pht('Log In or Register')) - ->setRenderNameAsTooltip(false) - ->setIcon('authentication') - ->setHref('/login/'); + ->setSelected(($controller instanceof PhabricatorLogoutController)) + ->setOrder(900); $items[] = $item; + } else { + if ($controller instanceof PhabricatorAuthController) { + // Don't show the "Login" item on auth controllers, since they're + // generally all related to logging in anyway. + } else { + $item = id(new PHUIListItemView()) + ->addClass('core-menu-item') + ->setName(pht('Log In or Register')) + ->setRenderNameAsTooltip(false) + ->setIcon('authentication') + ->setHref('/login/'); + $items[] = $item; + } } return $items; diff --git a/src/applications/auth/constants/PhabricatorCommonPasswords.php b/src/applications/auth/constants/PhabricatorCommonPasswords.php new file mode 100644 index 0000000000..1313257553 --- /dev/null +++ b/src/applications/auth/constants/PhabricatorCommonPasswords.php @@ -0,0 +1,70 @@ + Map of common passwords. + * + * @task common + */ + private static function loadWordlist() { + $root = dirname(phutil_get_library_root('phabricator')); + $file = $root.'/externals/wordlist/password.lst'; + $data = Filesystem::readFile($file); + + $words = phutil_split_lines($data, $retain_endings = false); + + $map = array(); + foreach ($words as $key => $word) { + // The wordlist file has some comments at the top, strip those out. + if (preg_match('/^#!comment:/', $word)) { + continue; + } + $map[strtolower($word)] = true; + } + + // Add in some application-specific passwords. + $map += array( + 'phabricator' => true, + 'phab' => true, + 'devtools' => true, + 'differential' => true, + 'codereview' => true, + 'review' => true, + ); + + return $map; + } + +} diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php new file mode 100644 index 0000000000..0bb34569f3 --- /dev/null +++ b/src/applications/auth/constants/PhabricatorCookies.php @@ -0,0 +1,130 @@ +getCookie(self::COOKIE_NEXTURI); + list($set_at, $current_uri) = self::parseNextURICookie($cookie_value); + + // If the cookie was set within the last 2 minutes, don't overwrite it. + // Primarily, this prevents browser requests for resources which do not + // exist (like "humans.txt" and various icons) from overwriting a normal + // URI like "/feed/". + if ($set_at > (time() - 120)) { + return; + } + } + + $new_value = time().','.$next_uri; + $request->setCookie(self::COOKIE_NEXTURI, $new_value); + } + + + /** + * Read the URI out of the Next URI cookie. + * + * @param AphrontRequest Request to examine. + * @return string|null Next URI cookie's URI value. + * + * @task next + */ + public static function getNextURICookie(AphrontRequest $request) { + $cookie_value = $request->getCookie(self::COOKIE_NEXTURI); + list($set_at, $next_uri) = self::parseNExtURICookie($cookie_value); + + return $next_uri; + } + + + /** + * Parse a Next URI cookie into its components. + * + * @param string Raw cookie value. + * @return list List of timestamp and URI. + * + * @task next + */ + private static function parseNextURICookie($cookie) { + // Old cookies look like: /uri + // New cookies look like: timestamp,/uri + + if (!strlen($cookie)) { + return null; + } + + if (strpos($cookie, ',') !== false) { + list($timestamp, $uri) = explode(',', $cookie, 2); + return array((int)$timestamp, $uri); + } + + return array(0, $cookie); + } + +} diff --git a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php index cedf46bd33..5834984a65 100644 --- a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php @@ -70,13 +70,8 @@ final class PhabricatorAuthConfirmLinkController $dialog->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Confirm Link')) - ->setHref($panel_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName())); + $crumbs->addTextCrumb(pht('Confirm Link'), $panel_uri); + $crumbs->addTextCrumb($provider->getProviderName()); return $this->buildApplicationPage( array( diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index 51db692f50..697d306252 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -65,7 +65,7 @@ abstract class PhabricatorAuthController extends PhabricatorController { protected function loginUser(PhabricatorUser $user) { $response = $this->buildLoginValidateResponse($user); - $session_type = 'web'; + $session_type = PhabricatorAuthSession::TYPE_WEB; $event_type = PhabricatorEventType::TYPE_AUTH_WILLLOGINUSER; $event_data = array( @@ -81,14 +81,19 @@ abstract class PhabricatorAuthController extends PhabricatorController { $should_login = $event->getValue('shouldLogin'); if ($should_login) { - $session_key = $user->establishSession($session_type); + $session_key = id(new PhabricatorAuthSessionEngine()) + ->establishSession($session_type, $user->getPHID()); // NOTE: We allow disabled users to login and roadblock them later, so // there's no check for users being disabled here. $request = $this->getRequest(); - $request->setCookie('phusr', $user->getUsername()); - $request->setCookie('phsid', $session_key); + $request->setCookie( + PhabricatorCookies::COOKIE_USERNAME, + $user->getUsername()); + $request->setCookie( + PhabricatorCookies::COOKIE_SESSION, + $session_key); $this->clearRegistrationCookies(); } @@ -100,15 +105,15 @@ abstract class PhabricatorAuthController extends PhabricatorController { $request = $this->getRequest(); // Clear the registration key. - $request->clearCookie('phreg'); + $request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION); // Clear the client ID / OAuth state key. - $request->clearCookie('phcid'); + $request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID); } private function buildLoginValidateResponse(PhabricatorUser $user) { $validate_uri = new PhutilURI($this->getApplicationURI('validate/')); - $validate_uri->setQueryParam('phusr', $user->getUsername()); + $validate_uri->setQueryParam('expect', $user->getUsername()); return id(new AphrontRedirectResponse())->setURI((string)$validate_uri); } @@ -167,7 +172,8 @@ abstract class PhabricatorAuthController extends PhabricatorController { return array($account, $provider, $response); } - $registration_key = $request->getCookie('phreg'); + $registration_key = $request->getCookie( + PhabricatorCookies::COOKIE_REGISTRATION); // NOTE: This registration key check is not strictly necessary, because // we're only creating new accounts, not linking existing accounts. It @@ -180,7 +186,7 @@ abstract class PhabricatorAuthController extends PhabricatorController { // since you could have simply completed the process yourself. if (!$registration_key) { - $response = $this->renderError( + $response = $this->renderError( pht( 'Your browser did not submit a registration key with the request. '. 'You must use the same browser to begin and complete registration. '. diff --git a/src/applications/auth/controller/PhabricatorAuthLinkController.php b/src/applications/auth/controller/PhabricatorAuthLinkController.php index 326474fe2b..44be8adfb3 100644 --- a/src/applications/auth/controller/PhabricatorAuthLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthLinkController.php @@ -79,7 +79,10 @@ final class PhabricatorAuthLinkController $panel_uri = '/settings/panel/external/'; - $request->setCookie('phcid', Filesystem::readRandomCharacters(16)); + $request->setCookie( + PhabricatorCookies::COOKIE_CLIENTID, + Filesystem::readRandomCharacters(16)); + switch ($this->action) { case 'link': $form = $provider->buildLinkForm($this); @@ -115,13 +118,8 @@ final class PhabricatorAuthLinkController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Link Account')) - ->setHref($panel_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName($name))); + $crumbs->addTextCrumb(pht('Link Account'), $panel_uri); + $crumbs->addTextCrumb($provider->getProviderName($name)); return $this->buildApplicationPage( array( diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index deb71a75b1..ffab2fe5bb 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -166,7 +166,9 @@ final class PhabricatorAuthLoginController $account->save(); unset($unguarded); - $this->getRequest()->setCookie('phreg', $registration_key); + $this->getRequest()->setCookie( + PhabricatorCookies::COOKIE_REGISTRATION, + $registration_key); return id(new AphrontRedirectResponse())->setURI($next_uri); } @@ -202,20 +204,12 @@ final class PhabricatorAuthLoginController $crumbs = $this->buildApplicationCrumbs(); if ($this->getRequest()->getUser()->isLoggedIn()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Link Account')) - ->setHref($provider->getSettingsURI())); + $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI()); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Login')) - ->setHref($this->getApplicationURI('start/'))); + $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/')); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName())); + $crumbs->addTextCrumb($provider->getProviderName()); return $this->buildApplicationPage( array( diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php b/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php new file mode 100644 index 0000000000..d0ff45b38c --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php @@ -0,0 +1,40 @@ +getRequest(); + $user = $request->getUser(); + + $wait_for_approval = pht( + "Your account has been created, but needs to be approved by an ". + "administrator. You'll receive an email once your account is approved."); + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle(pht('Wait for Approval')) + ->appendChild($wait_for_approval) + ->addCancelButton('/', pht('Wait Patiently')); + + return $this->buildApplicationPage( + $dialog, + array( + 'title' => pht('Wait For Approval'), + 'device' => true, + )); + } + +} diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 5baeecdeb4..eb8f68a81b 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -59,16 +59,30 @@ final class PhabricatorAuthRegisterController $default_realname = $account->getRealName(); $default_email = $account->getEmail(); if ($default_email) { - // If the account source provided an email but it's not allowed by - // the configuration, just pretend we didn't get an email at all. + // If the account source provided an email, but it's not allowed by + // the configuration, roadblock the user. Previously, we let the user + // pick a valid email address instead, but this does not align well with + // user expectation and it's not clear the cases it enables are valuable. + // See discussion in T3472. if (!PhabricatorUserEmail::isAllowedAddress($default_email)) { - $default_email = null; + return $this->renderError( + array( + pht( + 'The account you are attempting to register with has an invalid '. + 'email address (%s). This Phabricator install only allows '. + 'registration with specific email addresses:', + $default_email), + phutil_tag('br'), + phutil_tag('br'), + PhabricatorUserEmail::describeAllowedAddresses(), + )); } // If the account source provided an email, but another account already // has that email, just pretend we didn't get an email. // TODO: See T3340. + // TODO: See T3472. if ($default_email) { $same_email = id(new PhabricatorUserEmail())->loadOneWhere( @@ -168,6 +182,14 @@ final class PhabricatorAuthRegisterController $errors[] = pht( 'Password is too short (must be at least %d characters long).', $min_len); + } else if ( + PhabricatorCommonPasswords::isCommonPassword($value_password)) { + + $e_password = pht('Very Weak'); + $errors[] = pht( + 'Password is pathologically weak. This password is one of the '. + 'most common passwords in use, and is extremely easy for '. + 'attackers to guess. You must choose a stronger password.'); } else { $e_password = null; } @@ -226,6 +248,19 @@ final class PhabricatorAuthRegisterController $user->setUsername($value_username); $user->setRealname($value_realname); + if ($is_setup) { + $must_approve = false; + } else { + $must_approve = PhabricatorEnv::getEnvConfig( + 'auth.require-approval'); + } + + if ($must_approve) { + $user->setIsApproved(0); + } else { + $user->setIsApproved(1); + } + $user->openTransaction(); $editor = id(new PhabricatorUserEditor()) @@ -251,6 +286,10 @@ final class PhabricatorAuthRegisterController $email_obj->sendVerificationEmail($user); } + if ($must_approve) { + $this->sendWaitingForApprovalEmail($user); + } + return $this->loginUser($user); } catch (AphrontQueryDuplicateKeyException $exception) { $same_username = id(new PhabricatorUser())->loadOneWhere( @@ -281,13 +320,6 @@ final class PhabricatorAuthRegisterController unset($unguarded); } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Registration Failed')); - $error_view->setErrors($errors); - } - $form = id(new AphrontFormView()) ->setUser($request->getUser()); @@ -303,13 +335,20 @@ final class PhabricatorAuthRegisterController } - $form - ->appendChild( + if ($can_edit_username) { + $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phabricator Username')) ->setName('username') ->setValue($value_username) ->setError($e_username)); + } else { + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Phabricator Username')) + ->setValue($value_username) + ->setError($e_username)); + } if ($must_set_password) { $form->appendChild( @@ -371,17 +410,11 @@ final class PhabricatorAuthRegisterController $crumbs = $this->buildApplicationCrumbs(); if ($is_setup) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Setup Admin Account'))); + $crumbs->addTextCrumb(pht('Setup Admin Account')); $title = pht('Welcome to Phabricator'); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Register'))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName())); + $crumbs->addTextCrumb(pht('Register')); + $crumbs->addTextCrumb($provider->getProviderName()); $title = pht('Phabricator Registration'); } @@ -400,7 +433,7 @@ final class PhabricatorAuthRegisterController $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) - ->setFormError($error_view); + ->setFormErrors($errors); return $this->buildApplicationPage( array( @@ -500,4 +533,43 @@ final class PhabricatorAuthRegisterController array($message)); } + private function sendWaitingForApprovalEmail(PhabricatorUser $user) { + $title = '[Phabricator] '.pht( + 'New User "%s" Awaiting Approval', + $user->getUsername()); + + $body = new PhabricatorMetaMTAMailBody(); + + $body->addRawSection( + pht( + 'Newly registered user "%s" is awaiting account approval by an '. + 'administrator.', + $user->getUsername())); + + $body->addTextSection( + pht('APPROVAL QUEUE'), + PhabricatorEnv::getProductionURI( + '/people/query/approval/')); + + $body->addTextSection( + pht('DISABLE APPROVAL QUEUE'), + PhabricatorEnv::getProductionURI( + '/config/edit/auth.require-approval/')); + + $admins = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIsAdmin(true) + ->execute(); + + if (!$admins) { + return; + } + + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(mpull($admins, 'getPHID')) + ->setSubject($title) + ->setBody($body->render()) + ->saveAndSend(); + } + } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 468e1f8617..3c1cb7491d 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -24,18 +24,33 @@ final class PhabricatorAuthStartController return $this->processConduitRequest(); } - if ($request->getCookie('phusr') && $request->getCookie('phsid')) { - // The session cookie is invalid, so clear it. - $request->clearCookie('phusr'); - $request->clearCookie('phsid'); + // If the user gets this far, they aren't logged in, so if they have a + // user session token we can conclude that it's invalid: if it was valid, + // they'd have been logged in above and never made it here. Try to clear + // it and warn the user they may need to nuke their cookies. - return $this->renderError( - pht( - "Your login session is invalid. Try reloading the page and logging ". - "in again. If that does not work, clear your browser cookies.")); + $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); + if (strlen($session_token)) { + $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken( + $session_token); + switch ($kind) { + case PhabricatorAuthSessionEngine::KIND_ANONYMOUS: + // If this is an anonymous session. It's expected that they won't + // be logged in, so we can just continue. + break; + default: + // The session cookie is invalid, so clear it. + $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); + $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); + + return $this->renderError( + pht( + "Your login session is invalid. Try reloading the page and ". + "logging in again. If that does not work, clear your browser ". + "cookies.")); + } } - $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($providers as $key => $provider) { if (!$provider->shouldAllowLogin()) { @@ -71,8 +86,11 @@ final class PhabricatorAuthStartController } if (!$request->isFormPost()) { - $request->setCookie('next_uri', $next_uri); - $request->setCookie('phcid', Filesystem::readRandomCharacters(16)); + PhabricatorCookies::setNextURICookie($request, $next_uri); + + $request->setCookie( + PhabricatorCookies::COOKIE_CLIENTID, + Filesystem::readRandomCharacters(16)); } $not_buttons = array(); @@ -126,9 +144,7 @@ final class PhabricatorAuthStartController $login_message = phutil_safe_html($login_message); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Login'))); + $crumbs->addTextCrumb(pht('Login')); return $this->buildApplicationPage( array( diff --git a/src/applications/auth/controller/PhabricatorAuthValidateController.php b/src/applications/auth/controller/PhabricatorAuthValidateController.php index 9332d479d2..18524163e0 100644 --- a/src/applications/auth/controller/PhabricatorAuthValidateController.php +++ b/src/applications/auth/controller/PhabricatorAuthValidateController.php @@ -13,7 +13,7 @@ final class PhabricatorAuthValidateController $failures = array(); - if (!strlen($request->getStr('phusr'))) { + if (!strlen($request->getStr('expect'))) { return $this->renderErrors( array( pht( @@ -21,8 +21,8 @@ final class PhabricatorAuthValidateController 'phusr'))); } - $expect_phusr = $request->getStr('phusr'); - $actual_phusr = $request->getCookie('phusr'); + $expect_phusr = $request->getStr('expect'); + $actual_phusr = $request->getCookie(PhabricatorCookies::COOKIE_USERNAME); if ($actual_phusr != $expect_phusr) { if ($actual_phusr) { $failures[] = pht( @@ -54,8 +54,8 @@ final class PhabricatorAuthValidateController return $this->renderErrors($failures); } - $next = $request->getCookie('next_uri'); - $request->clearCookie('next_uri'); + $next = PhabricatorCookies::getNextURICookie($request); + $request->clearCookie(PhabricatorCookies::COOKIE_NEXTURI); if (!PhabricatorEnv::isValidLocalWebResource($next)) { $next = '/'; diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 71ba31657f..8dd7be7c25 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -133,9 +133,7 @@ EOBODY; ->setError($e_captcha)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Reset Password'))); + $crumbs->addTextCrumb(pht('Reset Password')); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); diff --git a/src/applications/auth/controller/PhabricatorEmailTokenController.php b/src/applications/auth/controller/PhabricatorEmailTokenController.php index e907a7dcb4..f4cdfa96d2 100644 --- a/src/applications/auth/controller/PhabricatorEmailTokenController.php +++ b/src/applications/auth/controller/PhabricatorEmailTokenController.php @@ -16,19 +16,14 @@ final class PhabricatorEmailTokenController public function processRequest() { $request = $this->getRequest(); + if ($request->getUser()->isLoggedIn()) { + return $this->renderError( + pht('You are already logged in.')); + } + $token = $this->token; $email = $request->getStr('email'); - // NOTE: We need to bind verification to **addresses**, not **users**, - // because we verify addresses when they're used to login this way, and if - // we have a user-based verification you can: - // - // - Add some address you do not own; - // - request a password reset; - // - change the URI in the email to the address you don't own; - // - login via the email link; and - // - get a "verified" address you don't control. - $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $email); @@ -40,6 +35,16 @@ final class PhabricatorEmailTokenController $target_email->getUserPHID()); } + // NOTE: We need to bind verification to **addresses**, not **users**, + // because we verify addresses when they're used to login this way, and if + // we have a user-based verification you can: + // + // - Add some address you do not own; + // - request a password reset; + // - change the URI in the email to the address you don't own; + // - login via the email link; and + // - get a "verified" address you don't control. + if (!$target_email || !$target_user || !$target_user->validateEmailToken($target_email, $token)) { @@ -51,11 +56,12 @@ final class PhabricatorEmailTokenController 'invalid or out of date. Make sure you are copy-and-pasting the '. 'entire link into your browser. You can try again, or request '. 'a new email.'))); - $view->appendChild(hsprintf( - '
'. - '%s'. - '
', - pht('Send Another Email'))); + $view->appendChild(phutil_tag_div( + 'aphront-failure-continue', + phutil_tag( + 'a', + array('class' => 'button', 'href' => '/login/email/'), + pht('Send Another Email')))); return $this->buildStandardPageResponse( $view, @@ -64,29 +70,59 @@ final class PhabricatorEmailTokenController )); } - // Verify email so that clicking the link in the "Welcome" email is good - // enough, without requiring users to go through a second round of email - // verification. + if ($request->isFormPost()) { + // Verify email so that clicking the link in the "Welcome" email is good + // enough, without requiring users to go through a second round of email + // verification. - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $target_email->setIsVerified(1); - $target_email->save(); - unset($unguarded); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $target_email->setIsVerified(1); + $target_email->save(); + unset($unguarded); - $next = '/'; - if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { - $next = '/settings/panel/external/'; - } else if (PhabricatorEnv::getEnvConfig('account.editable')) { - $next = (string)id(new PhutilURI('/settings/panel/password/')) - ->setQueryParams( - array( - 'token' => $token, - 'email' => $email, - )); + $next = '/'; + if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { + $next = '/settings/panel/external/'; + } else if (PhabricatorEnv::getEnvConfig('account.editable')) { + $next = (string)id(new PhutilURI('/settings/panel/password/')) + ->setQueryParams( + array( + 'token' => $token, + 'email' => $email, + )); + } + + PhabricatorCookies::setNextURICookie($request, $next, $force = true); + + return $this->loginUser($target_user); } - $request->setCookie('next_uri', $next); + // NOTE: We need to CSRF here so attackers can't generate an email link, + // then log a user in to an account they control via sneaky invisible + // form submissions. - return $this->loginUser($target_user); + // TODO: Since users can arrive here either through password reset or + // through welcome emails, it might be nice to include the workflow type + // in the URI or query params so we can tailor the messaging. Right now, + // it has to be generic enough to make sense in either workflow, which + // leaves it feeling a little awkward. + + $dialog = id(new AphrontDialogView()) + ->setUser($request->getUser()) + ->setTitle(pht('Login to Phabricator')) + ->addHiddenInput('email', $email) + ->appendParagraph( + pht( + 'Use the button below to log in as: %s', + phutil_tag('strong', array(), $email))) + ->appendParagraph( + pht( + 'After logging in you should set a password for your account, or '. + 'link your account to an external account that you can use to '. + 'authenticate in the future.')) + ->addSubmitButton(pht('Login (%s)', $email)) + ->addCancelButton('/'); + + return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/auth/controller/PhabricatorEmailVerificationController.php b/src/applications/auth/controller/PhabricatorEmailVerificationController.php index 1836e10153..0dbb918851 100644 --- a/src/applications/auth/controller/PhabricatorEmailVerificationController.php +++ b/src/applications/auth/controller/PhabricatorEmailVerificationController.php @@ -15,10 +15,22 @@ final class PhabricatorEmailVerificationController return false; } + public function shouldRequireEnabledUser() { + // Unapproved users are allowed to verify their email addresses. We'll kick + // disabled users out later. + return false; + } + public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); + if ($user->getIsDisabled()) { + // We allowed unapproved and disabled users to hit this controller, but + // want to kick out disabled users now. + return new Aphront400Response(); + } + $email = id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND verificationCode = %s', $user->getPHID(), @@ -32,15 +44,27 @@ final class PhabricatorEmailVerificationController 'user. Make sure you followed the link in the email correctly and are '. 'logged in with the user account associated with the email address.'); $continue = pht('Rats!'); - } else if ($email->getIsVerified()) { + } else if ($email->getIsVerified() && $user->getIsEmailVerified()) { $title = pht('Address Already Verified'); $content = pht( 'This email address has already been verified.'); $continue = pht('Continue to Phabricator'); } else { $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); - $email->setIsVerified(1); - $email->save(); + $email->openTransaction(); + + $email->setIsVerified(1); + $email->save(); + + // If the user just verified their primary email address, mark their + // account as email verified. + $user_primary = $user->loadPrimaryEmail(); + if ($user_primary->getID() == $email->getID()) { + $user->setIsEmailVerified(1); + $user->save(); + } + + $email->saveTransaction(); unset($guard); $title = pht('Address Verified'); @@ -58,9 +82,7 @@ final class PhabricatorEmailVerificationController ->appendChild($content); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Verify Email'))); + $crumbs->addTextCrumb(pht('Verify Email')); return $this->buildApplicationPage( array( diff --git a/src/applications/auth/controller/PhabricatorLogoutController.php b/src/applications/auth/controller/PhabricatorLogoutController.php index f75b4c601d..91e6ebff26 100644 --- a/src/applications/auth/controller/PhabricatorLogoutController.php +++ b/src/applications/auth/controller/PhabricatorLogoutController.php @@ -23,20 +23,26 @@ final class PhabricatorLogoutController if ($request->isFormPost()) { - $log = PhabricatorUserLog::newLog( - $user, + $log = PhabricatorUserLog::initializeNewLog( $user, + $user->getPHID(), PhabricatorUserLog::ACTION_LOGOUT); $log->save(); // Destroy the user's session in the database so logout works even if // their cookies have some issues. We'll detect cookie issues when they // try to login again and tell them to clear any junk. - $phsid = $request->getCookie('phsid'); - if ($phsid) { - $user->destroySession($phsid); + $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); + if (strlen($phsid)) { + $session = id(new PhabricatorAuthSessionQuery()) + ->setViewer($user) + ->withSessionKeys(array($phsid)) + ->executeOne(); + if ($session) { + $session->delete(); + } } - $request->clearCookie('phsid'); + $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); return id(new AphrontRedirectResponse()) ->setURI('/login/'); @@ -46,8 +52,7 @@ final class PhabricatorLogoutController $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Log out of Phabricator?')) - ->appendChild(phutil_tag('p', array(), pht( - 'Are you sure you want to log out?'))) + ->appendChild(pht('Are you sure you want to log out?')) ->addSubmitButton(pht('Logout')) ->addCancelButton('/'); diff --git a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php index 67a253b406..efb4a3987e 100644 --- a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php +++ b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php @@ -19,7 +19,7 @@ final class PhabricatorMustVerifyEmailController $email = $user->loadPrimaryEmail(); - if ($email->getIsVerified()) { + if ($user->getIsEmailVerified()) { return id(new AphrontRedirectResponse())->setURI('/'); } @@ -31,42 +31,33 @@ final class PhabricatorMustVerifyEmailController $sent = new AphrontErrorView(); $sent->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $sent->setTitle(pht('Email Sent')); - $sent->appendChild(phutil_tag( - 'p', - array(), + $sent->appendChild( pht( 'Another verification email was sent to %s.', - phutil_tag('strong', array(), $email_address)))); + phutil_tag('strong', array(), $email_address))); } - $error_view = new AphrontRequestFailureView(); - $error_view->setHeader(pht('Check Your Email')); - $error_view->appendChild(phutil_tag('p', array(), pht( - 'You must verify your email address to login. You should have a new '. - 'email message from Phabricator with verification instructions in your '. - 'inbox (%s).', phutil_tag('strong', array(), $email_address)))); - $error_view->appendChild(phutil_tag('p', array(), pht( - 'If you did not receive an email, you can click the button below '. - 'to try sending another one.'))); - $error_view->appendChild(hsprintf( - '
%s
', - phabricator_form( - $user, - array( - 'action' => '/login/mustverify/', - 'method' => 'POST', - ), - phutil_tag( - 'button', - array( - ), - pht('Send Another Email'))))); + $must_verify = pht( + 'You must verify your email address to login. You should have a '. + 'new email message from Phabricator with verification instructions '. + 'in your inbox (%s).', + phutil_tag('strong', array(), $email_address)); + $send_again = pht( + 'If you did not receive an email, you can click the button below '. + 'to try sending another one.'); + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle(pht('Check Your Email')) + ->appendParagraph($must_verify) + ->appendParagraph($send_again) + ->addSubmitButton(pht('Send Another Email')); return $this->buildApplicationPage( array( $sent, - $error_view, + $dialog, ), array( 'title' => pht('Must Verify Email'), diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index 9662ca2764..5ed434c1b6 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -153,10 +153,6 @@ final class PhabricatorAuthEditController $issues = array(); } - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } - if ($is_new) { $button = pht('Add Provider'); $crumb = pht('Add Provider'); @@ -169,14 +165,35 @@ final class PhabricatorAuthEditController $cancel_uri = $this->getApplicationURI(); } - $str_registration = hsprintf( - '%s: %s', - pht('Allow Registration'), + $config_name = 'auth.email-domains'; + $config_href = '/config/edit/'.$config_name.'/'; + + $email_domains = PhabricatorEnv::getEnvConfig($config_name); + if ($email_domains) { + $registration_warning = pht( + "Users will only be able to register with a verified email address ". + "at one of the configured [[ %s | %s ]] domains: **%s**", + $config_href, + $config_name, + implode(', ', $email_domains)); + } else { + $registration_warning = pht( + "NOTE: Any user who can browse to this install's login page will be ". + "able to register a Phabricator account. To restrict who can register ". + "an account, configure [[ %s | %s ]].", + $config_href, + $config_name); + } + + $str_registration = array( + phutil_tag('strong', array(), pht('Allow Registration:')), + ' ', pht( 'Allow users to register new Phabricator accounts using this '. 'provider. If you disable registration, users can still use this '. 'provider to log in to existing accounts, but will not be able to '. - 'create new accounts.')); + 'create new accounts.'), + ); $str_link = hsprintf( '%s: %s', @@ -195,8 +212,8 @@ final class PhabricatorAuthEditController 'existing Phabricator accounts. If you disable this, Phabricator '. 'accounts will be permanently bound to provider accounts.')); - $status_tag = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE); + $status_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE); if ($is_new) { $status_tag ->setName(pht('New Provider')) @@ -229,6 +246,7 @@ final class PhabricatorAuthEditController 1, $str_registration, $v_registration)) + ->appendRemarkupInstructions($registration_warning) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( @@ -259,9 +277,7 @@ final class PhabricatorAuthEditController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($crumb)); + $crumbs->addTextCrumb($crumb); $xaction_view = null; if (!$is_new) { @@ -282,7 +298,7 @@ final class PhabricatorAuthEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index cf59f68a0f..421e3197a2 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -86,13 +86,45 @@ final class PhabricatorAuthListController pht('Add Authentication Provider')))); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Auth Providers'))); + $crumbs->addTextCrumb(pht('Auth Providers')); + + $config_name = 'auth.email-domains'; + $config_href = '/config/edit/'.$config_name.'/'; + $config_link = phutil_tag( + 'a', + array( + 'href' => $config_href, + 'target' => '_blank', + ), + $config_name); + + $warning = new AphrontErrorView(); + + $email_domains = PhabricatorEnv::getEnvConfig($config_name); + if ($email_domains) { + $warning->setSeverity(AphrontErrorView::SEVERITY_NOTICE); + $warning->setTitle(pht('Registration is Restricted')); + $warning->appendChild( + pht( + 'Only users with a verified email address at one of the %s domains '. + 'will be able to register a Phabricator account: %s', + $config_link, + phutil_tag('strong', array(), implode(', ', $email_domains)))); + } else { + $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); + $warning->setTitle(pht('Anyone Can Register an Account')); + $warning->appendChild( + pht( + 'Anyone who can browse to this Phabricator install will be able to '. + 'register an account. To restrict who can register an account, '. + 'configure %s.', + $config_link)); + } return $this->buildApplicationPage( array( $crumbs, + $warning, $list, ), array( diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index 7583c8869a..4e3051ca93 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -37,10 +37,6 @@ final class PhabricatorAuthNewController } } - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } - $options = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Provider')) ->setName('provider') @@ -83,13 +79,11 @@ final class PhabricatorAuthNewController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Add Authentication Provider')) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Add Provider'))); + $crumbs->addTextCrumb(pht('Add Provider')); return $this->buildApplicationPage( array( diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php new file mode 100644 index 0000000000..fe40e41536 --- /dev/null +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -0,0 +1,185 @@ +establishConnection('r'); + + // NOTE: We're being clever here because this happens on every page load, + // and by joining we can save a query. + + $info = queryfx_one( + $conn_r, + 'SELECT s.sessionExpires AS _sessionExpires, s.id AS _sessionID, u.* + FROM %T u JOIN %T s ON u.phid = s.userPHID + AND s.type = %s AND s.sessionKey = %s', + $user_table->getTableName(), + $session_table->getTableName(), + $session_type, + PhabricatorHash::digest($session_token)); + + if (!$info) { + return null; + } + + $expires = $info['_sessionExpires']; + $id = $info['_sessionID']; + unset($info['_sessionExpires']); + unset($info['_sessionID']); + + $ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); + + // If more than 20% of the time on this session has been used, refresh the + // TTL back up to the full duration. The idea here is that sessions are + // good forever if used regularly, but get GC'd when they fall out of use. + + if (time() + (0.80 * $ttl) > $expires) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $conn_w = $session_table->establishConnection('w'); + queryfx( + $conn_w, + 'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d', + $session_table->getTableName(), + $ttl, + $id); + unset($unguarded); + } + + return $user_table->loadFromArray($info); + } + + + /** + * Issue a new session key for a given identity. Phabricator supports + * different types of sessions (like "web" and "conduit") and each session + * type may have multiple concurrent sessions (this allows a user to be + * logged in on multiple browsers at the same time, for instance). + * + * Note that this method is transport-agnostic and does not set cookies or + * issue other types of tokens, it ONLY generates a new session key. + * + * You can configure the maximum number of concurrent sessions for various + * session types in the Phabricator configuration. + * + * @param const Session type constant (see + * @{class:PhabricatorAuthSession}). + * @param phid|null Identity to establish a session for, usually a user + * PHID. With `null`, generates an anonymous session. + * @return string Newly generated session key. + */ + public function establishSession($session_type, $identity_phid) { + // Consume entropy to generate a new session key, forestalling the eventual + // heat death of the universe. + $session_key = Filesystem::readRandomCharacters(40); + + if ($identity_phid === null) { + return self::KIND_ANONYMOUS.'/'.$session_key; + } + + $session_table = new PhabricatorAuthSession(); + $conn_w = $session_table->establishConnection('w'); + + // This has a side effect of validating the session type. + $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); + + // Logging-in users don't have CSRF stuff yet, so we have to unguard this + // write. + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + id(new PhabricatorAuthSession()) + ->setUserPHID($identity_phid) + ->setType($session_type) + ->setSessionKey(PhabricatorHash::digest($session_key)) + ->setSessionStart(time()) + ->setSessionExpires(time() + $session_ttl) + ->save(); + + $log = PhabricatorUserLog::initializeNewLog( + null, + $identity_phid, + PhabricatorUserLog::ACTION_LOGIN); + $log->setDetails( + array( + 'session_type' => $session_type, + )); + $log->setSession($session_key); + $log->save(); + unset($unguarded); + + return $session_key; + } + +} diff --git a/src/applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php b/src/applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php new file mode 100644 index 0000000000..f9994c2fd6 --- /dev/null +++ b/src/applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php @@ -0,0 +1,18 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE sessionExpires <= UNIX_TIMESTAMP() LIMIT 100', + $session_table->getTableName()); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php index 99e7fa0178..26fca176ab 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php @@ -22,7 +22,7 @@ final class PhabricatorAuthManagementRecoverWorkflow public function execute(PhutilArgumentParser $args) { $can_recover = id(new PhabricatorPeopleQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withIsAdmin(true) ->execute(); if (!$can_recover) { @@ -48,7 +48,7 @@ final class PhabricatorAuthManagementRecoverWorkflow $username = head($usernames); $user = id(new PhabricatorPeopleQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withUsernames(array($username)) ->executeOne(); @@ -72,7 +72,8 @@ final class PhabricatorAuthManagementRecoverWorkflow $console = PhutilConsole::getConsole(); $console->writeOut( pht( - 'Use this link to recover access to the "%s" account:', + 'Use this link to recover access to the "%s" account from the web '. + 'interface:', $username)); $console->writeOut("\n\n"); $console->writeOut(" %s", $user->getEmailLoginURI()); diff --git a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php index b36f28e0dc..86cb03a21f 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php @@ -33,7 +33,7 @@ final class PhabricatorAuthManagementRefreshWorkflow public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $query = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer); diff --git a/src/applications/auth/management/PhabricatorAuthManagementWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementWorkflow.php index 60643c1e4e..c73deff290 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementWorkflow.php @@ -1,10 +1,6 @@ getApplicationURI('/login/'.$this->getProviderKey().'/'); - return PhabricatorEnv::getURI($uri); + return $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); } public function getSettingsURI() { diff --git a/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php b/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php index 6b68b42493..1c9844a34f 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php @@ -150,25 +150,27 @@ final class PhabricatorAuthProviderLDAP return array($account, $response); } - try { - if (strlen($username) && $has_password) { - $adapter = $this->getAdapter(); - $adapter->setLoginUsername($username); - $adapter->setLoginPassword($password); + if ($request->isFormPost()) { + try { + if (strlen($username) && $has_password) { + $adapter = $this->getAdapter(); + $adapter->setLoginUsername($username); + $adapter->setLoginPassword($password); - // TODO: This calls ldap_bind() eventually, which dumps cleartext - // passwords to the error log. See note in PhutilAuthAdapterLDAP. - // See T3351. + // TODO: This calls ldap_bind() eventually, which dumps cleartext + // passwords to the error log. See note in PhutilAuthAdapterLDAP. + // See T3351. - DarkConsoleErrorLogPluginAPI::enableDiscardMode(); - $account_id = $adapter->getAccountID(); - DarkConsoleErrorLogPluginAPI::disableDiscardMode(); - } else { - throw new Exception("Username and password are required!"); + DarkConsoleErrorLogPluginAPI::enableDiscardMode(); + $account_id = $adapter->getAccountID(); + DarkConsoleErrorLogPluginAPI::disableDiscardMode(); + } else { + throw new Exception("Username and password are required!"); + } + } catch (Exception $ex) { + // TODO: Make this cleaner. + throw $ex; } - } catch (Exception $ex) { - // TODO: Make this cleaner. - throw $ex; } return array($this->loadOrCreateAccount($account_id), $response); @@ -253,19 +255,21 @@ final class PhabricatorAuthProviderLDAP $captions = array( self::KEY_HOSTNAME => - pht('Example: %s', - hsprintf('%s', pht('ldap.example.com'))), + pht('Example: %s%sFor LDAPS, use: %s', + phutil_tag('tt', array(), pht('ldap.example.com')), + phutil_tag('br'), + phutil_tag('tt', array(), pht('ldaps://ldaps.example.com/'))), self::KEY_DISTINGUISHED_NAME => pht('Example: %s', - hsprintf('%s', pht('ou=People, dc=example, dc=com'))), + phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))), self::KEY_SEARCH_ATTRIBUTE => pht('Example: %s', - hsprintf('%s', pht('sn'))), + phutil_tag('tt', array(), pht('sn'))), self::KEY_USERNAME_ATTRIBUTE => pht('Optional, if different from search attribute.'), self::KEY_REALNAME_ATTRIBUTES => pht('Optional. Example: %s', - hsprintf('%s', pht('firstname, lastname'))), + phutil_tag('tt', array(), pht('firstname, lastname'))), self::KEY_REFERRALS => pht('Follow referrals. Disable this for Windows AD 2003.'), self::KEY_START_TLS => @@ -359,7 +363,7 @@ final class PhabricatorAuthProviderLDAP $label); } - if (!strlen($old)) { + if ($old === null || $old === '') { return pht( '%s set the "%s" value to "%s".', $xaction->renderHandleLink($author_phid), diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php index 035c2d9adf..cf74ff8f46 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php @@ -25,7 +25,7 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider { $adapter->setClientSecret( new PhutilOpaqueEnvelope( $config->getProperty(self::PROPERTY_APP_SECRET))); - $adapter->setRedirectURI($this->getLoginURI()); + $adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI())); return $adapter; } @@ -35,7 +35,14 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider { protected function renderLoginForm(AphrontRequest $request, $mode) { $adapter = $this->getAdapter(); - $adapter->setState(PhabricatorHash::digest($request->getCookie('phcid'))); + $adapter->setState( + PhabricatorHash::digest( + $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID))); + + $scope = $request->getStr('scope'); + if ($scope) { + $adapter->setScope($scope); + } $attributes = array( 'method' => 'GET', @@ -76,14 +83,15 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider { } if ($adapter->supportsStateParameter()) { - $phcid = $request->getCookie('phcid'); + $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID); if (!strlen($phcid)) { $response = $controller->buildProviderErrorResponse( $this, pht( - 'Your browser did not submit a "phcid" cookie with OAuth state '. + 'Your browser did not submit a "%s" cookie with OAuth state '. 'information in the request. Check that cookies are enabled. '. - 'If this problem persists, you may need to clear your cookies.')); + 'If this problem persists, you may need to clear your cookies.', + PhabricatorCookies::COOKIE_CLIENTID)); } $state = $request->getStr('state'); diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuth1.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuth1.php index 30c82cd65f..3173c13aeb 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuth1.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuth1.php @@ -30,7 +30,7 @@ abstract class PhabricatorAuthProviderOAuth1 extends PhabricatorAuthProvider { if (strlen($secret)) { $adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret)); } - $adapter->setCallbackURI($this->getLoginURI()); + $adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI())); return $adapter; } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php index 5653b4da89..9c835790d9 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php @@ -11,13 +11,17 @@ final class PhabricatorAuthProviderOAuth1JIRA return pht('JIRA'); } + public function getDescriptionForCreate() { + return pht('Configure JIRA OAuth. NOTE: Only supports JIRA 6.'); + } + public function getConfigurationHelp() { if ($this->isSetup()) { return pht( "**Step 1 of 2**: Provide the name and URI for your JIRA install.\n\n". "In the next step, you will configure JIRA."); } else { - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "**Step 2 of 2**: In this step, you will configure JIRA.\n\n". "**Create a JIRA Application**: Log into JIRA and go to ". @@ -162,6 +166,11 @@ final class PhabricatorAuthProviderOAuth1JIRA "again.")); } + $form->appendRemarkupInstructions( + pht( + 'NOTE: This provider **only supports JIRA 6**. It will not work with '. + 'JIRA 5 or earlier.')); + $is_setup = $this->isSetup(); $e_required = $request->isFormPost() ? null : true; diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php index c1cdd0ded9..a6df0295a4 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php @@ -8,7 +8,7 @@ final class PhabricatorAuthProviderOAuth1Twitter } public function getConfigurationHelp() { - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "To configure Twitter OAuth, create a new application here:". diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php index 99f7fccd29..d01f216eef 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php @@ -8,7 +8,7 @@ final class PhabricatorAuthProviderOAuthAmazon } public function getConfigurationHelp() { - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); $uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); $https_note = null; diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php index a929c8f26e..183bcc221a 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php @@ -9,7 +9,7 @@ final class PhabricatorAuthProviderOAuthAsana public function getConfigurationHelp() { $app_uri = PhabricatorEnv::getProductionURI('/'); - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "To configure Asana OAuth, create a new application here:". diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php index 4c374be64c..58c55ccaab 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php @@ -8,7 +8,7 @@ final class PhabricatorAuthProviderOAuthDisqus } public function getConfigurationHelp() { - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "To configure Disqus OAuth, create a new application here:". diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php index 0fe5169683..10f7a5921e 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php @@ -84,9 +84,7 @@ final class PhabricatorAuthProviderOAuthFacebook "improves security by preventing an attacker from capturing ". "an insecure Facebook session and escalating it into a ". "Phabricator session. Enabling it is recommended.", - hsprintf( - '%s', - pht('Require Secure Browsing:'))))); + phutil_tag('strong', array(), pht('Require Secure Browsing:'))))); } public function renderConfigPropertyTransactionTitle( diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php index fe57b7d96a..9c0eb7c907 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php @@ -9,7 +9,7 @@ final class PhabricatorAuthProviderOAuthGitHub public function getConfigurationHelp() { $uri = PhabricatorEnv::getProductionURI('/'); - $callback_uri = $this->getLoginURI(); + $callback_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "To configure GitHub OAuth, create a new GitHub Application here:". @@ -38,7 +38,7 @@ final class PhabricatorAuthProviderOAuthGitHub public function getLoginURI() { // TODO: Clean this up. See PhabricatorAuthOldOAuthRedirectController. - return PhabricatorEnv::getURI('/oauth/github/login/'); + return '/oauth/github/login/'; } } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthGoogle.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthGoogle.php index bd10f13afd..6815032e6f 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthGoogle.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthGoogle.php @@ -8,7 +8,7 @@ final class PhabricatorAuthProviderOAuthGoogle } public function getConfigurationHelp() { - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "To configure Google OAuth, create a new 'API Project' here:". @@ -38,7 +38,7 @@ final class PhabricatorAuthProviderOAuthGoogle public function getLoginURI() { // TODO: Clean this up. See PhabricatorAuthOldOAuthRedirectController. - return PhabricatorEnv::getURI('/oauth/google/login/'); + return '/oauth/google/login/'; } } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthTwitch.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthTwitch.php index db4b12730f..8ab03e06a4 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthTwitch.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthTwitch.php @@ -8,7 +8,7 @@ final class PhabricatorAuthProviderOAuthTwitch } public function getConfigurationHelp() { - $login_uri = $this->getLoginURI(); + $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "To configure Twitch.tv OAuth, create a new application here:". diff --git a/src/applications/auth/provider/PhabricatorAuthProviderPassword.php b/src/applications/auth/provider/PhabricatorAuthProviderPassword.php index e941577010..e84cfdfd02 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderPassword.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderPassword.php @@ -92,7 +92,7 @@ final class PhabricatorAuthProviderPassword $v_user = nonempty( $request->getStr('username'), - $request->getCookie('phusr')); + $request->getCookie(PhabricatorCookies::COOKIE_USERNAME)); $e_user = null; $e_pass = null; @@ -190,36 +190,40 @@ final class PhabricatorAuthProviderPassword $account = null; $log_user = null; - if (!$require_captcha || $captcha_valid) { - $username_or_email = $request->getStr('username'); - if (strlen($username_or_email)) { - $user = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $username_or_email); + if ($request->isFormPost()) { + if (!$require_captcha || $captcha_valid) { + $username_or_email = $request->getStr('username'); + if (strlen($username_or_email)) { + $user = id(new PhabricatorUser())->loadOneWhere( + 'username = %s', + $username_or_email); - if (!$user) { - $user = PhabricatorUser::loadOneWithEmailAddress($username_or_email); - } + if (!$user) { + $user = PhabricatorUser::loadOneWithEmailAddress( + $username_or_email); + } - if ($user) { - $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); - if ($user->comparePassword($envelope)) { - $account = $this->loadOrCreateAccount($user->getPHID()); - $log_user = $user; + if ($user) { + $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); + if ($user->comparePassword($envelope)) { + $account = $this->loadOrCreateAccount($user->getPHID()); + $log_user = $user; + } } } } } if (!$account) { - $log = PhabricatorUserLog::newLog( - null, - $log_user, - PhabricatorUserLog::ACTION_LOGIN_FAILURE); - $log->save(); + if ($request->isFormPost()) { + $log = PhabricatorUserLog::initializeNewLog( + null, + $log_user ? $log_user->getPHID() : null, + PhabricatorUserLog::ACTION_LOGIN_FAILURE); + $log->save(); + } - $request->clearCookie('phusr'); - $request->clearCookie('phsid'); + $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); $response = $controller->buildProviderPageResponse( $this, diff --git a/src/applications/auth/provider/PhabricatorAuthProviderPersona.php b/src/applications/auth/provider/PhabricatorAuthProviderPersona.php index e96c8d96d1..dca32ed3dd 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderPersona.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderPersona.php @@ -29,7 +29,7 @@ final class PhabricatorAuthProviderPersona Javelin::initBehavior( 'persona-login', array( - 'loginURI' => $this->getLoginURI(), + 'loginURI' => PhabricatorEnv::getURI($this->getLoginURI()), )); return $this->renderStandardLoginButton( diff --git a/src/applications/auth/query/PhabricatorAuthSessionQuery.php b/src/applications/auth/query/PhabricatorAuthSessionQuery.php new file mode 100644 index 0000000000..d1e4dca895 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthSessionQuery.php @@ -0,0 +1,99 @@ +identityPHIDs = $identity_phids; + return $this; + } + + public function withSessionKeys(array $keys) { + $this->sessionKeys = $keys; + return $this; + } + + public function withSessionTypes(array $types) { + $this->sessionTypes = $types; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorAuthSession(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function willFilterPage(array $sessions) { + $identity_phids = mpull($sessions, 'getUserPHID'); + + $identity_objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($identity_phids) + ->execute(); + $identity_objects = mpull($identity_objects, null, 'getPHID'); + + foreach ($sessions as $key => $session) { + $identity_object = idx($identity_objects, $session->getUserPHID()); + if (!$identity_object) { + unset($sessions[$key]); + } else { + $session->attachIdentityObject($identity_object); + } + } + + return $sessions; + } + + protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->identityPHIDs) { + $where[] = qsprintf( + $conn_r, + 'userPHID IN (%Ls)', + $this->identityPHIDs); + } + + if ($this->sessionKeys) { + $hashes = array(); + foreach ($this->sessionKeys as $session_key) { + $hashes[] = PhabricatorHash::digest($session_key); + } + $where[] = qsprintf( + $conn_r, + 'sessionKey IN (%Ls)', + $hashes); + } + + if ($this->sessionTypes) { + $where[] = qsprintf( + $conn_r, + 'type IN (%Ls)', + $this->sessionTypes); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationAuth'; + } + +} diff --git a/src/applications/auth/query/PhabricatorExternalAccountQuery.php b/src/applications/auth/query/PhabricatorExternalAccountQuery.php index 37ec836065..0198d5bd38 100644 --- a/src/applications/auth/query/PhabricatorExternalAccountQuery.php +++ b/src/applications/auth/query/PhabricatorExternalAccountQuery.php @@ -167,4 +167,35 @@ final class PhabricatorExternalAccountQuery return 'PhabricatorApplicationPeople'; } + /** + * Attempts to find an external account and if none exists creates a new + * external account with a shiny new ID and PHID. + * + * NOTE: This function assumes the first item in various query parameters is + * the correct value to use in creating a new external account. + */ + public function loadOneOrCreate() { + $account = $this->executeOne(); + if (!$account) { + $account = new PhabricatorExternalAccount(); + if ($this->accountIDs) { + $account->setAccountID(reset($this->accountIDs)); + } + if ($this->accountTypes) { + $account->setAccountType(reset($this->accountTypes)); + } + if ($this->accountDomains) { + $account->setAccountDomain(reset($this->accountDomains)); + } + if ($this->accountSecrets) { + $account->setAccountSecret(reset($this->accountSecrets)); + } + if ($this->userPHIDs) { + $account->setUserPHID(reset($this->userPHIDs)); + } + $account->save(); + } + return $account; + } + } diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php new file mode 100644 index 0000000000..f9d2e41f6e --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthSession.php @@ -0,0 +1,85 @@ + false, + ) + parent::getConfiguration(); + } + + public function getApplicationName() { + // This table predates the "Auth" application, and really all applications. + return 'user'; + } + + public function getTableName() { + // This is a very old table with a nonstandard name. + return PhabricatorUser::SESSION_TABLE; + } + + public function attachIdentityObject($identity_object) { + $this->identityObject = $identity_object; + return $this; + } + + public function getIdentityObject() { + return $this->assertAttached($this->identityObject); + } + + public static function getSessionTypeTTL($session_type) { + switch ($session_type) { + case self::TYPE_WEB: + return (60 * 60 * 24 * 30); // 30 days + case self::TYPE_CONDUIT: + return (60 * 60 * 24); // 24 hours + default: + throw new Exception(pht('Unknown session type "%s".', $session_type)); + } + } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::POLICY_NOONE; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if (!$viewer->getPHID()) { + return false; + } + + $object = $this->getIdentityObject(); + if ($object instanceof PhabricatorUser) { + return ($object->getPHID() == $viewer->getPHID()); + } else if ($object instanceof PhabricatorExternalAccount) { + return ($object->getUserPHID() == $viewer->getPHID()); + } + + return false; + } + + public function describeAutomaticCapability($capability) { + return pht('A session is visible only to its owner.'); + } + +} diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 49c7241e8a..c3f1ff2438 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -248,7 +248,7 @@ abstract class PhabricatorApplication * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. - * @return list List of menu items. + * @return list List of menu items. * @task ui */ public function buildMainMenuItems( @@ -259,14 +259,30 @@ abstract class PhabricatorApplication /** - * On the Phabricator homepage sidebar, this function returns the URL for - * a quick create X link which is displayed in the wide button only. + * Build extra items for the main menu. Generally, this is used to render + * static dropdowns. * - * @return string + * @param PhabricatorUser The viewing user. + * @param AphrontController The current controller. May be null for special + * pages like 404, exception handlers, etc. + * @return view List of menu items. * @task ui */ - public function getQuickCreateURI() { - return null; + public function buildMainMenuExtraNodes( + PhabricatorUser $viewer, + PhabricatorController $controller = null) { + return array(); + } + + + /** + * Build items for the "quick create" menu. + * + * @param PhabricatorUser The viewing user. + * @return list List of menu items. + */ + public function getQuickCreateItems(PhabricatorUser $viewer) { + return array(); } diff --git a/src/applications/base/controller/Phabricator404Controller.php b/src/applications/base/controller/Phabricator404Controller.php index e47c51fd4b..1aa785a040 100644 --- a/src/applications/base/controller/Phabricator404Controller.php +++ b/src/applications/base/controller/Phabricator404Controller.php @@ -2,35 +2,6 @@ final class Phabricator404Controller extends PhabricatorController { - public function shouldRequireLogin() { - - // NOTE: See T2102 for discussion. When a logged-out user requests a page, - // we give them a login form and set a `next_uri` cookie so we send them - // back to the page they requested after they login. However, some browsers - // or extensions request resources which may not exist (like - // "apple-touch-icon.png" and "humans.txt") and these requests may overwrite - // the stored "next_uri" after the login page loads. Our options for dealing - // with this are all bad: - // - // 1. We can't put the URI in the form because some login methods (OAuth2) - // issue redirects to third-party sites. After T1536 we might be able - // to. - // 2. We could set the cookie only if it doesn't exist, but then a user who - // declines to login will end up in the wrong place if they later do - // login. - // 3. We can blacklist all the resources browsers request, but this is a - // mess. - // 4. We can just allow users to access the 404 page without login, so - // requesting bad URIs doesn't set the cookie. - // - // This implements (4). The main downside is that an attacker can now detect - // if a URI is routable (i.e., some application is installed) by testing for - // 404 vs login. If possible, we should implement T1536 in such a way that - // we can pass the next URI through the login process. - - return false; - } - public function processRequest() { return new Aphront404Response(); } diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 7262edb024..f100cffcfe 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -34,22 +34,33 @@ abstract class PhabricatorController extends AphrontController { $user = $request->getUser(); } else { $user = new PhabricatorUser(); + $session_engine = new PhabricatorAuthSessionEngine(); - $phusr = $request->getCookie('phusr'); - $phsid = $request->getCookie('phsid'); - - if (strlen($phusr) && $phsid) { - $info = queryfx_one( - $user->establishConnection('r'), - 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID - AND s.type LIKE %> AND s.sessionKey = %s', - $user->getTableName(), - 'phabricator_session', - 'web-', - PhabricatorHash::digest($phsid)); - if ($info) { - $user->loadFromArray($info); + $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); + if (strlen($phsid)) { + $session_user = $session_engine->loadUserForSession( + PhabricatorAuthSession::TYPE_WEB, + $phsid); + if ($session_user) { + $user = $session_user; } + } else { + // If the client doesn't have a session token, generate an anonymous + // session. This is used to provide CSRF protection to logged-out users. + $phsid = $session_engine->establishSession( + PhabricatorAuthSession::TYPE_WEB, + null); + + // This may be a resource request, in which case we just don't set + // the cookie. + if ($request->canSetCookies()) { + $request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid); + } + } + + + if (!$user->isLoggedIn()) { + $user->attachAlternateCSRFString(PhabricatorHash::digest($phsid)); } $request->setUser($user); @@ -74,10 +85,15 @@ abstract class PhabricatorController extends AphrontController { } } - if ($user->getIsDisabled() && $this->shouldRequireEnabledUser()) { - $disabled_user_controller = new PhabricatorDisabledUserController( - $request); - return $this->delegateToController($disabled_user_controller); + if ($this->shouldRequireEnabledUser()) { + if ($user->isLoggedIn() && !$user->getIsApproved()) { + $controller = new PhabricatorAuthNeedsApprovalController($request); + return $this->delegateToController($controller); + } + if ($user->getIsDisabled()) { + $controller = new PhabricatorDisabledUserController($request); + return $this->delegateToController($controller); + } } $event = new PhabricatorEvent( @@ -114,12 +130,7 @@ abstract class PhabricatorController extends AphrontController { if ($user->isLoggedIn()) { if ($this->shouldRequireEmailVerification()) { - $email = $user->loadPrimaryEmail(); - if (!$email) { - throw new Exception( - "No primary email address associated with this account!"); - } - if (!$email->getIsVerified()) { + if (!$user->getIsEmailVerified()) { $controller = new PhabricatorMustVerifyEmailController($request); $this->setCurrentApplication($auth_application); return $this->delegateToController($controller); @@ -246,8 +257,9 @@ abstract class PhabricatorController extends AphrontController { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->setController($this); - $view->appendChild(hsprintf( - '
%s
', + $view->appendChild(phutil_tag( + 'div', + array('style' => 'padding: 2em 0;'), $response->buildResponseString())); $page_response = new AphrontWebpageResponse(); $page_response->setContent($view->render()); @@ -408,4 +420,9 @@ abstract class PhabricatorController extends AphrontController { return array($can_act, $message); } + public function getDefaultResourceSource() { + return 'phabricator'; + } + + } diff --git a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php index 1d6be0e793..1f5c94a498 100644 --- a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php +++ b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php @@ -31,7 +31,7 @@ final class PhabricatorAccessControlTestCase $u_unverified = $this->generateNewTestUser() ->setUsername('unverified') ->save(); - $u_unverified->loadPrimaryEmail()->setIsVerified(0)->save(); + $u_unverified->setIsEmailVerified(0)->save(); $u_normal = $this->generateNewTestUser() ->setUsername('normal') @@ -47,6 +47,11 @@ final class PhabricatorAccessControlTestCase ->setUsername('admin') ->save(); + $u_notapproved = $this->generateNewTestUser() + ->setIsApproved(0) + ->setUsername('notapproved') + ->save(); + $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('phabricator.base-uri', 'http://'.$host); $env->overrideEnvConfig('policy.allow-public', false); @@ -68,6 +73,7 @@ final class PhabricatorAccessControlTestCase array( $u_public, $u_disabled, + $u_notapproved, )); @@ -86,6 +92,7 @@ final class PhabricatorAccessControlTestCase $u_unverified, $u_public, $u_disabled, + $u_notapproved, )); $this->checkAccess( @@ -100,6 +107,7 @@ final class PhabricatorAccessControlTestCase array( $u_public, $u_disabled, + $u_notapproved, )); $env->overrideEnvConfig('auth.require-email-verification', false); @@ -118,6 +126,7 @@ final class PhabricatorAccessControlTestCase $u_unverified, $u_public, $u_disabled, + $u_notapproved, )); @@ -132,6 +141,7 @@ final class PhabricatorAccessControlTestCase $u_unverified, $u_admin, $u_disabled, + $u_notapproved, ), array( $u_public, @@ -152,6 +162,7 @@ final class PhabricatorAccessControlTestCase ), array( $u_disabled, + $u_notapproved, )); @@ -184,6 +195,7 @@ final class PhabricatorAccessControlTestCase ), array( $u_disabled, + $u_notapproved, )); $env->overrideEnvConfig('policy.allow-public', false); @@ -208,6 +220,7 @@ final class PhabricatorAccessControlTestCase $u_admin, $u_public, $u_disabled, + $u_notapproved, )); $this->checkAccess( @@ -222,6 +235,7 @@ final class PhabricatorAccessControlTestCase ), array( $u_disabled, + $u_notapproved, )); } diff --git a/src/applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php b/src/applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php new file mode 100644 index 0000000000..29ad8ffb18 --- /dev/null +++ b/src/applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php @@ -0,0 +1,26 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE cacheCreated < %d + ORDER BY cacheCreated ASC LIMIT 100', + $cache->getTableName(), + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php b/src/applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php new file mode 100644 index 0000000000..75b25e2530 --- /dev/null +++ b/src/applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php @@ -0,0 +1,25 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', + $table->getTableName(), + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php b/src/applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php new file mode 100644 index 0000000000..c459553396 --- /dev/null +++ b/src/applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php @@ -0,0 +1,20 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE cacheExpires < %d + ORDER BY cacheExpires ASC LIMIT 100', + $cache->getTableName(), + time()); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php b/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php index cfa29c0b5e..709c66ba6b 100644 --- a/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php +++ b/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php @@ -1,7 +1,7 @@ getBaseURI().'status/create/'; - } - public function getRoutes() { return array( '/calendar/' => array( @@ -55,4 +51,16 @@ final class PhabricatorApplicationCalendar extends PhabricatorApplication { ); } + public function getQuickCreateItems(PhabricatorUser $viewer) { + $items = array(); + + $item = id(new PHUIListItemView()) + ->setName(pht('Calendar Event')) + ->setAppIcon('calendar-dark') + ->setHref($this->getBaseURI().'status/create/'); + $items[] = $item; + + return $items; + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php index 5ab0928687..88e2dfeb05 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php @@ -61,7 +61,7 @@ final class PhabricatorCalendarBrowseController $nav->appendChild( array( $this->getNoticeView(), - hsprintf('
%s
', $month_view), + phutil_tag('div', array('style' => 'padding: 20px;'), $month_view), )); return $this->buildApplicationPage( diff --git a/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php index 1d4ced5015..47be7d376e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php @@ -165,7 +165,7 @@ final class PhabricatorCalendarEditStatusController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $nav = $this->buildSideNavView($status); diff --git a/src/applications/calendar/view/AphrontCalendarMonthView.php b/src/applications/calendar/view/AphrontCalendarMonthView.php index 3597857a8d..0859e0acf9 100644 --- a/src/applications/calendar/view/AphrontCalendarMonthView.php +++ b/src/applications/calendar/view/AphrontCalendarMonthView.php @@ -87,10 +87,9 @@ final class AphrontCalendarMonthView extends AphrontView { } else { $show_events = array_fill_keys( array_keys($show_events), - hsprintf( - '
'. - ' '. - '
')); + phutil_tag_div( + 'aphront-calendar-event aphront-calendar-event-empty', + "\xC2\xA0")); //   } foreach ($events as $event) { @@ -119,45 +118,49 @@ final class AphrontCalendarMonthView extends AphrontView { $name); } - $markup[] = hsprintf( - '
'. - '
%s
'. - '%s%s'. - '
', + $markup[] = phutil_tag_div( $class, - $day_number, - $holiday_markup, - phutil_implode_html("\n", $show_events)); + array( + phutil_tag_div('aphront-calendar-date-number', $day_number), + $holiday_markup, + phutil_implode_html("\n", $show_events), + )); } $table = array(); $rows = array_chunk($markup, 7); foreach ($rows as $row) { - $table[] = hsprintf(''); + $cells = array(); while (count($row) < 7) { $row[] = $empty_box; } foreach ($row as $cell) { - $table[] = phutil_tag('td', array(), $cell); + $cells[] = phutil_tag('td', array(), $cell); } - $table[] = hsprintf(''); + $table[] = phutil_tag('tr', array(), $cells); } - $table = hsprintf( - ''. - '%s'. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '%s'. - '
SunMonTueWedThuFriSat
', - $this->renderCalendarHeader($first), - phutil_implode_html("\n", $table)); + + $header = phutil_tag( + 'tr', + array('class' => 'aphront-calendar-day-of-week-header'), + array( + phutil_tag('th', array(), pht('Sun')), + phutil_tag('th', array(), pht('Mon')), + phutil_tag('th', array(), pht('Tue')), + phutil_tag('th', array(), pht('Wed')), + phutil_tag('th', array(), pht('Thu')), + phutil_tag('th', array(), pht('Fri')), + phutil_tag('th', array(), pht('Sat')), + )); + + $table = phutil_tag( + 'table', + array('class' => 'aphront-calendar-view'), + array( + $this->renderCalendarHeader($first), + $header, + phutil_implode_html("\n", $table), + )); return $table; } @@ -190,11 +193,14 @@ final class AphrontCalendarMonthView extends AphrontView { $right_th = phutil_tag('th', array(), $next_link); } - return hsprintf( - '%s%s%s', - $left_th, - phutil_tag('th', array('colspan' => $colspan), $date->format('F Y')), - $right_th); + return phutil_tag( + 'tr', + array('class' => 'aphront-calendar-month-year-header'), + array( + $left_th, + phutil_tag('th', array('colspan' => $colspan), $date->format('F Y')), + $right_th, + )); } private function getNextYearAndMonth() { diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php index 88ebc89e93..945909bbbb 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php @@ -23,10 +23,7 @@ final class PhabricatorChatLogChannelListController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Channel List')) - ->setHref($this->getApplicationURI())); + ->addTextCrumb(pht('Channel List'), $this->getApplicationURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php index 07d414264a..b457b6f001 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php @@ -183,10 +183,7 @@ final class PhabricatorChatLogChannelLogController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($channel->getChannelName()) - ->setHref($uri)); + ->addTextCrumb($channel->getChannelName(), $uri); $form = id(new AphrontFormView()) ->setUser($user) diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index 837674996e..997a2ac857 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -279,28 +279,13 @@ final class PhabricatorConduitAPIController ); } - $session = queryfx_one( - id(new PhabricatorUser())->establishConnection('r'), - 'SELECT * FROM %T WHERE sessionKey = %s', - PhabricatorUser::SESSION_TABLE, - PhabricatorHash::digest($session_key)); - if (!$session) { - return array( - 'ERR-INVALID-SESSION', - 'Session key is invalid.', - ); - } + $user = id(new PhabricatorAuthSessionEngine()) + ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key); - // TODO: Make sessions timeout. - // TODO: When we pull a session, read connectionID from the session table. - - $user = id(new PhabricatorUser())->loadOneWhere( - 'phid = %s', - $session['userPHID']); if (!$user) { return array( 'ERR-INVALID-SESSION', - 'Session is for nonexistent user.', + 'Session key is invalid.', ); } @@ -313,24 +298,11 @@ final class PhabricatorConduitAPIController ConduitAPIRequest $request, PhabricatorUser $user) { - if ($user->getIsDisabled()) { + if (!$user->isUserActivated()) { return array( 'ERR-USER-DISABLED', - 'User is disabled.'); - } - - if (PhabricatorUserEmail::isEmailVerificationRequired()) { - $email = $user->loadPrimaryEmail(); - if (!$email) { - return array( - 'ERR-USER-NOEMAIL', - 'User has no primary email address.'); - } - if (!$email->getIsVerified()) { - return array( - 'ERR-USER-UNVERIFIED', - 'User has unverified email address.'); - } + pht('User account is not activated.'), + ); } $request->setUser($user); @@ -393,15 +365,9 @@ final class PhabricatorConduitAPIController $method_uri = $this->getApplicationURI('method/'.$method.'/'); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($method) - ->setHref($method_uri)) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Call'))); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($method, $method_uri) + ->addTextCrumb(pht('Call')); return $this->buildApplicationPage( array( @@ -423,7 +389,10 @@ final class PhabricatorConduitAPIController $value = $json->encodeFormatted($value); } - $value = hsprintf('
%s
', $value); + $value = phutil_tag( + 'pre', + array('style' => 'white-space: pre-wrap;'), + $value); return $value; } diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index 5efb0d7377..e6d8d975c3 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -112,13 +112,14 @@ final class PhabricatorConduitConsoleController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($method->getAPIMethodName()) - ->setFormError($status_view) ->setForm($form); + if ($status_view) { + $form_box->setErrorView($status_view); + } + $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($method->getAPIMethodName())); + $crumbs->addTextCrumb($method->getAPIMethodName()); return $this->buildApplicationPage( array( diff --git a/src/applications/conduit/controller/PhabricatorConduitLogController.php b/src/applications/conduit/controller/PhabricatorConduitLogController.php index ee95ff0533..ba384c77f8 100644 --- a/src/applications/conduit/controller/PhabricatorConduitLogController.php +++ b/src/applications/conduit/controller/PhabricatorConduitLogController.php @@ -40,9 +40,7 @@ final class PhabricatorConduitLogController $table = $this->renderCallTable($calls, $conns); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Call Logs'))); + $crumbs->addTextCrumb(pht('Call Logs')); return $this->buildApplicationPage( array( diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenController.php b/src/applications/conduit/controller/PhabricatorConduitTokenController.php index b750d60d4c..1bb0dfa2b9 100644 --- a/src/applications/conduit/controller/PhabricatorConduitTokenController.php +++ b/src/applications/conduit/controller/PhabricatorConduitTokenController.php @@ -38,6 +38,8 @@ final class PhabricatorConduitTokenController 'After you copy and paste this token, `arc` will complete '. 'the certificate install process for you.'); + Javelin::initBehavior('select-on-click'); + $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions($pre_instructions) @@ -45,13 +47,13 @@ final class PhabricatorConduitTokenController id(new AphrontFormTextAreaControl()) ->setLabel(pht('Token')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) + ->setReadonly(true) + ->setSigil('select-on-click') ->setValue($token->getToken())) ->appendRemarkupInstructions($post_instructions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Install Certificate'))); + $crumbs->addTextCrumb(pht('Install Certificate')); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Certificate Token')) diff --git a/src/applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php b/src/applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php new file mode 100644 index 0000000000..14467af2bf --- /dev/null +++ b/src/applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php @@ -0,0 +1,25 @@ +establishConnection('w'); + queryfx( + $conn_w, + 'DELETE FROM %T WHERE dateCreated < %d + ORDER BY dateCreated ASC LIMIT 100', + $table->getTableName(), + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/conduit/garbagecollector/ConduitLogGarbageCollector.php b/src/applications/conduit/garbagecollector/ConduitLogGarbageCollector.php new file mode 100644 index 0000000000..9ed39d5832 --- /dev/null +++ b/src/applications/conduit/garbagecollector/ConduitLogGarbageCollector.php @@ -0,0 +1,25 @@ +establishConnection('w'); + queryfx( + $conn_w, + 'DELETE FROM %T WHERE dateCreated < %d + ORDER BY dateCreated ASC LIMIT 100', + $table->getTableName(), + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index e549f5755b..87604dc6d2 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,9 +1,8 @@ 'optional string', + 'after' => 'optional string', + 'limit' => 'optional int (default = 100)', + ); + } + + + /** + * @task pager + */ + protected function newPager(ConduitAPIRequest $request) { + $limit = $request->getValue('limit', 100); + $limit = min(1000, $limit); + $limit = max(1, $limit); + + $pager = id(new AphrontCursorPagerView()) + ->setPageSize($limit); + + $before_id = $request->getValue('before'); + if ($before_id !== null) { + $pager->setBeforeID($before_id); + } + + $after_id = $request->getValue('after'); + if ($after_id !== null) { + $pager->setAfterID($after_id); + } + + return $pager; + } + + + /** + * @task pager + */ + protected function addPagerResults( + array $results, + AphrontCursorPagerView $pager) { + + $results['cursor'] = array( + 'limit' => $pager->getPageSize(), + 'after' => $pager->getNextPageID(), + 'before' =>$pager->getPrevPageID(), + ); + + return $results; + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/conduit/method/ConduitAPI_conduit_connect_Method.php b/src/applications/conduit/method/ConduitAPI_conduit_connect_Method.php index 00887d6b4d..0995f2748f 100644 --- a/src/applications/conduit/method/ConduitAPI_conduit_connect_Method.php +++ b/src/applications/conduit/method/ConduitAPI_conduit_connect_Method.php @@ -142,7 +142,10 @@ final class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod { if ($valid != $signature) { throw new ConduitException('ERR-INVALID-CERTIFICATE'); } - $session_key = $user->establishSession('conduit'); + $session_key = id(new PhabricatorAuthSessionEngine()) + ->establishSession( + PhabricatorAuthSession::TYPE_CONDUIT, + $user->getPHID()); } else { throw new ConduitException('ERR-NO-CERTIFICATE'); } diff --git a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php index 33b61ba007..43bf22bd3a 100644 --- a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php +++ b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php @@ -4,7 +4,7 @@ final class PhabricatorConduitSearchEngine extends PhabricatorApplicationSearchEngine { public function getPageSize(PhabricatorSavedQuery $saved) { - return INF; + return PHP_INT_MAX - 1; } public function buildSavedQueryFromRequest(AphrontRequest $request) { @@ -61,8 +61,9 @@ final class PhabricatorConduitSearchEngine ->setLabel('Applications') ->setName('applicationNames') ->setValue(implode(', ', $names)) - ->setCaption( - pht('Example: %s', hsprintf('differential, paste')))); + ->setCaption(pht( + 'Example: %s', + phutil_tag('tt', array(), 'differential, paste')))); $is_stable = $saved->getParameter('isStable'); $is_unstable = $saved->getParameter('isUnstable'); diff --git a/src/applications/config/check/PhabricatorSetupCheckDaemons.php b/src/applications/config/check/PhabricatorSetupCheckDaemons.php new file mode 100644 index 0000000000..df3fe6d89d --- /dev/null +++ b/src/applications/config/check/PhabricatorSetupCheckDaemons.php @@ -0,0 +1,46 @@ +setViewer(PhabricatorUser::getOmnipotentUser()) + ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) + ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) + ->setLimit(1) + ->execute(); + + if (!$task_daemon) { + $doc_href = PhabricatorEnv::getDocLink( + 'article/Managing_Daemons_with_phd.html'); + + $summary = pht( + 'You must start the Phabricator daemons to send email, rebuild '. + 'search indexes, and do other background processing.'); + + $message = pht( + 'The Phabricator daemons are not running, so Phabricator will not '. + 'be able to perform background processing (including sending email, '. + 'rebuilding search indexes, importing commits, cleaning up old data, '. + 'running builds, etc.).'. + "\n\n". + 'Use %s to start daemons. See %s for more information.', + phutil_tag('tt', array(), 'bin/phd start'), + phutil_tag( + 'a', + array( + 'href' => $doc_href, + ), + pht('Managing Daemons with phd'))); + + $this->newIssue('daemons.not-running') + ->setShortName(pht('Daemons Not Running')) + ->setName(pht('Phabricator Daemons Are Not Running')) + ->setSummary($summary) + ->setMessage($message) + ->addCommand('phabricator/ $ ./bin/phd start'); + } + + } +} diff --git a/src/applications/config/check/PhabricatorSetupCheckExtraConfig.php b/src/applications/config/check/PhabricatorSetupCheckExtraConfig.php index ed76ab1997..fd12e035cd 100644 --- a/src/applications/config/check/PhabricatorSetupCheckExtraConfig.php +++ b/src/applications/config/check/PhabricatorSetupCheckExtraConfig.php @@ -146,6 +146,10 @@ final class PhabricatorSetupCheckExtraConfig extends PhabricatorSetupCheck { 'PhabricatorRemarkupCustomInlineRule or '. 'PhabricatorRemarkupCustomBlockRule.'); + $session_reason = pht( + 'Sessions now expire and are garbage collected rather than having an '. + 'arbitrary concurrency limit.'); + $ancient_config += array( 'phid.external-loaders' => pht( @@ -164,6 +168,16 @@ final class PhabricatorSetupCheckExtraConfig extends PhabricatorSetupCheck { 'differential.custom-remarkup-block-rules' => $markup_reason, 'auth.sshkeys.enabled' => pht( 'SSH keys are now actually useful, so they are always enabled.'), + 'differential.anonymous-access' => pht( + 'Phabricator now has meaningful global access controls. See '. + '`policy.allow-public`.'), + 'celerity.resource-path' => pht( + 'An alternate resource map is no longer supported. Instead, use '. + 'multiple maps. See T4222.'), + 'metamta.send-immediately' => pht( + 'Mail is now always delivered by the daemons.'), + 'auth.sessions.conduit' => $session_reason, + 'auth.sessions.web' => $session_reason, ); return $ancient_config; diff --git a/src/applications/config/controller/PhabricatorConfigAllController.php b/src/applications/config/controller/PhabricatorConfigAllController.php index c6a8dba05a..d1222775ef 100644 --- a/src/applications/config/controller/PhabricatorConfigAllController.php +++ b/src/applications/config/controller/PhabricatorConfigAllController.php @@ -56,9 +56,7 @@ final class PhabricatorConfigAllController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + ->addTextCrumb($title); $panel = new AphrontPanelView(); $panel->appendChild($table); diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index e2da019e96..af9ceda313 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -195,26 +195,20 @@ final class PhabricatorConfigEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Config')) - ->setHref($this->getApplicationURI())); - - if ($group) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($group->getName()) - ->setHref($group_uri)); + if ($error_view) { + $form_box->setErrorView($error_view); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($this->key) - ->setHref('/config/edit/'.$this->key)); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Config'), $this->getApplicationURI()); + + if ($group) { + $crumbs->addTextCrumb($group->getName(), $group_uri); + } + + $crumbs->addTextCrumb($this->key, '/config/edit/'.$this->key); $xactions = id(new PhabricatorConfigTransactionQuery()) ->withObjectPHIDs(array($config_entry->getPHID())) @@ -473,10 +467,10 @@ final class PhabricatorConfigEditController } $table = array(); - $table[] = hsprintf( - '%s%s', - pht('Example'), - pht('Value')); + $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( + phutil_tag('th', array(), pht('Example')), + phutil_tag('th', array(), pht('Value')), + )); foreach ($examples as $example) { list($value, $description) = $example; @@ -488,10 +482,10 @@ final class PhabricatorConfigEditController } } - $table[] = hsprintf( - '%s%s', - $description, - $value); + $table[] = phutil_tag('tr', array(), array( + phutil_tag('th', array(), $description), + phutil_tag('td', array(), $value), + )); } require_celerity_resource('config-options-css'); @@ -509,10 +503,10 @@ final class PhabricatorConfigEditController $stack = $stack->getStack(); $table = array(); - $table[] = hsprintf( - '%s%s', - pht('Source'), - pht('Value')); + $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( + phutil_tag('th', array(), pht('Source')), + phutil_tag('th', array(), pht('Value')), + )); foreach ($stack as $key => $source) { $value = $source->getKeys( array( @@ -526,10 +520,10 @@ final class PhabricatorConfigEditController $value[$option->getKey()]); } - $table[] = hsprintf( - '%s%s', - $source->getName(), - $value); + $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( + phutil_tag('th', array(), $source->getName()), + phutil_tag('td', array(), $value), + )); } require_celerity_resource('config-options-css'); diff --git a/src/applications/config/controller/PhabricatorConfigGroupController.php b/src/applications/config/controller/PhabricatorConfigGroupController.php index bdd4c73cd9..0f18fb7813 100644 --- a/src/applications/config/controller/PhabricatorConfigGroupController.php +++ b/src/applications/config/controller/PhabricatorConfigGroupController.php @@ -28,14 +28,8 @@ final class PhabricatorConfigGroupController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Config')) - ->setHref($this->getApplicationURI())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($options->getName()) - ->setHref($this->getApplicationURI())); + ->addTextCrumb(pht('Config'), $this->getApplicationURI()) + ->addTextCrumb($options->getName(), $this->getApplicationURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php index f9b1333ca3..7e002c92ea 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueListController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php @@ -30,10 +30,7 @@ final class PhabricatorConfigIssueListController $crumbs = $this ->buildApplicationCrumbs($nav) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Setup')) - ->setHref($this->getApplicationURI('issue/'))); + ->addTextCrumb(pht('Setup'), $this->getApplicationURI('issue/')); $nav->setCrumbs($crumbs); diff --git a/src/applications/config/controller/PhabricatorConfigIssueViewController.php b/src/applications/config/controller/PhabricatorConfigIssueViewController.php index bfc294d7ca..d10d154c9a 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueViewController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueViewController.php @@ -38,14 +38,8 @@ final class PhabricatorConfigIssueViewController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Setup Issues')) - ->setHref($this->getApplicationURI('issue/'))) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + ->addTextCrumb(pht('Setup Issues'), $this->getApplicationURI('issue/')) + ->addTextCrumb($title, $request->getRequestURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php index a583fbb491..101e163834 100644 --- a/src/applications/config/controller/PhabricatorConfigListController.php +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -26,10 +26,7 @@ final class PhabricatorConfigListController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Config')) - ->setHref($this->getApplicationURI())); + ->addTextCrumb(pht('Config'), $this->getApplicationURI()); $nav->setCrumbs($crumbs); diff --git a/src/applications/config/management/PhabricatorConfigManagementWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementWorkflow.php index f984b0ea6e..306eab68bf 100644 --- a/src/applications/config/management/PhabricatorConfigManagementWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementWorkflow.php @@ -1,10 +1,6 @@ pht("The HTTP response code."), - 'C' => pht("The controller which handled the request."), + $common_map = array( + 'C' => pht("The controller or workflow which handled the request."), + 'c' => pht("The HTTP response code or process exit code."), 'D' => pht("The request date."), 'e' => pht("Epoch timestamp."), 'h' => pht("The webserver's host name."), 'p' => pht("The PID of the server process."), - 'R' => pht("The HTTP referrer."), 'r' => pht("The remote IP."), 'T' => pht("The request duration, in microseconds."), - 'U' => pht("The request path."), + 'U' => pht("The request path, or request target."), + 'm' => pht("For conduit, the Conduit method which was invoked."), 'u' => pht("The logged-in username, if one is logged in."), 'P' => pht("The logged-in user PHID, if one is logged in."), - 'M' => pht("The HTTP method."), - 'm' => pht("For conduit, the Conduit method which was invoked."), + 'i' => pht("Request input, in bytes."), + 'o' => pht("Request output, in bytes."), ); - $fdesc = pht("Format for the access log. Available variables are:"); - $fdesc .= "\n\n"; - foreach ($map as $key => $desc) { - $fdesc .= " - %".$key." ".$desc."\n"; - } - $fdesc .= "\n"; - $fdesc .= pht( - "If a variable isn't available (for example, %%m appears in the file ". - "format but the request is not a Conduit request), it will be rendered ". - "as '-'"); - $fdesc .= "\n\n"; - $fdesc .= pht( - "Note that the default format is subject to change in the future, so ". - "if you rely on the log's format, specify it explicitly."); + $http_map = $common_map + array( + 'R' => pht("The HTTP referrer."), + 'M' => pht("The HTTP method."), + ); + + $ssh_map = $common_map + array( + 's' => pht("The system user."), + 'S' => pht("The system sudo user."), + ); + + $http_desc = pht( + "Format for the HTTP access log. Use {{log.access.path}} to set the ". + "path. Available variables are:"); + $http_desc .= "\n\n"; + $http_desc .= $this->renderMapHelp($http_map); + + $ssh_desc = pht( + "Format for the SSH access log. Use {{log.ssh.path}} to set the ". + "path. Available variables are:"); + $ssh_desc .= "\n\n"; + $ssh_desc .= $this->renderMapHelp($ssh_map); return array( $this->newOption('log.access.path', 'string', null) + ->setLocked(true) ->setSummary(pht("Access log location.")) ->setDescription( pht( @@ -57,19 +65,61 @@ final class PhabricatorAccessLogConfigOptions "If not set, no log will be written.")) ->addExample( null, - pht('Disable access log')) + pht('Disable access log.')) ->addExample( '/var/log/phabricator/access.log', - pht('Write access log here')), + pht('Write access log here.')), $this->newOption( 'log.access.format', // NOTE: This is 'wild' intead of 'string' so "\t" and such can be // specified. 'wild', "[%D]\t%p\t%h\t%r\t%u\t%C\t%m\t%U\t%R\t%c\t%T") + ->setLocked(true) ->setSummary(pht("Access log format.")) - ->setDescription($fdesc), + ->setDescription($http_desc), + $this->newOption('log.ssh.path', 'string', null) + ->setLocked(true) + ->setSummary(pht("SSH log location.")) + ->setDescription( + pht( + "To enable the Phabricator SSH log, specify a path. The ". + "access log can provide more detailed information about SSH ". + "access than a normal SSH log (for instance, it can show ". + "logged-in users, commands, and other application data).\n\n". + "If not set, no log will be written.")) + ->addExample( + null, + pht('Disable SSH log.')) + ->addExample( + '/var/log/phabricator/ssh.log', + pht('Write SSH log here.')), + $this->newOption( + 'log.ssh.format', + 'wild', + "[%D]\t%p\t%h\t%r\t%s\t%S\t%u\t%C\t%U\t%c\t%T\t%i\t%o") + ->setLocked(true) + ->setSummary(pht("SSH log format.")) + ->setDescription($ssh_desc), ); } + private function renderMapHelp(array $map) { + $desc = ''; + foreach ($map as $key => $kdesc) { + $desc .= " - `%".$key."` ".$kdesc."\n"; + } + $desc .= "\n"; + $desc .= pht( + "If a variable isn't available (for example, %%m appears in the file ". + "format but the request is not a Conduit request), it will be rendered ". + "as '-'"); + $desc .= "\n\n"; + $desc .= pht( + "Note that the default format is subject to change in the future, so ". + "if you rely on the log's format, specify it explicitly."); + + return $desc; + } + } diff --git a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php index dffe91e7be..7e761085a8 100644 --- a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php +++ b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php @@ -13,23 +13,7 @@ final class PhabricatorAuthenticationConfigOptions public function getOptions() { return array( - $this->newOption('auth.sessions.web', 'int', 5) - ->setSummary( - pht("Number of web sessions a user can have simultaneously.")) - ->setDescription( - pht( - "Maximum number of simultaneous web sessions each user is ". - "permitted to have. Setting this to '1' will prevent a user from ". - "logging in on more than one browser at the same time.")), - $this->newOption('auth.sessions.conduit', 'int', 5) - ->setSummary( - pht( - "Number of simultaneous Conduit sessions each user is permitted.")) - ->setDescription( - pht( - "Maximum number of simultaneous Conduit sessions each user is ". - "permitted to have.")), - $this->newOption('auth.require-email-verification', 'bool', false) + $this->newOption('auth.require-email-verification', 'bool', false) ->setBoolOptions( array( pht("Require email verification"), @@ -41,24 +25,47 @@ final class PhabricatorAuthenticationConfigOptions pht( "If true, email addresses must be verified (by clicking a link ". "in an email) before a user can login. By default, verification ". - "is optional unless 'auth.email-domains' is nonempty.")), - $this->newOption('auth.email-domains', 'list', array()) + "is optional unless {{auth.email-domains}} is nonempty.")), + $this->newOption('auth.require-approval', 'bool', true) + ->setBoolOptions( + array( + pht("Require Administrators to Approve Accounts"), + pht("Don't Require Manual Approval"), + )) + ->setSummary( + pht("Require administrators to approve new accounts.")) + ->setDescription( + pht( + "Newly registered Phabricator accounts can either be placed ". + "into a manual approval queue for administrative review, or ". + "automatically activated immediately. The approval queue is ". + "enabled by default because it gives you greater control over ". + "who can register an account and access Phabricator.\n\n". + "If your install is completely public, or on a VPN, or users can ". + "only register with a trusted provider like LDAP, or you've ". + "otherwise configured Phabricator to prevent unauthorized ". + "registration, you can disable the queue to reduce administrative ". + "overhead.\n\n". + "NOTE: Before you disable the queue, make sure ". + "{{auth.email-domains}} is configured correctly for your ". + "install!")), + $this->newOption('auth.email-domains', 'list', array()) ->setSummary(pht("Only allow registration from particular domains.")) ->setDescription( pht( "You can restrict allowed email addresses to certain domains ". - "(like 'yourcompany.com') by setting a list of allowed domains ". - "here. Users will only be allowed to register using email ". + "(like `yourcompany.com`) by setting a list of allowed domains ". + "here.\n\nUsers will only be allowed to register using email ". "addresses at one of the domains, and will only be able to add ". "new email addresses for these domains. If you configure this, ". - "it implies 'auth.require-email-verification'.\n\n". - "You should omit the '@' from domains. Note that the domain must ". - "match exactly. If you allow 'yourcompany.com', that permits ". - "'joe@yourcompany.com' but rejects 'joe@mail.yourcompany.com'.")) + "it implies {{auth.require-email-verification}}.\n\n". + "You should omit the `@` from domains. Note that the domain must ". + "match exactly. If you allow `yourcompany.com`, that permits ". + "`joe@yourcompany.com` but rejects `joe@mail.yourcompany.com`.")) ->addExample( "yourcompany.com\nmail.yourcompany.com", pht('Valid Setting')), - $this->newOption('auth.login-message', 'string', null) + $this->newOption('auth.login-message', 'string', null) ->setLocked(true) ->setSummary(pht("A block of HTML displayed on the login screen.")) ->setDescription( @@ -66,7 +73,7 @@ final class PhabricatorAuthenticationConfigOptions "You can provide an arbitrary block of HTML here, which will ". "appear on the login screen. Normally, you'd use this to provide ". "login or registration instructions to users.")), - $this->newOption('account.editable', 'bool', true) + $this->newOption('account.editable', 'bool', true) ->setBoolOptions( array( pht("Allow editing"), @@ -83,7 +90,7 @@ final class PhabricatorAuthenticationConfigOptions "synchronize account information from some other authoritative ". "system, you can disable this to ensure information remains ". "consistent across both systems.")), - $this->newOption('account.minimum-password-length', 'int', 8) + $this->newOption('account.minimum-password-length', 'int', 8) ->setSummary(pht("Minimum password length.")) ->setDescription( pht( diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index 9f29205044..e4c5ee2949 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -76,6 +76,16 @@ final class PhabricatorCoreConfigOptions ->addExample('America/Chicago', pht('US Central (CDT)')) ->addExample('America/Boise', pht('US Mountain (MDT)')) ->addExample('America/Los_Angeles', pht('US West (PDT)')), + $this->newOption('phabricator.cookie-prefix', 'string', null) + ->setSummary( + pht("Set a string Phabricator should use to prefix ". + "cookie names")) + ->setDescription( + pht( + "Cookies set for x.com are also sent for y.x.com. Assuming ". + "Phabricator instances are running on both domains, this will ". + "create a collision preventing you from logging in.")) + ->addExample('dev', pht('Prefix cookie with "dev"')), $this->newOption('phabricator.show-beta-applications', 'bool', false) ->setBoolOptions( array( diff --git a/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php b/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php index 0b5b33b577..aa8ea620f6 100644 --- a/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php +++ b/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php @@ -28,18 +28,6 @@ final class PhabricatorExtendingPhabricatorConfigOptions "occur. Specify a list of classes which extend ". "PhabricatorEventListener here.")) ->addExample('MyEventListener', pht('Valid Setting')), - $this->newOption( - 'celerity.resource-path', - 'string', - '__celerity_resource_map__.php') - ->setLocked(true) - ->setSummary( - pht("Custom celerity resource map.")) - ->setDescription( - pht( - "Path to custom celerity resource map relative to ". - "'phabricator/src'. See also `scripts/celerity_mapper.php`.")) - ->addExample('local/my_celerity_map.php', pht('Valid Setting')), $this->newOption( 'aphront.default-application-configuration-class', 'class', diff --git a/src/applications/config/option/PhabricatorMailgunConfigOptions.php b/src/applications/config/option/PhabricatorMailgunConfigOptions.php new file mode 100644 index 0000000000..cc4fb56d6d --- /dev/null +++ b/src/applications/config/option/PhabricatorMailgunConfigOptions.php @@ -0,0 +1,28 @@ +newOption('mailgun.api-key', 'string', null) + ->setMasked(true) + ->setDescription(pht('Mailgun API key.')), + $this->newOption('mailgun.domain', 'string', null) + ->setDescription( + pht( + 'Mailgun domain name. See https://mailgun.com/cp/domains')) + ->addExample('mycompany.com', 'Use specific domain'), + ); + + } + +} diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index afd7738e27..18ae3d27c5 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -136,15 +136,6 @@ great. A number of other mailers are available (e.g., SES, SendGrid, SMTP, custom mailers), consult "Configuring Outbound Email" in the documentation for details. EODOC -)); - - $immediately_description = $this->deformat(pht(<<deformat(pht(<<setSummary(pht('Trust "Reply-To" headers for authentication.')) ->setDescription($reply_to_description), - $this->newOption('metamta.send-immediately', 'bool', true) - ->setBoolOptions( - array( - pht('Send Immediately (Slow)'), - pht('Send Via Daemons (Must Run Daemons)'), - )) - ->setSummary(pht('Improve performance by sending email via daemons.')) - ->setDescription($immediately_description), $this->newOption('metamta.placeholder-to-recipient', 'string', null) ->setSummary(pht('Placeholder for mail with only CCs.')) ->setDescription($placeholder_description), diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php index ae3f7f52b7..934684b91b 100644 --- a/src/applications/config/option/PhabricatorPHDConfigOptions.php +++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php @@ -41,6 +41,14 @@ final class PhabricatorPHDConfigOptions "of output, but can help debug issues. Daemons launched in debug ". "mode with 'phd debug' are always launched in verbose mode. See ". "also 'phd.trace'.")), + $this->newOption('phd.user', 'string', null) + ->setSummary(pht("System user to run daemons as.")) + ->setDescription( + pht( + "Specify a system user to run the daemons as. Primarily, this ". + "user will own the working copies of any repositories that ". + "Phabricator imports or manages. This option is new and ". + "experimental.")), $this->newOption('phd.trace', 'bool', false) ->setBoolOptions( array( diff --git a/src/applications/config/option/PhabricatorUIConfigOptions.php b/src/applications/config/option/PhabricatorUIConfigOptions.php new file mode 100644 index 0000000000..83c9f054c3 --- /dev/null +++ b/src/applications/config/option/PhabricatorUIConfigOptions.php @@ -0,0 +1,32 @@ +newOption('ui.header-color', 'enum', 'dark') + ->setDescription( + pht( + 'Sets the color of the main header.')) + ->setEnumOptions($options), + ); + } + +} diff --git a/src/applications/config/phid/PhabricatorConfigPHIDTypeConfig.php b/src/applications/config/phid/PhabricatorConfigPHIDTypeConfig.php index 204ab5b9a8..5742a7e9e5 100644 --- a/src/applications/config/phid/PhabricatorConfigPHIDTypeConfig.php +++ b/src/applications/config/phid/PhabricatorConfigPHIDTypeConfig.php @@ -39,8 +39,4 @@ final class PhabricatorConfigPHIDTypeConfig extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/config/view/PhabricatorSetupIssueView.php b/src/applications/config/view/PhabricatorSetupIssueView.php index f7bd7d68c8..ef8914e5b6 100644 --- a/src/applications/config/view/PhabricatorSetupIssueView.php +++ b/src/applications/config/view/PhabricatorSetupIssueView.php @@ -318,9 +318,14 @@ final class PhabricatorSetupIssueView extends AphrontView { array(), pht( 'You can find more information about PHP configuration values in the '. - 'PHP Documentation.', - 'http://php.net/manual/ini.list.php', - hsprintf(''))); + '%s.', + phutil_tag( + 'a', + array( + 'href' => 'http://php.net/manual/ini.list.php', + 'target' => '_blank', + ), + pht('PHP Documentation')))); $info[] = phutil_tag( 'p', diff --git a/src/applications/conpherence/application/PhabricatorApplicationConpherence.php b/src/applications/conpherence/application/PhabricatorApplicationConpherence.php index 04ce42b584..2668b79cb0 100644 --- a/src/applications/conpherence/application/PhabricatorApplicationConpherence.php +++ b/src/applications/conpherence/application/PhabricatorApplicationConpherence.php @@ -9,10 +9,6 @@ final class PhabricatorApplicationConpherence extends PhabricatorApplication { return '/conpherence/'; } - public function getQuickCreateURI() { - return $this->getBaseURI().'new/'; - } - public function getShortDescription() { return pht('Messaging'); } @@ -50,4 +46,18 @@ final class PhabricatorApplicationConpherence extends PhabricatorApplication { ); } + public function getQuickCreateItems(PhabricatorUser $viewer) { + $items = array(); + + $item = id(new PHUIListItemView()) + ->setName(pht('Conpherence Thread')) + ->setAppIcon('conpherence-dark') + ->setWorkflow(true) + ->setHref($this->getBaseURI().'new/'); + $items[] = $item; + + return $items; + } + + } diff --git a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php index 76c5897e62..1da1521bd2 100644 --- a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php +++ b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php @@ -72,8 +72,8 @@ final class ConpherenceNotificationPanelController } $content = $view->render(); } else { - $content = hsprintf( - '
%s
', + $content = phutil_tag_div( + 'phabricator-notification no-notifications', pht('You have no messages.')); } diff --git a/src/applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php b/src/applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php index 6e2fa8cd71..aba7adc756 100644 --- a/src/applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php +++ b/src/applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php @@ -53,13 +53,15 @@ final class ConpherenceCreateThreadMailReceiver $phids = mpull($users, 'getPHID'); $conpherence = id(new ConpherenceReplyHandler()) - ->setMailReceiver(new ConpherenceThread()) + ->setMailReceiver(ConpherenceThread::initializeNewThread($sender)) ->setMailAddedParticipantPHIDs($phids) ->setActor($sender) ->setExcludeMailRecipientPHIDs($mail->loadExcludeMailRecipientPHIDs()) ->processEmail($mail); - $mail->setRelatedPHID($conpherence->getPHID()); + if ($conpherence) { + $mail->setRelatedPHID($conpherence->getPHID()); + } } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 3e632fec27..0622f9fa9f 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -18,6 +18,12 @@ final class ConpherenceThread extends ConpherenceDAO private $widgetData = self::ATTACHABLE; private $images = array(); + public function initializeNewThread(PhabricatorUser $sender) { + return id(new ConpherenceThread()) + ->setMessageCount(0) + ->setTitle(''); + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php index e241aac4f1..d82cebaa5d 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php @@ -43,7 +43,7 @@ final class PhabricatorCountdownDeleteController $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle(pht('Really delete this countdown?')); - $dialog->appendChild(hsprintf('

%s

', $inst)); + $dialog->appendChild(phutil_tag('p', array(), $inst)); $dialog->addSubmitButton(pht('Delete')); $dialog->addCancelButton('/countdown/'); $dialog->setSubmitURI($request->getPath()); diff --git a/src/applications/countdown/controller/PhabricatorCountdownEditController.php b/src/applications/countdown/controller/PhabricatorCountdownEditController.php index 18d7b0bdff..e8443645be 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownEditController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownEditController.php @@ -35,7 +35,6 @@ final class PhabricatorCountdownEditController $countdown = PhabricatorCountdown::initializeNewCountdown($user); } - $error_view = null; $e_text = true; $e_epoch = null; @@ -71,11 +70,6 @@ final class PhabricatorCountdownEditController $countdown->save(); return id(new AphrontRedirectResponse()) ->setURI('/countdown/'.$countdown->getID().'/'); - } else { - $error_view = id(new AphrontErrorView()) - ->setErrors($errors) - ->setTitle(pht('It\'s not The Final Countdown (du nu nuuu nun)' . - ' until you fix these problem')); } } @@ -90,18 +84,11 @@ final class PhabricatorCountdownEditController $cancel_uri = '/countdown/'; if ($countdown->getID()) { $cancel_uri = '/countdown/'.$countdown->getID().'/'; - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('C'.$countdown->getID()) - ->setHref($cancel_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb('C'.$countdown->getID(), $cancel_uri); + $crumbs->addTextCrumb(pht('Edit')); $submit_label = pht('Save Changes'); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Countdown'))); + $crumbs->addTextCrumb(pht('Create Countdown')); $submit_label = pht('Create Countdown'); } @@ -142,7 +129,7 @@ final class PhabricatorCountdownEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/countdown/controller/PhabricatorCountdownViewController.php b/src/applications/countdown/controller/PhabricatorCountdownViewController.php index 1522b42513..7d6ed8c2ce 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownViewController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownViewController.php @@ -36,9 +36,7 @@ final class PhabricatorCountdownViewController $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("C{$id}")); + ->addTextCrumb("C{$id}"); $header = id(new PHUIHeaderView()) ->setHeader($title) diff --git a/src/applications/countdown/view/PhabricatorCountdownView.php b/src/applications/countdown/view/PhabricatorCountdownView.php index 9c09144b51..1f4b961144 100644 --- a/src/applications/countdown/view/PhabricatorCountdownView.php +++ b/src/applications/countdown/view/PhabricatorCountdownView.php @@ -42,30 +42,31 @@ final class PhabricatorCountdownView extends AphrontTagView { } - $container = celerity_generate_unique_node_id(); - $content = hsprintf( - '
- %s - - - - - - - - %s%s%s%s -
%s%s%s%s
-
', - $container, - $header, - pht('Days'), - pht('Hours'), - pht('Minutes'), - pht('Seconds'), + $ths = array( + phutil_tag('th', array(), pht('Days')), + phutil_tag('th', array(), pht('Hours')), + phutil_tag('th', array(), pht('Minutes')), + phutil_tag('th', array(), pht('Seconds')), + ); + + $dashes = array( javelin_tag('td', array('sigil' => 'phabricator-timer-days'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-hours'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-minutes'), '-'), - javelin_tag('td', array('sigil' => 'phabricator-timer-seconds'), '-')); + javelin_tag('td', array('sigil' => 'phabricator-timer-seconds'), '-'), + ); + + $container = celerity_generate_unique_node_id(); + $content = phutil_tag( + 'div', + array('class' => 'phabricator-timer', 'id' => $container), + array( + $header, + phutil_tag('table', array('class' => 'phabricator-timer-table'), array( + phutil_tag('tr', array(), $ths), + phutil_tag('tr', array(), $dashes), + )), + )); Javelin::initBehavior('countdown-timer', array( 'timestamp' => $countdown->getEpoch(), diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index ffaaf262f0..70eb3c78d3 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -7,13 +7,23 @@ final class PhabricatorDaemonConsoleController $request = $this->getRequest(); $user = $request->getUser(); + $window_start = (time() - (60 * 15)); + + // Assume daemons spend about 250ms second in overhead per task acquiring + // leases and doing other bookkeeping. This is probably an over-estimation, + // but we'd rather show that utilization is too high than too low. + $lease_overhead = 0.250; + $completed = id(new PhabricatorWorkerArchiveTask())->loadAllWhere( 'dateModified > %d', - time() - (60 * 15)); + $window_start); $failed = id(new PhabricatorWorkerActiveTask())->loadAllWhere( 'failureTime > %d', - time() - (60 * 15)); + $window_start); + + $usage_total = 0; + $usage_start = PHP_INT_MAX; $completed_info = array(); foreach ($completed as $completed_task) { @@ -27,6 +37,11 @@ final class PhabricatorDaemonConsoleController $completed_info[$class]['n']++; $duration = $completed_task->getDuration(); $completed_info[$class]['duration'] += $duration; + + // NOTE: Duration is in microseconds, but we're just using seconds to + // compute utilization. + $usage_total += $lease_overhead + ($duration / 1000000); + $usage_start = min($usage_start, $completed_task->getDateModified()); } $completed_info = isort($completed_info, 'n'); @@ -41,6 +56,13 @@ final class PhabricatorDaemonConsoleController } if ($failed) { + // Add the time it takes to restart the daemons. This includes a guess + // about other overhead of 2X. + $usage_total += PhutilDaemonOverseer::RESTART_WAIT * count($failed) * 2; + foreach ($failed as $failed_task) { + $usage_start = min($usage_start, $failed_task->getFailureTime()); + } + $rows[] = array( phutil_tag('em', array(), pht('Temporary Failures')), count($failed), @@ -48,6 +70,37 @@ final class PhabricatorDaemonConsoleController ); } + $logs = id(new PhabricatorDaemonLogQuery()) + ->setViewer($user) + ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) + ->setAllowStatusWrites(true) + ->execute(); + + $taskmasters = 0; + foreach ($logs as $log) { + if ($log->getDaemon() == 'PhabricatorTaskmasterDaemon') { + $taskmasters++; + } + } + + if ($taskmasters && $usage_total) { + // Total number of wall-time seconds the daemons have been running since + // the oldest event. For very short times round up to 15s so we don't + // render any ridiculous numbers if you reload the page immediately after + // restarting the daemons. + $available_time = $taskmasters * max(15, (time() - $usage_start)); + + // Percentage of those wall-time seconds we can account for, which the + // daemons spent doing work: + $used_time = ($usage_total / $available_time); + + $rows[] = array( + phutil_tag('em', array(), pht('Queue Utilization (Approximate)')), + sprintf('%.1f%%', 100 * $used_time), + null, + ); + } + $completed_table = new AphrontTableView($rows); $completed_table->setNoDataString( pht('No tasks have completed in the last 15 minutes.')); @@ -64,20 +117,10 @@ final class PhabricatorDaemonConsoleController 'n', )); - $completed_header = id(new PHUIHeaderView()) - ->setHeader(pht('Recently Completed Tasks (Last 15m)')); - - $completed_panel = new AphrontPanelView(); + $completed_panel = new PHUIObjectBoxView(); + $completed_panel->setHeaderText( + pht('Recently Completed Tasks (Last 15m)')); $completed_panel->appendChild($completed_table); - $completed_panel->setNoBackground(); - - $logs = id(new PhabricatorDaemonLogQuery()) - ->setViewer($user) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) - ->execute(); - - $daemon_header = id(new PHUIHeaderView()) - ->setHeader(pht('Active Daemons')); $daemon_table = new PhabricatorDaemonLogListView(); $daemon_table->setUser($user); @@ -104,6 +147,10 @@ final class PhabricatorDaemonConsoleController ); } + $daemon_panel = new PHUIObjectBoxView(); + $daemon_panel->setHeaderText(pht('Active Daemons')); + $daemon_panel->appendChild($daemon_table); + $leased_table = new AphrontTableView($rows); $leased_table->setHeaders( array( @@ -125,10 +172,9 @@ final class PhabricatorDaemonConsoleController )); $leased_table->setNoDataString(pht('No tasks are leased by workers.')); - $leased_panel = new AphrontPanelView(); - $leased_panel->setHeader('Leased Tasks'); + $leased_panel = new PHUIObjectBoxView(); + $leased_panel->setHeaderText(pht('Leased Tasks')); $leased_panel->appendChild($leased_table); - $leased_panel->setNoBackground(); $task_table = new PhabricatorWorkerActiveTask(); $queued = queryfx_all( @@ -158,25 +204,20 @@ final class PhabricatorDaemonConsoleController )); $queued_table->setNoDataString(pht('Task queue is empty.')); - $queued_panel = new AphrontPanelView(); - $queued_panel->setHeader(pht('Queued Tasks')); + $queued_panel = new PHUIObjectBoxView(); + $queued_panel->setHeaderText(pht('Queued Tasks')); $queued_panel->appendChild($queued_table); - $queued_panel->setNoBackground(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Console'))); + $crumbs->addTextCrumb(pht('Console')); $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $nav->appendChild( array( $crumbs, - $completed_header, $completed_panel, - $daemon_header, - $daemon_table, + $daemon_panel, $queued_panel, $leased_panel, )); diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php index a57b41c483..aaf5e00e8f 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php @@ -29,14 +29,11 @@ final class PhabricatorDaemonLogEventViewController $daemon_id = $event->getLogID(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Daemon %s', $daemon_id)) - ->setHref($this->getApplicationURI("log/{$daemon_id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Event %s', $event->getID()))); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb( + pht('Daemon %s', $daemon_id), + $this->getApplicationURI("log/{$daemon_id}/")) + ->addTextCrumb(pht('Event %s', $event->getID())); return $this->buildApplicationPage( diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogListController.php b/src/applications/daemon/controller/PhabricatorDaemonLogListController.php index 32aa288f46..0e5552dcf7 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogListController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogListController.php @@ -12,6 +12,7 @@ final class PhabricatorDaemonLogListController $logs = id(new PhabricatorDaemonLogQuery()) ->setViewer($viewer) + ->setAllowStatusWrites(true) ->executeWithCursorPager($pager); $daemon_table = new PhabricatorDaemonLogListView(); @@ -19,9 +20,7 @@ final class PhabricatorDaemonLogListController $daemon_table->setDaemonLogs($logs); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('All Daemons'))); + $crumbs->addTextCrumb(pht('All Daemons')); $nav = $this->buildSideNavView(); $nav->selectFilter('log'); diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php index 12fd283680..9da72ceb01 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -16,6 +16,7 @@ final class PhabricatorDaemonLogViewController $log = id(new PhabricatorDaemonLogQuery()) ->setViewer($user) ->withIDs(array($this->id)) + ->setAllowStatusWrites(true) ->executeOne(); if (!$log) { return new Aphront404Response(); @@ -26,36 +27,34 @@ final class PhabricatorDaemonLogViewController $log->getID()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Daemon %s', $log->getID()))); + $crumbs->addTextCrumb(pht('Daemon %s', $log->getID())); $header = id(new PHUIHeaderView()) ->setHeader($log->getDaemon()); - $tag = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE); + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE); $status = $log->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_UNKNOWN: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_ORANGE); + $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE); $tag->setName(pht('Unknown')); break; case PhabricatorDaemonLog::STATUS_RUNNING: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_GREEN); + $tag->setBackgroundColor(PHUITagView::COLOR_GREEN); $tag->setName(pht('Running')); break; case PhabricatorDaemonLog::STATUS_DEAD: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_RED); + $tag->setBackgroundColor(PHUITagView::COLOR_RED); $tag->setName(pht('Dead')); break; case PhabricatorDaemonLog::STATUS_WAIT: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_BLUE); + $tag->setBackgroundColor(PHUITagView::COLOR_BLUE); $tag->setName(pht('Waiting')); break; case PhabricatorDaemonLog::STATUS_EXITED: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_GREY); + $tag->setBackgroundColor(PHUITagView::COLOR_GREY); $tag->setName(pht('Exited')); break; } diff --git a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php index f8f9b39f72..04ec5a4490 100644 --- a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php +++ b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php @@ -41,26 +41,28 @@ final class PhabricatorWorkerTaskDetailController $actions = $this->buildActionListView($task); $properties = $this->buildPropertyListView($task, $actions); + $object_box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + $retry_head = id(new PHUIHeaderView()) ->setHeader(pht('Retries')); $retry_info = $this->buildRetryListView($task); - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + $retry_box = id(new PHUIObjectBoxView()) + ->setHeader($retry_head) + ->addPropertyList($retry_info); $content = array( $object_box, - $retry_head, - $retry_info, + $retry_box, ); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( @@ -120,6 +122,8 @@ final class PhabricatorWorkerTaskDetailController PhabricatorWorkerTask $task, PhabricatorActionListView $actions) { + $viewer = $this->getRequest()->getUser(); + $view = new PHUIPropertyListView(); $view->setActionList($actions); @@ -193,7 +197,7 @@ final class PhabricatorWorkerTaskDetailController $data = id(new PhabricatorWorkerTaskData())->load($task->getDataID()); $task->setData($data->getData()); $worker = $task->getWorkerInstance(); - $data = $worker->renderForDisplay(); + $data = $worker->renderForDisplay($viewer); $view->addProperty( pht('Data'), diff --git a/src/applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php b/src/applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php new file mode 100644 index 0000000000..22a11523ae --- /dev/null +++ b/src/applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php @@ -0,0 +1,24 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE epoch < %d LIMIT 100', + $table->getTableName(), + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php b/src/applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php new file mode 100644 index 0000000000..331462323d --- /dev/null +++ b/src/applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php @@ -0,0 +1,48 @@ +establishConnection('w'); + + $rows = queryfx_all( + $conn_w, + 'SELECT id, dataID FROM %T WHERE dateCreated < %d LIMIT 100', + $table->getTableName(), + time() - $ttl); + + if (!$rows) { + return false; + } + + $data_ids = array_filter(ipull($rows, 'dataID')); + $task_ids = ipull($rows, 'id'); + + $table->openTransaction(); + if ($data_ids) { + queryfx( + $conn_w, + 'DELETE FROM %T WHERE id IN (%Ld)', + $data_table->getTableName(), + $data_ids); + } + queryfx( + $conn_w, + 'DELETE FROM %T WHERE id IN (%Ld)', + $table->getTableName(), + $task_ids); + $table->saveTransaction(); + + return (count($task_ids) == 100); + } + +} diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php index d955807dbe..4c65c74e8f 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php @@ -32,8 +32,9 @@ final class PhabricatorDaemonManagementLogWorkflow $id = head($id); $daemon = id(new PhabricatorDaemonLogQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withIDs(array($id)) + ->setAllowStatusWrites(true) ->executeOne(); if (!$daemon) { diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index fef75d856c..54e071b201 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -1,11 +1,7 @@ allowStatusWrites = $allow; + return $this; + } + public function loadPage() { $table = new PhabricatorDaemonLog(); $conn_r = $table->establishConnection('r'); @@ -80,11 +86,16 @@ final class PhabricatorDaemonLogQuery $status = $status_dead; } - // If we changed the daemon's status, update it. + // If we changed the daemon's status, adjust it. if ($status != $daemon->getStatus()) { - $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); - $daemon->setStatus($status)->save(); - unset($guard); + $daemon->setStatus($status); + + // ...and write it, if we're in a context where that's reasonable. + if ($this->allowStatusWrites) { + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $daemon->save(); + unset($guard); + } } // If the daemon no longer matches the filter, get rid of it. diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php index 3ac45fae6d..1f4bbf4bdd 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php @@ -17,7 +17,9 @@ final class PhabricatorDaemonLogListView extends AphrontView { throw new Exception("Call setUser() before rendering!"); } - $list = id(new PHUIObjectItemListView()); + $list = id(new PHUIObjectItemListView()) + ->setCards(true) + ->setFlush(true); foreach ($this->daemonLogs as $log) { $id = $log->getID(); $epoch = $log->getDateCreated(); diff --git a/src/applications/dashboard/application/PhabricatorApplicationDashboard.php b/src/applications/dashboard/application/PhabricatorApplicationDashboard.php new file mode 100644 index 0000000000..20f49ef58d --- /dev/null +++ b/src/applications/dashboard/application/PhabricatorApplicationDashboard.php @@ -0,0 +1,40 @@ +\d+)' => 'PhabricatorDashboardPanelViewController', + '/dashboard/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PhabricatorDashboardListController', + 'view/(?P\d+)/' => 'PhabricatorDashboardViewController', + 'panel/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PhabricatorDashboardPanelListController', + ), + ), + ); + } + + public function shouldAppearInLaunchView() { + return false; + } + + public function canUninstall() { + return false; + } + +} diff --git a/src/applications/dashboard/controller/PhabricatorDashboardController.php b/src/applications/dashboard/controller/PhabricatorDashboardController.php new file mode 100644 index 0000000000..f7773cd42e --- /dev/null +++ b/src/applications/dashboard/controller/PhabricatorDashboardController.php @@ -0,0 +1,5 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PhabricatorDashboardSearchEngine()) + ->setNavigation($this->buildSideNavView()); + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $user = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new PhabricatorDashboardSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function renderResultsList( + array $dashboards, + PhabricatorSavedQuery $query) { + + return 'got '.count($dashboards).' ok'; + } + +} diff --git a/src/applications/dashboard/controller/PhabricatorDashboardPanelListController.php b/src/applications/dashboard/controller/PhabricatorDashboardPanelListController.php new file mode 100644 index 0000000000..d4531d8ab8 --- /dev/null +++ b/src/applications/dashboard/controller/PhabricatorDashboardPanelListController.php @@ -0,0 +1,43 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PhabricatorDashboardPanelSearchEngine()) + ->setNavigation($this->buildSideNavView()); + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $user = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new PhabricatorDashboardPanelSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function renderResultsList( + array $panels, + PhabricatorSavedQuery $query) { + + return 'got '.count($panels).' ok'; + } + +} diff --git a/src/applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php b/src/applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php new file mode 100644 index 0000000000..0af0fd3d9d --- /dev/null +++ b/src/applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php @@ -0,0 +1,42 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $dashboard = $objects[$phid]; + + $id = $dashboard->getID(); + + $handle->setName($dashboard->getName()); + $handle->setURI("/dashboard/view/{$id}/"); + } + } + +} diff --git a/src/applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php b/src/applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php new file mode 100644 index 0000000000..8b075e2602 --- /dev/null +++ b/src/applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php @@ -0,0 +1,73 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $panel = $objects[$phid]; + + $name = $panel->getName(); + $monogram = $panel->getMonogram(); + + $handle->setName($panel->getMonogram()); + $handle->setFullName("{$monogram} {$name}"); + $handle->setURI("/{$monogram}"); + } + } + + public function canLoadNamedObject($name) { + return preg_match('/^W\d*[1-9]\d*$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + $id_map = array(); + foreach ($names as $name) { + $id = (int)substr($name, 1); + $id_map[$id][] = $name; + } + + $objects = id(new PhabricatorDashboardPanelQuery()) + ->setViewer($query->getViewer()) + ->withIDs(array_keys($id_map)) + ->execute(); + + $results = array(); + foreach ($objects as $id => $object) { + foreach (idx($id_map, $id, array()) as $name) { + $results[$name] = $object; + } + } + + return $results; + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPanelQuery.php b/src/applications/dashboard/query/PhabricatorDashboardPanelQuery.php new file mode 100644 index 0000000000..8e13ee9d5f --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPanelQuery.php @@ -0,0 +1,60 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorDashboardPanel(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationDashboard'; + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php b/src/applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php new file mode 100644 index 0000000000..3245dc165e --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php @@ -0,0 +1,49 @@ + pht('All Panels'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardQuery.php b/src/applications/dashboard/query/PhabricatorDashboardQuery.php new file mode 100644 index 0000000000..461ec32a41 --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardQuery.php @@ -0,0 +1,60 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorDashboard(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationDashboard'; + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php b/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php new file mode 100644 index 0000000000..961a7e5f57 --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php @@ -0,0 +1,49 @@ + pht('All Dashboards'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php new file mode 100644 index 0000000000..96cbc21eec --- /dev/null +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -0,0 +1,52 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorDashboardPHIDTypeDashboard::TYPECONST); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/dashboard/storage/PhabricatorDashboardDAO.php b/src/applications/dashboard/storage/PhabricatorDashboardDAO.php new file mode 100644 index 0000000000..23adff06f2 --- /dev/null +++ b/src/applications/dashboard/storage/PhabricatorDashboardDAO.php @@ -0,0 +1,9 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorDashboardPHIDTypeDashboard::TYPECONST); + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getMonogram() { + return 'W'.$this->getID(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/differential/DifferentialGetWorkingCopy.php b/src/applications/differential/DifferentialGetWorkingCopy.php index 01641261d2..91245bf296 100644 --- a/src/applications/differential/DifferentialGetWorkingCopy.php +++ b/src/applications/differential/DifferentialGetWorkingCopy.php @@ -24,6 +24,12 @@ final class DifferentialGetWorkingCopy { 'clone -- file://%s %s', $origin_path, $path); + + if (!$repo->isHosted()) { + id(new ArcanistGitAPI($path))->execxLocal( + 'remote set-url origin %s', + $repo->getRemoteURI()); + } } $workspace = new ArcanistGitAPI($path); @@ -36,4 +42,32 @@ final class DifferentialGetWorkingCopy { return $workspace; } + /** + * Creates and/or cleans a workspace for the requested repo. + * + * return ArcanistMercurialAPI + */ + public static function getCleanMercurialWorkspace( + PhabricatorRepository $repo) { + + $origin_path = $repo->getLocalPath(); + + $path = rtrim($origin_path, '/'); + $path = $path . '__workspace'; + + if (!Filesystem::pathExists($path)) { + $repo->execxLocalCommand( + 'clone -- file://%s %s', + $origin_path, + $path); + } + + $workspace = new ArcanistMercurialAPI($path); + $workspace->execxLocal('pull'); + $workspace->execxLocal('update --clean default'); + $workspace->reloadWorkingCopy(); + + return $workspace; + } + } diff --git a/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php index 68a6284929..5ddfbb114d 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php @@ -1,8 +1,5 @@ 'required string', 'branch' => 'required string', 'bookmark' => 'optional string', - 'sourceControlSystem' => 'required enum', + 'sourceControlSystem' => 'required enum', 'sourceControlPath' => 'required string', 'sourceControlBaseRevision' => 'required string', - 'parentRevisionID' => 'optional revisionid', 'creationMethod' => 'optional string', - 'authorPHID' => 'optional phid', 'arcanistProject' => 'optional string', - 'repositoryUUID' => 'optional string', 'lintStatus' => 'required enum', 'unitStatus' => 'required enum', + 'repositoryPHID' => 'optional phid', + + 'parentRevisionID' => 'deprecated', + 'authorPHID' => 'deprecated', + 'repositoryUUID' => 'deprecated', ); } @@ -54,24 +53,23 @@ final class ConduitAPI_differential_creatediff_Method extends ConduitAPIMethod { $diff->setBranch($request->getValue('branch')); $diff->setCreationMethod($request->getValue('creationMethod')); - $diff->setAuthorPHID($request->getValue('authorPHID')); + $diff->setAuthorPHID($request->getUser()->getPHID()); $diff->setBookmark($request->getValue('bookmark')); - $parent_id = $request->getValue('parentRevisionID'); - if ($parent_id) { - // NOTE: If the viewer can't see the parent revision, just don't set - // a parent revision ID. This isn't used for anything meaningful. - // TODO: Can we delete this entirely? - $parent_rev = id(new DifferentialRevisionQuery()) + // TODO: Remove this eventually; for now continue writing the UUID. Note + // that we'll overwrite it below if we identify a repository, and `arc` + // no longer sends it. This stuff is retained for backward compatibility. + $diff->setRepositoryUUID($request->getValue('repositoryUUID')); + + $repository_phid = $request->getValue('repositoryPHID'); + if ($repository_phid) { + $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($request->getUser()) - ->withIDs(array($parent_id)) - ->execute(); - if ($parent_rev) { - $parent_rev = head($parent_rev); - if ($parent_rev->getStatus() != - ArcanistDifferentialRevisionStatus::CLOSED) { - $diff->setParentRevisionID($parent_id); - } + ->withPHIDs(array($repository_phid)) + ->executeOne(); + if ($repository) { + $diff->setRepositoryPHID($repository->getPHID()); + $diff->setRepositoryUUID($repository->getUUID()); } } @@ -97,7 +95,6 @@ final class ConduitAPI_differential_creatediff_Method extends ConduitAPIMethod { } $diff->setArcanistProjectPHID($project_phid); - $diff->setRepositoryUUID($request->getValue('repositoryUUID')); switch ($request->getValue('lintStatus')) { case 'skip': diff --git a/src/applications/differential/conduit/ConduitAPI_differential_getdiff_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_getdiff_Method.php index 2c864a43ab..dd5d6e495f 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_getdiff_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_getdiff_Method.php @@ -6,6 +6,10 @@ final class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod { + public function shouldAllowPublic() { + return true; + } + public function getMethodStatus() { return self::METHOD_STATUS_DEPRECATED; } @@ -38,10 +42,6 @@ final class ConduitAPI_differential_getdiff_Method ); } - public function shouldRequireAuthentication() { - return !PhabricatorEnv::getEnvConfig('differential.anonymous-access'); - } - protected function execute(ConduitAPIRequest $request) { $diff_id = $request->getValue('diff_id'); diff --git a/src/applications/differential/conduit/ConduitAPI_differential_query_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_query_Method.php index 6096c8386d..8d9d62261b 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_query_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_query_Method.php @@ -202,27 +202,28 @@ final class ConduitAPI_differential_query_Method $auxiliary_fields = $this->loadAuxiliaryFields( $revision, $request->getUser()); $result = array( - 'id' => $id, - 'phid' => $revision->getPHID(), - 'title' => $revision->getTitle(), - 'uri' => PhabricatorEnv::getProductionURI('/D'.$id), - 'dateCreated' => $revision->getDateCreated(), - 'dateModified' => $revision->getDateModified(), - 'authorPHID' => $revision->getAuthorPHID(), - 'status' => $revision->getStatus(), - 'statusName' => + 'id' => $id, + 'phid' => $revision->getPHID(), + 'title' => $revision->getTitle(), + 'uri' => PhabricatorEnv::getProductionURI('/D'.$id), + 'dateCreated' => $revision->getDateCreated(), + 'dateModified' => $revision->getDateModified(), + 'authorPHID' => $revision->getAuthorPHID(), + 'status' => $revision->getStatus(), + 'statusName' => ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $revision->getStatus()), - 'branch' => $diff->getBranch(), - 'summary' => $revision->getSummary(), - 'testPlan' => $revision->getTestPlan(), - 'lineCount' => $revision->getLineCount(), - 'diffs' => $revision->getDiffIDs(), - 'commits' => $revision->getCommitPHIDs(), - 'reviewers' => array_values($revision->getReviewers()), - 'ccs' => array_values($revision->getCCPHIDs()), - 'hashes' => $revision->getHashes(), - 'auxiliary' => $auxiliary_fields, + 'branch' => $diff->getBranch(), + 'summary' => $revision->getSummary(), + 'testPlan' => $revision->getTestPlan(), + 'lineCount' => $revision->getLineCount(), + 'diffs' => $revision->getDiffIDs(), + 'commits' => $revision->getCommitPHIDs(), + 'reviewers' => array_values($revision->getReviewers()), + 'ccs' => array_values($revision->getCCPHIDs()), + 'hashes' => $revision->getHashes(), + 'auxiliary' => $auxiliary_fields, + 'arcanistProjectPHID' => $diff->getArcanistProjectPHID() ); // TODO: This is a hacky way to put permissions on this field until we diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php index 1c0033b7d5..5909fb71a6 100644 --- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -96,18 +96,6 @@ final class PhabricatorDifferentialConfigOptions 'sketchy and implies the revision may not actually be receiving '. 'thorough review. You can enable "!accept" by setting this '. 'option to true.')), - $this->newOption('differential.anonymous-access', 'bool', false) - ->setBoolOptions( - array( - pht('Allow guests to view revisions'), - pht('Require authentication to view revisions'), - )) - ->setSummary(pht('Anonymous access to Differential revisions.')) - ->setDescription( - pht( - "If you set this to true, users won't need to login to view ". - "Differential revisions. Anonymous users will have read-only ". - "access and won't be able to interact with the revisions.")), $this->newOption('differential.generated-paths', 'list', array()) ->setSummary(pht("File regexps to treat as automatically generated.")) ->setDescription( @@ -158,6 +146,26 @@ final class PhabricatorDifferentialConfigOptions "is accidentally closed or if a developer changes his or her ". "mind after closing a revision. If it is false, reopening ". "is not allowed.")), + $this->newOption('differential.close-on-accept', 'bool', false) + ->setBoolOptions( + array( + pht('Treat Accepted Revisions as "Closed"'), + pht('Treat Accepted Revisions as "Open"'), + )) + ->setSummary(pht('Allows "Accepted" to act as a closed status.')) + ->setDescription( + pht( + 'Normally, Differential revisions remain on the dashboard when '. + 'they are "Accepted", and the author then commits the changes '. + 'to "Close" the revision and move it off the dashboard.'. + "\n\n". + 'If you have an unusual workflow where Differential is used for '. + 'post-commit review (normally called "Audit", elsewhere in '. + 'Phabricator), you can set this flag to treat the "Accepted" '. + 'state as a "Closed" state and end the review workflow early.'. + "\n\n". + 'This sort of workflow is very unusual. Very few installs should '. + 'need to change this option.')), $this->newOption('differential.days-fresh', 'int', 1) ->setSummary( pht( diff --git a/src/applications/differential/constants/DifferentialRevisionStatus.php b/src/applications/differential/constants/DifferentialRevisionStatus.php index 1088ac40e3..a35e00e9cc 100644 --- a/src/applications/differential/constants/DifferentialRevisionStatus.php +++ b/src/applications/differential/constants/DifferentialRevisionStatus.php @@ -70,4 +70,36 @@ final class DifferentialRevisionStatus { return $tag; } + + public static function getClosedStatuses() { + $statuses = array( + ArcanistDifferentialRevisionStatus::CLOSED, + ArcanistDifferentialRevisionStatus::ABANDONED, + ); + + if (PhabricatorEnv::getEnvConfig('differential.close-on-accept')) { + $statuses[] = ArcanistDifferentialRevisionStatus::ACCEPTED; + } + + return $statuses; + } + + public static function getOpenStatuses() { + return array_diff(self::getAllStatuses(), self::getClosedStatuses()); + } + + public static function getAllStatuses() { + return array( + ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, + ArcanistDifferentialRevisionStatus::NEEDS_REVISION, + ArcanistDifferentialRevisionStatus::ACCEPTED, + ArcanistDifferentialRevisionStatus::CLOSED, + ArcanistDifferentialRevisionStatus::ABANDONED, + ); + } + + public static function isClosedStatus($status) { + return in_array($status, self::getClosedStatuses()); + } + } diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php index a99ad41268..b952c7dee4 100644 --- a/src/applications/differential/controller/DifferentialChangesetViewController.php +++ b/src/applications/differential/controller/DifferentialChangesetViewController.php @@ -2,14 +2,6 @@ final class DifferentialChangesetViewController extends DifferentialController { - public function shouldRequireLogin() { - if ($this->allowsAnonymousAccess()) { - return false; - } - - return parent::shouldRequireLogin(); - } - public function shouldAllowPublic() { return true; } diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php index f61722d078..c6a27020bc 100644 --- a/src/applications/differential/controller/DifferentialController.php +++ b/src/applications/differential/controller/DifferentialController.php @@ -2,10 +2,6 @@ abstract class DifferentialController extends PhabricatorController { - protected function allowsAnonymousAccess() { - return PhabricatorEnv::getEnvConfig('differential.anonymous-access'); - } - public function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index d190234b7b..e88b74bd09 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -66,7 +66,7 @@ final class DifferentialDiffCreateController extends DifferentialController { $arcanist_link, phutil_tag('tt', array(), 'svn diff'), phutil_tag('tt', array(), 'git diff'), - phutil_tag('tt', array(), 'hg diff')); + phutil_tag('tt', array(), 'hg diff --git')); } $form @@ -92,18 +92,11 @@ final class DifferentialDiffCreateController extends DifferentialController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Diff')) - ->setFormError($errors) - ->setForm($form); + ->setForm($form) + ->setFormErrors($errors); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Diff'))); - - if ($errors) { - $errors = id(new AphrontErrorView()) - ->setErrors($errors); - } + $crumbs->addTextCrumb(pht('Create Diff')); return $this->buildApplicationPage( array( diff --git a/src/applications/differential/controller/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php index e55cb30afd..b20c6cd8d0 100644 --- a/src/applications/differential/controller/DifferentialDiffViewController.php +++ b/src/applications/differential/controller/DifferentialDiffViewController.php @@ -20,10 +20,10 @@ final class DifferentialDiffViewController extends DifferentialController { return new Aphront404Response(); } + $error_view = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE); if ($diff->getRevisionID()) { - $top_part = id(new AphrontErrorView()) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->appendChild( + $error_view->appendChild( pht( 'This diff belongs to revision %s.', phutil_tag( @@ -36,8 +36,9 @@ final class DifferentialDiffViewController extends DifferentialController { // TODO: implement optgroup support in AphrontFormSelectControl? $select = array(); $select[] = hsprintf('', pht('Create New Revision')); - $select[] = hsprintf( - '', + $select[] = phutil_tag( + 'option', + array('value' => ''), pht('Create a new Revision...')); $select[] = hsprintf(''); @@ -84,7 +85,7 @@ final class DifferentialDiffViewController extends DifferentialController { id(new AphrontFormSubmitControl()) ->setValue(pht('Continue'))); - $top_part = $form; + $error_view->appendChild($form); } $props = id(new DifferentialDiffProperty())->loadAllWhere( @@ -145,16 +146,17 @@ final class DifferentialDiffViewController extends DifferentialController { ->setUser($request->getUser()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Diff %d', $diff->getID()))); + $crumbs->addTextCrumb(pht('Diff %d', $diff->getID())); + + $prop_box = id(new PHUIObjectBoxView()) + ->setHeader($property_head) + ->addPropertyList($property_view) + ->setErrorView($error_view); return $this->buildApplicationPage( array( $crumbs, - $top_part, - $property_head, - $property_view, + $prop_box, $table_of_contents, $details, ), diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index a136274dea..a070dea9c9 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -108,13 +108,6 @@ final class DifferentialRevisionEditController extends DifferentialController { $form->setAction('/differential/revision/edit/'); } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - if ($diff && $revision->getID()) { $form ->appendChild( @@ -153,16 +146,14 @@ final class DifferentialRevisionEditController extends DifferentialController { if ($revision->getID()) { if ($diff) { $title = pht('Update Differential Revision'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('D'.$revision->getID()) - ->setHref('/differential/diff/'.$diff->getID().'/')); + $crumbs->addTextCrumb( + 'D'.$revision->getID(), + '/differential/diff/'.$diff->getID().'/'); } else { $title = pht('Edit Differential Revision'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('D'.$revision->getID()) - ->setHref('/D'.$revision->getID())); + $crumbs->addTextCrumb( + 'D'.$revision->getID(), + '/D'.$revision->getID()); } } else { $title = pht('Create New Differential Revision'); @@ -170,12 +161,10 @@ final class DifferentialRevisionEditController extends DifferentialController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php index 003c4900ac..aecc5644b7 100644 --- a/src/applications/differential/controller/DifferentialRevisionLandController.php +++ b/src/applications/differential/controller/DifferentialRevisionLandController.php @@ -36,39 +36,62 @@ final class DifferentialRevisionLandController extends DifferentialController { } if ($request->isDialogFormPost()) { + $response = null; + $text = ''; try { - $this->attemptLand($revision, $request); + $response = $this->attemptLand($revision, $request); $title = pht("Success!"); $text = pht("Revision was successfully landed."); } catch (Exception $ex) { $title = pht("Failed to land revision"); - $text = 'moo'; if ($ex instanceof PhutilProxyException) { $text = hsprintf( '%s:
%s
', $ex->getMessage(), $ex->getPreviousException()->getMessage()); } else { - $text = hsprintf('
%s
', $ex->getMessage()); + $text = phutil_tag('pre', array(), $ex->getMessage()); } $text = id(new AphrontErrorView()) ->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($title) - ->appendChild(phutil_tag('p', array(), $text)) - ->setSubmitURI('/D'.$revision_id) - ->addSubmitButton(pht('Done')); + ->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 '. - 'origin/master.', + 'the default / master branch.', $revision_id), pht('It is an experimental feature and may not work.')); @@ -108,7 +131,7 @@ final class DifferentialRevisionLandController extends DifferentialController { $lock = $this->lockRepository($repository); try { - $this->pushStrategy->processLandRequest( + $response = $this->pushStrategy->processLandRequest( $request, $revision, $repository); @@ -118,6 +141,7 @@ final class DifferentialRevisionLandController extends DifferentialController { } $lock->unlock(); + return $response; } private function lockRepository($repository) { diff --git a/src/applications/differential/controller/DifferentialRevisionListController.php b/src/applications/differential/controller/DifferentialRevisionListController.php index 8322f5615b..39cc2a605b 100644 --- a/src/applications/differential/controller/DifferentialRevisionListController.php +++ b/src/applications/differential/controller/DifferentialRevisionListController.php @@ -5,13 +5,6 @@ final class DifferentialRevisionListController extends DifferentialController private $queryKey; - public function shouldRequireLogin() { - if ($this->allowsAnonymousAccess()) { - return false; - } - return parent::shouldRequireLogin(); - } - public function shouldAllowPublic() { return true; } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index ee0e13f6eb..8e15dd7de3 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -4,13 +4,6 @@ final class DifferentialRevisionViewController extends DifferentialController { private $revisionID; - public function shouldRequireLogin() { - if ($this->allowsAnonymousAccess()) { - return false; - } - return parent::shouldRequireLogin(); - } - public function shouldAllowPublic() { return true; } @@ -77,6 +70,7 @@ final class DifferentialRevisionViewController extends DifferentialController { if ($request->getExists('download')) { return $this->buildRawDiffResponse( + $revision, $changesets, $vs_changesets, $vs_map, @@ -428,10 +422,7 @@ final class DifferentialRevisionViewController extends DifferentialController { ); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($object_id) - ->setHref('/'.$object_id)); + $crumbs->addTextCrumb($object_id, '/'.$object_id); $prefs = $user->loadPreferences(); @@ -506,8 +497,8 @@ final class DifferentialRevisionViewController extends DifferentialController { ); } - require_celerity_resource('phabricator-object-selector-css'); - require_celerity_resource('javelin-behavior-phabricator-object-selector'); + $this->requireResource('phabricator-object-selector-css'); + $this->requireResource('javelin-behavior-phabricator-object-selector'); $links[] = array( 'icon' => 'link', @@ -860,6 +851,7 @@ final class DifferentialRevisionViewController extends DifferentialController { * @return @{class:AphrontRedirectResponse} */ private function buildRawDiffResponse( + DifferentialRevision $revision, array $changesets, array $vs_changesets, array $vs_map, @@ -870,43 +862,12 @@ final class DifferentialRevisionViewController extends DifferentialController { $viewer = $this->getRequest()->getUser(); - $engine = new PhabricatorDifferenceEngine(); - $generated_changesets = array(); foreach ($changesets as $changeset) { $changeset->attachHunks($changeset->loadHunks()); - $right = $changeset->makeNewFile(); - $choice = $changeset; - $vs = idx($vs_map, $changeset->getID()); - if ($vs == -1) { - $left = $right; - $right = $changeset->makeOldFile(); - } else if ($vs) { - $choice = $vs_changeset = $vs_changesets[$vs]; - $vs_changeset->attachHunks($vs_changeset->loadHunks()); - $left = $vs_changeset->makeNewFile(); - } else { - $left = $changeset->makeOldFile(); - } - - $synthetic = $engine->generateChangesetFromFileContent( - $left, - $right); - - if (!$synthetic->getAffectedLineCount()) { - $filetype = $choice->getFileType(); - if ($filetype == DifferentialChangeType::FILE_TEXT || - $filetype == DifferentialChangeType::FILE_SYMLINK) { - continue; - } - } - - $choice->attachHunks($synthetic->getHunks()); - - $generated_changesets[] = $choice; } $diff = new DifferentialDiff(); - $diff->attachChangesets($generated_changesets); + $diff->attachChangesets($changesets); $raw_changes = $diff->buildChangesList(); $changes = array(); foreach ($raw_changes as $changedict) { @@ -951,8 +912,16 @@ final class DifferentialRevisionViewController extends DifferentialController { $raw_diff, array( 'name' => $file_name, + 'ttl' => (60 * 60 * 24), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file->attachToObject( + $this->getRequest()->getUser(), + $revision->getPHID()); + unset($unguarded); + return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 74f9117ced..5bfd5ee836 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -78,13 +78,7 @@ final class DifferentialDoorkeeperRevisionFeedStoryPublisher } public function isObjectClosed($object) { - switch ($object->getStatus()) { - case ArcanistDifferentialRevisionStatus::CLOSED: - case ArcanistDifferentialRevisionStatus::ABANDONED: - return true; - default: - return false; - } + return $object->isClosed(); } public function getResponsibilityTitle($object) { diff --git a/src/applications/differential/editor/DifferentialCommentEditor.php b/src/applications/differential/editor/DifferentialCommentEditor.php index 0fef83eb2d..3884f5c3ae 100644 --- a/src/applications/differential/editor/DifferentialCommentEditor.php +++ b/src/applications/differential/editor/DifferentialCommentEditor.php @@ -700,7 +700,7 @@ final class DifferentialCommentEditor extends PhabricatorEditor { ->publish(); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($revision->getPHID()); + ->queueDocumentForIndexing($revision->getPHID()); return $comment; } diff --git a/src/applications/differential/editor/DifferentialRevisionEditor.php b/src/applications/differential/editor/DifferentialRevisionEditor.php index 028948d32b..bf3d82068c 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditor.php +++ b/src/applications/differential/editor/DifferentialRevisionEditor.php @@ -273,6 +273,11 @@ final class DifferentialRevisionEditor extends PhabricatorEditor { $rem_ccs = $adapter->getCCsRemovedByHerald(); $blocking_reviewers = array_keys( $adapter->getBlockingReviewersAddedByHerald()); + + HarbormasterBuildable::applyBuildPlans( + $diff->getPHID(), + $revision->getPHID(), + $adapter->getBuildPlans()); } else { $sub = array( 'rev' => array(), @@ -529,7 +534,7 @@ final class DifferentialRevisionEditor extends PhabricatorEditor { ->publish(); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($revision->getPHID()); + ->queueDocumentForIndexing($revision->getPHID()); } public static function addCCAndUpdateRevision( diff --git a/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php b/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php index f591bad95e..ac7a287c4f 100644 --- a/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php @@ -36,13 +36,6 @@ final class DifferentialArcanistProjectFieldSpecification } public function renderValueForMail($phase) { - $status = $this->getRevision()->getStatus(); - - if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION && - $status != ArcanistDifferentialRevisionStatus::ACCEPTED) { - return null; - } - $diff = $this->getRevision()->loadActiveDiff(); if ($diff) { $phid = $diff->getArcanistProjectPHID(); diff --git a/src/applications/differential/field/specification/DifferentialAsanaRepresentationFieldSpecification.php b/src/applications/differential/field/specification/DifferentialAsanaRepresentationFieldSpecification.php index 7edc8ccdd7..0ad3c69022 100644 --- a/src/applications/differential/field/specification/DifferentialAsanaRepresentationFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialAsanaRepresentationFieldSpecification.php @@ -65,11 +65,11 @@ final class DifferentialAsanaRepresentationFieldSpecification ), )); - return id(new PhabricatorTagView()) + return id(new PHUITagView()) ->setID($tag_id) ->setName($href) ->setHref($href) - ->setType(PhabricatorTagView::TYPE_OBJECT) + ->setType(PHUITagView::TYPE_OBJECT) ->setExternal(true); } diff --git a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php index 40ab4400d0..d5fbcf2b49 100644 --- a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php @@ -32,13 +32,6 @@ final class DifferentialBranchFieldSpecification } public function renderValueForMail($phase) { - $status = $this->getRevision()->getStatus(); - - if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION && - $status != ArcanistDifferentialRevisionStatus::ACCEPTED) { - return null; - } - $diff = $this->getRevision()->loadActiveDiff(); if ($diff) { $description = $this->getBranchOrBookmarkDescription($diff); diff --git a/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php b/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php index a30a6ec788..62963e3d67 100644 --- a/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialEditPolicyFieldSpecification.php @@ -32,10 +32,7 @@ final class DifferentialEditPolicyFieldSpecification ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($revision) ->setPolicies($policies) - ->setName('editPolicy') - ->setCaption( - pht( - 'NOTE: These policy features are not yet fully supported.')); + ->setName('editPolicy'); } public function willWriteRevision(DifferentialRevisionEditor $editor) { diff --git a/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php b/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php index 0bd42805f5..8f9394d069 100644 --- a/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php @@ -58,16 +58,11 @@ final class DifferentialJIRAIssuesFieldSpecification $links = array(); foreach ($xobjs as $xobj) { - $links[] = phutil_tag( - 'a', - array( - 'href' => $xobj->getObjectURI(), - 'target' => '_blank', - ), - $xobj->getObjectID()); + $links[] = id(new DoorkeeperTagView()) + ->setExternalObject($xobj); } - return phutil_implode_html(', ', $links); + return phutil_implode_html(phutil_tag('br'), $links); } public function shouldAppearOnConduitView() { diff --git a/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php b/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php index eecce6657b..64533ea65e 100644 --- a/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php @@ -70,8 +70,24 @@ final class DifferentialUnitFieldSpecification $udata[$key]['sort'] = idx($sort_map, idx($test, 'result')); } $udata = isort($udata, 'sort'); - - foreach ($udata as $test) { + $engine = new PhabricatorMarkupEngine(); + $engine->setViewer($this->getUser()); + $markup_objects = array(); + foreach ($udata as $key => $test) { + $userdata = idx($test, 'userdata'); + if ($userdata) { + if ($userdata !== false) { + $userdata = str_replace("\000", '', $userdata); + } + $markup_object = id(new PhabricatorMarkupOneOff()) + ->setContent($userdata) + ->setPreserveLinebreaks(true); + $engine->addObject($markup_object, 'default'); + $markup_objects[$key] = $markup_object; + } + } + $engine->process(); + foreach ($udata as $key => $test) { $result = idx($test, 'result'); $default_hide = false; @@ -110,17 +126,10 @@ final class DifferentialUnitFieldSpecification 'show' => $show, ); - $userdata = idx($test, 'userdata'); - if ($userdata) { - if ($userdata !== false) { - $userdata = str_replace("\000", '', $userdata); - } - $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); - $engine->setConfig('viewer', $this->getUser()); - $userdata = $engine->markupText($userdata); + if (isset($markup_objects[$key])) { $rows[] = array( 'style' => 'details', - 'value' => $userdata, + 'value' => $engine->getOutput($markup_objects[$key], 'default'), 'show' => false, ); if (empty($hidden['details'])) { diff --git a/src/applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php b/src/applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php new file mode 100644 index 0000000000..08cdfe97db --- /dev/null +++ b/src/applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php @@ -0,0 +1,25 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', + DifferentialChangeset::TABLE_CACHE, + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php index fdfea16ae0..f2fb2b16c1 100644 --- a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php +++ b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php @@ -1,5 +1,8 @@ getValue('object'); - - $actions = null; if ($object instanceof DifferentialRevision) { - $actions = $this->renderRevisionAction($event); + $this->renderRevisionAction($event); } - - $this->addActionMenuItems($event, $actions); } private function renderRevisionAction(PhutilEvent $event) { @@ -42,13 +41,15 @@ final class DifferentialLandingActionMenuEventListener ->setAncestorClass('DifferentialLandingStrategy') ->loadObjects(); foreach ($strategies as $strategy) { - $actions = $strategy->createMenuItems( - $event->getUser(), - $revision, - $repository); - $this->addActionMenuItems($event, $actions); + $viewer = $event->getUser(); + $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 index 46a0f74333..aea5d83719 100644 --- a/src/applications/differential/landing/DifferentialLandingStrategy.php +++ b/src/applications/differential/landing/DifferentialLandingStrategy.php @@ -8,9 +8,9 @@ abstract class DifferentialLandingStrategy { PhabricatorRepository $repository); /** - * returns PhabricatorActionView or an array of PhabricatorActionView or null. + * returns PhabricatorActionView or null. */ - abstract function createMenuItems( + abstract function createMenuItem( PhabricatorUser $viewer, DifferentialRevision $revision, PhabricatorRepository $repository); @@ -18,14 +18,43 @@ abstract class DifferentialLandingStrategy { /** * returns PhabricatorActionView which can be attached to the revision view. */ - protected function createActionView($revision, $name, $disabled = false) { + protected function createActionView($revision, $name) { $strategy = get_class($this); $revision_id = $revision->getId(); return id(new PhabricatorActionView()) ->setRenderAsForm(true) + ->setWorkflow(true) ->setName($name) - ->setHref("/differential/revision/land/{$revision_id}/{$strategy}/") - ->setDisabled($disabled); + ->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, + DiffusionCapabilityPush::CAPABILITY)) { + return pht("You do not have permissions to push to this repository."); + } + + return false; } /** @@ -35,7 +64,21 @@ abstract class DifferentialLandingStrategy { try { return DifferentialGetWorkingCopy::getCleanGitWorkspace($repository); } catch (Exception $e) { - throw new PhutilProxyException ( + throw new PhutilProxyException( + '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( 'Failed to allocate a workspace', $e); } diff --git a/src/applications/differential/landing/DifferentialLandingToGitHub.php b/src/applications/differential/landing/DifferentialLandingToGitHub.php new file mode 100644 index 0000000000..00c5362b22 --- /dev/null +++ b/src/applications/differential/landing/DifferentialLandingToGitHub.php @@ -0,0 +1,178 @@ +getUser(); + $this->init($viewer, $repository); + + $workspace = $this->getGitWorkspace($repository); + + try { + id(new DifferentialLandingToHostedGit()) + ->commitRevisionToWorkspace( + $revision, + $workspace, + $viewer); + } catch (Exception $e) { + throw new PhutilProxyException( + '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( + 'Failed to push changes upstream', + $e); + } + } + + /** + * returns PhabricatorActionView or an array of PhabricatorActionView or null. + */ + public function createMenuItem( + PhabricatorUser $viewer, + DifferentialRevision $revision, + PhabricatorRepository $repository) { + + $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('octocat'); + } + + 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)) + ->executeOne(); + + if (!$this->account) { + throw new Exception( + "No matching GitHub account found for {$repo_domain}."); + } + + $this->provider = PhabricatorAuthProvider::getEnabledProviderByKey( + $this->account->getProviderKey()); + if (!$this->provider) { + throw new Exception("GitHub provider for {$repo_domain} is not enabled."); + } + } + + 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( + "You don't have permission to push to this repository. \n". + "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()) + ->addSubmitButton(pht('Refresh Account Link')); + } + } +} diff --git a/src/applications/differential/landing/DifferentialLandingToHostedGit.php b/src/applications/differential/landing/DifferentialLandingToHostedGit.php index 874b0aff2c..8438492aba 100644 --- a/src/applications/differential/landing/DifferentialLandingToHostedGit.php +++ b/src/applications/differential/landing/DifferentialLandingToHostedGit.php @@ -105,7 +105,7 @@ final class DifferentialLandingToHostedGit $workspace->execxLocal("push origin HEAD:master"); } - public function createMenuItems( + public function createMenuItem( PhabricatorUser $viewer, DifferentialRevision $revision, PhabricatorRepository $repository) { @@ -123,14 +123,8 @@ final class DifferentialLandingToHostedGit return; } - $can_push = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - DiffusionCapabilityPush::CAPABILITY); - return $this->createActionView( $revision, - pht('Land to Hosted Repository'), - !$can_push); + pht('Land to Hosted Repository')); } } diff --git a/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php b/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php new file mode 100644 index 0000000000..029f821814 --- /dev/null +++ b/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php @@ -0,0 +1,114 @@ +getUser(); + + $workspace = $this->getMercurialWorkspace($repository); + + try { + $this->commitRevisionToWorkspace( + $revision, + $workspace, + $viewer); + } catch (Exception $e) { + throw new PhutilProxyException( + 'Failed to commit patch', + $e); + } + + try { + $this->pushWorkspaceRepository( + $repository, + $workspace, + $viewer); + } catch (Exception $e) { + throw new PhutilProxyException( + '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/mail/DifferentialCommentMail.php b/src/applications/differential/mail/DifferentialCommentMail.php index bfe6a9cf48..ccf4020b63 100644 --- a/src/applications/differential/mail/DifferentialCommentMail.php +++ b/src/applications/differential/mail/DifferentialCommentMail.php @@ -56,7 +56,10 @@ final class DifferentialCommentMail extends DifferentialMail { break; } - if (strlen(trim($comment->getContent()))) { + $has_comment = strlen(trim($comment->getContent())); + $has_inlines = (bool)$this->getInlineComments(); + + if ($has_comment || $has_inlines) { switch ($action) { case DifferentialAction::ACTION_CLOSE: // Commit comments are auto-generated and not especially interesting, @@ -68,6 +71,10 @@ final class DifferentialCommentMail extends DifferentialMail { } } + if (!$tags) { + $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER; + } + return $tags; } diff --git a/src/applications/differential/phid/DifferentialPHIDTypeDiff.php b/src/applications/differential/phid/DifferentialPHIDTypeDiff.php index 5b8029bd35..f7ddcc7fe2 100644 --- a/src/applications/differential/phid/DifferentialPHIDTypeDiff.php +++ b/src/applications/differential/phid/DifferentialPHIDTypeDiff.php @@ -38,8 +38,4 @@ final class DifferentialPHIDTypeDiff extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/differential/phid/DifferentialPHIDTypeRevision.php b/src/applications/differential/phid/DifferentialPHIDTypeRevision.php index f13bd6770f..f576e0e062 100644 --- a/src/applications/differential/phid/DifferentialPHIDTypeRevision.php +++ b/src/applications/differential/phid/DifferentialPHIDTypeRevision.php @@ -29,11 +29,6 @@ final class DifferentialPHIDTypeRevision extends PhabricatorPHIDType { array $handles, array $objects) { - static $closed_statuses = array( - ArcanistDifferentialRevisionStatus::CLOSED => true, - ArcanistDifferentialRevisionStatus::ABANDONED => true, - ); - foreach ($handles as $phid => $handle) { $revision = $objects[$phid]; @@ -45,7 +40,7 @@ final class DifferentialPHIDTypeRevision extends PhabricatorPHIDType { $handle->setURI("/D{$id}"); $handle->setFullName("D{$id}: {$title}"); - if (isset($closed_statuses[$status])) { + if ($revision->isClosed()) { $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); } } diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index e124801d11..ffa7bc2ff3 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -23,8 +23,7 @@ final class DifferentialRevisionQuery const STATUS_ACCEPTED = 'status-accepted'; const STATUS_NEEDS_REVIEW = 'status-needs-review'; const STATUS_NEEDS_REVISION = 'status-needs-revision'; - const STATUS_CLOSED = 'status-closed'; // NOTE: Same as 'committed' - const STATUS_COMMITTED = 'status-committed'; // TODO: Remove. + const STATUS_CLOSED = 'status-closed'; const STATUS_ABANDONED = 'status-abandoned'; private $authors = array(); @@ -429,6 +428,13 @@ final class DifferentialRevisionQuery continue; } + if ($this->getViewer()->isOmnipotent()) { + // The viewer is omnipotent. Allow the revision to load even without + // a repository. + $revision->attachRepository(null); + continue; + } + // The revision has an associated repository, and the viewer can't see // it, and the viewer has no special capabilities. Filter out this // revision. @@ -765,11 +771,7 @@ final class DifferentialRevisionQuery $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', - array( - ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, - ArcanistDifferentialRevisionStatus::NEEDS_REVISION, - ArcanistDifferentialRevisionStatus::ACCEPTED, - )); + DifferentialRevisionStatus::getOpenStatuses()); break; case self::STATUS_NEEDS_REVIEW: $where[] = qsprintf( @@ -795,19 +797,11 @@ final class DifferentialRevisionQuery ArcanistDifferentialRevisionStatus::ACCEPTED, )); break; - case self::STATUS_COMMITTED: - phlog( - "WARNING: DifferentialRevisionQuery using deprecated ". - "STATUS_COMMITTED constant. This will be removed soon. ". - "Use STATUS_CLOSED."); - // fallthrough case self::STATUS_CLOSED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', - array( - ArcanistDifferentialRevisionStatus::CLOSED, - )); + DifferentialRevisionStatus::getClosedStatuses()); break; case self::STATUS_ABANDONED: $where[] = qsprintf( diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index b965b78fcd..bf2aeadded 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -250,9 +250,7 @@ abstract class DifferentialChangesetHTMLRenderer } } - return hsprintf( - '
%s
', - $message); + return phutil_tag_div('differential-meta-notice', $message); } protected function renderPropertyChangeHeader() { @@ -281,27 +279,21 @@ abstract class DifferentialChangesetHTMLRenderer $nval = phutil_escape_html_newlines($nval); } - $rows[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '', - $key, - $oval, - $nval); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag('th', array(), $key), + phutil_tag('td', array('class' => 'oval'), $oval), + phutil_tag('td', array('class' => 'nval'), $nval), + )); } } - array_unshift($rows, hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '', - pht('Property Changes'), - pht('Old Value'), - pht('New Value'))); + array_unshift( + $rows, + phutil_tag('tr', array('class' => 'property-table-header'), array( + phutil_tag('th', array(), pht('Property Changes')), + phutil_tag('td', array('class' => 'oval'), pht('Old Value')), + phutil_tag('td', array('class' => 'nval'), pht('New Value')), + ))); return phutil_tag( 'table', diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index 567116db05..25d8aeee58 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -27,19 +27,19 @@ final class DifferentialChangesetOneUpRenderer } else { $class = 'left'; } - $out[] = hsprintf('%s', $p['line']); - $out[] = hsprintf(''); - $out[] = hsprintf('%s', $class, $p['render']); + $out[] = phutil_tag('th', array(), $p['line']); + $out[] = phutil_tag('th', array()); + $out[] = phutil_tag('td', array('class' => $class), $p['render']); } else if ($type == 'new') { if ($p['htype']) { $class = 'right new'; - $out[] = hsprintf(''); + $out[] = phutil_tag('th', array()); } else { $class = 'right'; - $out[] = hsprintf('%s', $p['oline']); + $out[] = phutil_tag('th', array(), $p['oline']); } - $out[] = hsprintf('%s', $p['line']); - $out[] = hsprintf('%s', $class, $p['render']); + $out[] = phutil_tag('th', array(), $p['line']); + $out[] = phutil_tag('td', array('class' => $class), $p['render']); } $out[] = hsprintf(''); break; diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 378d4bdcc5..f44889ce5e 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -222,7 +222,7 @@ final class DifferentialChangesetTwoUpRenderer $cov_class = $coverage[$n_num - 1]; } $cov_class = 'cov-'.$cov_class; - $n_cov = hsprintf('', $cov_class); + $n_cov = phutil_tag('td', array('class' => "cov {$cov_class}")); $n_colspan--; } @@ -240,7 +240,7 @@ final class DifferentialChangesetTwoUpRenderer $n_classes = $n_class; if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) { - $n_copy = hsprintf('', $n_class); + $n_copy = phutil_tag('td', array('class' => "copy {$n_class}")); } else { list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num]; $title = ($orig_type == '-' ? 'Moved' : 'Copied').' from '; @@ -283,29 +283,25 @@ final class DifferentialChangesetTwoUpRenderer $n_id = null; } + // NOTE: This is a unicode zero-width space, which we use as a hint + // when intercepting 'copy' events to make sure sensible text ends + // up on the clipboard. See the 'phabricator-oncopy' behavior. + $zero_space = "\xE2\x80\x8B"; + // NOTE: The Javascript is sensitive to whitespace changes in this // block! - $html[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '%s'. - // NOTE: This is a unicode zero-width space, which we use as a hint - // when intercepting 'copy' events to make sure sensible text ends - // up on the clipboard. See the 'phabricator-oncopy' behavior. - ''. - "\xE2\x80\x8B%s". - ''. - '%s'. - '', + $html[] = phutil_tag('tr', array(), array( phutil_tag('th', array('id' => $o_id), $o_num), - $o_classes, $o_text, + phutil_tag('td', array('class' => $o_classes), $o_text), phutil_tag('th', array('id' => $n_id), $n_num), $n_copy, - $n_classes, $n_colspan, $n_text, - $n_cov); + phutil_tag( + 'td', + array('class' => $n_classes, 'colspan' => $n_colspan), + array($zero_space, $n_text)), + $n_cov, + )); if ($context_not_available && ($ii == $rows - 1)) { $html[] = $context_not_available; @@ -328,29 +324,27 @@ final class DifferentialChangesetTwoUpRenderer } } } - $html[] = hsprintf( - ''. - ''. - '%s'. - ''. - '%s'. - '', - $comment_html, - $new); + $html[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left'), $comment_html), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3, 'class' => 'right3'), $new), + )); } } if ($n_num && isset($new_comments[$n_num])) { foreach ($new_comments[$n_num] as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = true); - $html[] = hsprintf( - ''. - ''. - ''. - ''. - '%s'. - '', - $comment_html); + $html[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left')), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $comment_html), + )); } } } @@ -395,40 +389,39 @@ final class DifferentialChangesetTwoUpRenderer foreach ($this->getOldComments() as $on_line => $comment_group) { foreach ($comment_group as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = false); - $html_old[] = hsprintf( - ''. - ''. - '%s'. - ''. - ''. - '', - $comment_html); + $html_old[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left'), $comment_html), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3, 'class' => 'right3')), + )); } } foreach ($this->getNewComments() as $lin_line => $comment_group) { foreach ($comment_group as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = true); - $html_new[] = hsprintf( - ''. - ''. - ''. - ''. - '%s'. - '', - $comment_html); + $html_new[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left')), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $comment_html), + )); } } if (!$old) { - $th_old = hsprintf(''); + $th_old = phutil_tag('th', array()); } else { - $th_old = hsprintf('1', $vs); + $th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1); } if (!$new) { - $th_new = hsprintf(''); + $th_new = phutil_tag('th', array()); } else { - $th_new = hsprintf('1', $id); + $th_new = phutil_tag('th', array('id' => "C{$id}OL1"), 1); } $output = hsprintf( diff --git a/src/applications/differential/search/DifferentialSearchIndexer.php b/src/applications/differential/search/DifferentialSearchIndexer.php index 01de4dc2e9..a371b6e4e6 100644 --- a/src/applications/differential/search/DifferentialSearchIndexer.php +++ b/src/applications/differential/search/DifferentialSearchIndexer.php @@ -44,8 +44,7 @@ final class DifferentialSearchIndexer PhabricatorPeoplePHIDTypeUser::TYPECONST, $rev->getDateCreated()); - if ($rev->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED && - $rev->getStatus() != ArcanistDifferentialRevisionStatus::ABANDONED) { + if (!$rev->isClosed()) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_OPEN, $rev->getPHID(), diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 96bb8de94f..b5f66aea1e 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -2,10 +2,13 @@ final class DifferentialDiff extends DifferentialDAO - implements PhabricatorPolicyInterface { + implements + PhabricatorPolicyInterface, + HarbormasterBuildableInterface { protected $revisionID; protected $authorPHID; + protected $repositoryPHID; protected $sourceMachine; protected $sourcePath; @@ -22,7 +25,6 @@ final class DifferentialDiff protected $branch; protected $bookmark; - protected $parentRevisionID; protected $arcanistProjectPHID; protected $creationMethod; protected $repositoryUUID; @@ -221,7 +223,6 @@ final class DifferentialDiff public function getDiffDict() { $dict = array( 'id' => $this->getID(), - 'parent' => $this->getParentRevisionID(), 'revisionID' => $this->getRevisionID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), @@ -339,4 +340,24 @@ final class DifferentialDiff return null; } + + +/* -( HarbormasterBuildableInterface )------------------------------------- */ + + + public function getHarbormasterBuildablePHID() { + return $this->getPHID(); + } + + public function getHarbormasterContainerPHID() { + if ($this->getRevisionID()) { + $revision = id(new DifferentialRevision())->load($this->getRevisionID()); + if ($revision) { + return $revision->getPHID(); + } + } + + return null; + } + } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 350040e773..96e3e13b49 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -5,7 +5,8 @@ final class DifferentialRevision extends DifferentialDAO PhabricatorTokenReceiverInterface, PhabricatorPolicyInterface, PhabricatorFlaggableInterface, - PhrequentTrackableInterface { + PhrequentTrackableInterface, + HarbormasterBuildableInterface { protected $title = ''; protected $originalTitle; @@ -407,4 +408,21 @@ final class DifferentialRevision extends DifferentialDAO $this->repository = $repository; return $this; } + + public function isClosed() { + return DifferentialRevisionStatus::isClosedStatus($this->getStatus()); + } + + +/* -( HarbormasterBuildableInterface )------------------------------------- */ + + + public function getHarbormasterBuildablePHID() { + return $this->loadActiveDiff()->getPHID(); + } + + public function getHarbormasterContainerPHID() { + return $this->getPHID(); + } + } diff --git a/src/applications/differential/view/DifferentialAddCommentView.php b/src/applications/differential/view/DifferentialAddCommentView.php index ceba6a2197..a95ee9254f 100644 --- a/src/applications/differential/view/DifferentialAddCommentView.php +++ b/src/applications/differential/view/DifferentialAddCommentView.php @@ -48,7 +48,7 @@ final class DifferentialAddCommentView extends AphrontView { public function render() { - require_celerity_resource('differential-revision-add-comment-css'); + $this->requireResource('differential-revision-add-comment-css'); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); @@ -181,16 +181,17 @@ final class DifferentialAddCommentView extends AphrontView { $warn = phutil_tag('div', array('id' => 'warnings'), $warning_container); - $preview = hsprintf( - '
'. - '
'. - '%s'. - '
'. - '
'. - '
'. - '
', + $loading = phutil_tag( + 'span', + array('class' => 'aphront-panel-preview-loading-text'), pht('Loading comment preview...')); + $preview = phutil_tag_div( + 'aphront-panel-preview aphront-panel-flush', + array( + phutil_tag('div', array('id' => 'comment-preview'), $loading), + phutil_tag('div', array('id' => 'inline-comment-preview')), + )); $comment_box = id(new PHUIObjectBoxView()) diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php index 029bd0a2a0..0e9a1711f3 100644 --- a/src/applications/differential/view/DifferentialChangesetDetailView.php +++ b/src/applications/differential/view/DifferentialChangesetDetailView.php @@ -94,8 +94,8 @@ final class DifferentialChangesetDetailView extends AphrontView { } public function render() { - require_celerity_resource('differential-changeset-view-css'); - require_celerity_resource('syntax-highlighting-css'); + $this->requireResource('differential-changeset-view-css'); + $this->requireResource('syntax-highlighting-css'); Javelin::initBehavior('phabricator-oncopy', array()); diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index ca05bf97b0..069fd2bc4b 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -103,7 +103,7 @@ final class DifferentialChangesetListView extends AphrontView { } public function render() { - require_celerity_resource('differential-changeset-view-css'); + $this->requireResource('differential-changeset-view-css'); $changesets = $this->changesets; @@ -183,20 +183,20 @@ final class DifferentialChangesetListView extends AphrontView { $output[] = $detail->render(); } - require_celerity_resource('aphront-tooltip-css'); + $this->requireResource('aphront-tooltip-css'); - Javelin::initBehavior('differential-populate', array( + $this->initBehavior('differential-populate', array( 'registry' => $mapping, 'whitespace' => $this->whitespace, 'uri' => $this->renderURI, )); - Javelin::initBehavior('differential-show-more', array( + $this->initBehavior('differential-show-more', array( 'uri' => $this->renderURI, 'whitespace' => $this->whitespace, )); - Javelin::initBehavior('differential-comment-jump', array()); + $this->initBehavior('differential-comment-jump', array()); if ($this->inlineURI) { $undo_templates = $this->renderUndoTemplates(); @@ -246,19 +246,21 @@ final class DifferentialChangesetListView extends AphrontView { array('Changes discarded. ', $link)); return array( - 'l' => hsprintf( - ''. - ''. - ''. - '
%s
', - $div), + 'l' => phutil_tag('table', array(), + phutil_tag('tr', array(), array( + phutil_tag('th', array()), + phutil_tag('td', array(), $div), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3)), + ))), - 'r' => hsprintf( - ''. - ''. - ''. - '
%s
', - $div), + 'r' => phutil_tag('table', array(), + phutil_tag('tr', array(), array( + phutil_tag('th', array()), + phutil_tag('td', array()), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3), $div), + ))), ); } diff --git a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php index 1ba389839a..74391ef5f9 100644 --- a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php +++ b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php @@ -54,8 +54,8 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { public function render() { - require_celerity_resource('differential-core-view-css'); - require_celerity_resource('differential-table-of-contents-css'); + $this->requireResource('differential-core-view-css'); + $this->requireResource('differential-table-of-contents-css'); $rows = array(); @@ -136,7 +136,8 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { $pchar = ($changeset->getOldProperties() === $changeset->getNewProperties()) ? null - : hsprintf('M', pht('Properties Changed')); + : phutil_tag('span', array('title' => pht('Properties Changed')), 'M') + ; $fname = $changeset->getFilename(); $cov = $this->renderCoverage($coverage, $fname); @@ -152,28 +153,25 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { (isset($this->visibleChangesets[$id]) ? 'Loading...' : '?')); } - $rows[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '%s%s'. - '%s'. - '%s'. - '', - $chartitle, $char, - $pchar, - $desc, - $link, $lines, - $cov, - $mcov); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag( + 'td', + array('class' => 'differential-toc-char', 'title' => $chartitle), + $char), + phutil_tag('td', array('class' => 'differential-toc-prop'), $pchar), + phutil_tag('td', array('class' => 'differential-toc-ftype'), $desc), + phutil_tag( + 'td', + array('class' => 'differential-toc-file'), + array($link, $lines)), + phutil_tag('td', array('class' => 'differential-toc-cov'), $cov), + phutil_tag('td', array('class' => 'differential-toc-mcov'), $mcov), + )); if ($meta) { - $rows[] = hsprintf( - ''. - ''. - '%s'. - '', - $meta); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag('td', array('colspan' => 3)), + phutil_tag('td', array('class' => 'differential-toc-meta'), $meta), + )); } if ($this->diff && $this->repository) { $paths[] = @@ -208,10 +206,9 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { ), pht('Show All Context')); - $buttons = hsprintf( - '%s%s', - $editor_link, - $reveal_link); + $buttons = phutil_tag('tr', array(), + phutil_tag('td', array('colspan' => 7), + array($editor_link, $reveal_link))); $content = hsprintf( '%s'. diff --git a/src/applications/differential/view/DifferentialInlineCommentEditView.php b/src/applications/differential/view/DifferentialInlineCommentEditView.php index 672d945e4e..88c1b176ff 100644 --- a/src/applications/differential/view/DifferentialInlineCommentEditView.php +++ b/src/applications/differential/view/DifferentialInlineCommentEditView.php @@ -60,17 +60,21 @@ final class DifferentialInlineCommentEditView extends AphrontView { $this->renderBody(), )); - return hsprintf( - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '
%s%s
', - $this->onRight ? null : $content, - $this->onRight ? $content : null); + return phutil_tag('table', array(), phutil_tag( + 'tr', + array('class' => 'inline-comment-splint'), + array( + phutil_tag('th', array()), + phutil_tag( + 'td', + array('class' => 'left'), + $this->onRight ? null : $content), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $this->onRight ? $content : null), + ))); } private function renderInputs() { @@ -100,16 +104,6 @@ final class DifferentialInlineCommentEditView extends AphrontView { ), pht('Cancel')); - $formatting = phutil_tag( - 'a', - array( - 'href' => PhabricatorEnv::getDoclink( - 'article/Remarkup_Reference.html'), - 'tabindex' => '-1', - 'target' => '_blank', - ), - pht('Formatting Reference')); - $title = phutil_tag( 'div', array( @@ -130,7 +124,6 @@ final class DifferentialInlineCommentEditView extends AphrontView { 'class' => 'differential-inline-comment-edit-buttons', ), array( - $formatting, $buttons, phutil_tag('div', array('style' => 'clear: both'), ''), )); diff --git a/src/applications/differential/view/DifferentialInlineCommentView.php b/src/applications/differential/view/DifferentialInlineCommentView.php index f9ff40e2b7..0b491b64e0 100644 --- a/src/applications/differential/view/DifferentialInlineCommentView.php +++ b/src/applications/differential/view/DifferentialInlineCommentView.php @@ -217,6 +217,11 @@ final class DifferentialInlineCommentView extends AphrontView { $author = $handles[$inline->getAuthorPHID()]->getName(); } + $line = phutil_tag( + 'span', + array('class' => 'differential-inline-comment-line'), + $line); + $markup = javelin_tag( 'div', array( @@ -224,18 +229,19 @@ final class DifferentialInlineCommentView extends AphrontView { 'sigil' => $sigil, 'meta' => $metadata, ), - hsprintf( - '
'. - '%s%s %s %s'. - '
'. - '
'. - '
%s
'. - '
', - $anchor, - $links, - $line, - $author, - $content)); + array( + phutil_tag_div('differential-inline-comment-head', array( + $anchor, + $links, + ' ', + $line, + ' ', + $author, + )), + phutil_tag_div( + 'differential-inline-comment-content', + phutil_tag_div('phabricator-remarkup', $content)), + )); return $this->scaffoldMarkup($markup); } @@ -248,17 +254,16 @@ final class DifferentialInlineCommentView extends AphrontView { $left_markup = !$this->onRight ? $markup : ''; $right_markup = $this->onRight ? $markup : ''; - return hsprintf( - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '
%s%s
', - $left_markup, - $right_markup); + return phutil_tag('table', array(), + phutil_tag('tr', array(), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left'), $left_markup), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $right_markup), + ))); } } diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php index aec57bfe81..d4fbe45431 100644 --- a/src/applications/differential/view/DifferentialLocalCommitsView.php +++ b/src/applications/differential/view/DifferentialLocalCommitsView.php @@ -20,7 +20,7 @@ final class DifferentialLocalCommitsView extends AphrontView { return null; } - require_celerity_resource('differential-local-commits-view-css'); + $this->requireResource('differential-local-commits-view-css'); $has_tree = false; $has_local = false; @@ -128,12 +128,10 @@ final class DifferentialLocalCommitsView extends AphrontView { $headers = phutil_tag('tr', array(), $headers); - $content = hsprintf( - '
'. - '%s%s
'. - '
', - $headers, - phutil_implode_html("\n", $rows)); + $content = phutil_tag_div('differential-panel', phutil_tag( + 'table', + array('class' => 'differential-local-commits-table'), + array($headers, phutil_implode_html("\n", $rows)))); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Local Commits')) diff --git a/src/applications/differential/view/DifferentialResultsTableView.php b/src/applications/differential/view/DifferentialResultsTableView.php index f2be210728..226c68484c 100644 --- a/src/applications/differential/view/DifferentialResultsTableView.php +++ b/src/applications/differential/view/DifferentialResultsTableView.php @@ -97,10 +97,10 @@ final class DifferentialResultsTableView extends AphrontView { ), phutil_tag('th', array('colspan' => 2), $hide_more)); - Javelin::initBehavior('differential-show-field-details'); + $this->initBehavior('differential-show-field-details'); } - require_celerity_resource('differential-results-table-css'); + $this->requireResource('differential-results-table-css'); return javelin_tag( 'table', diff --git a/src/applications/differential/view/DifferentialRevisionCommentListView.php b/src/applications/differential/view/DifferentialRevisionCommentListView.php index 86d0c76bfa..b03cee03db 100644 --- a/src/applications/differential/view/DifferentialRevisionCommentListView.php +++ b/src/applications/differential/view/DifferentialRevisionCommentListView.php @@ -53,7 +53,7 @@ final class DifferentialRevisionCommentListView extends AphrontView { public function render() { - require_celerity_resource('differential-revision-comment-list-css'); + $this->requireResource('differential-revision-comment-list-css'); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($this->user); @@ -154,7 +154,7 @@ final class DifferentialRevisionCommentListView extends AphrontView { $visible = array_reverse($visible); if ($hidden) { - Javelin::initBehavior( + $this->initBehavior( 'differential-show-all-comments', array( 'markup' => implode("\n", $hidden), diff --git a/src/applications/differential/view/DifferentialRevisionCommentView.php b/src/applications/differential/view/DifferentialRevisionCommentView.php index c7499ddcca..293d93a4a6 100644 --- a/src/applications/differential/view/DifferentialRevisionCommentView.php +++ b/src/applications/differential/view/DifferentialRevisionCommentView.php @@ -67,8 +67,8 @@ final class DifferentialRevisionCommentView extends AphrontView { throw new Exception("Call setUser() before rendering!"); } - require_celerity_resource('phabricator-remarkup-css'); - require_celerity_resource('differential-revision-comment-css'); + $this->requireResource('phabricator-remarkup-css'); + $this->requireResource('differential-revision-comment-css'); $comment = $this->comment; @@ -87,9 +87,7 @@ final class DifferentialRevisionCommentView extends AphrontView { $comment, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); - $content = hsprintf( - '
%s
', - $content); + $content = phutil_tag_div('phabricator-remarkup', $content); } $inline_render = $this->renderInlineComments(); @@ -207,10 +205,9 @@ final class DifferentialRevisionCommentView extends AphrontView { } if (!$hide_comments) { - $xaction_view->appendChild(hsprintf( - '
%s%s
', - $content, - $inline_render)); + $xaction_view->appendChild(phutil_tag_div( + 'differential-comment-core', + array($content, $inline_render))); } return $xaction_view->render(); diff --git a/src/applications/differential/view/DifferentialRevisionDetailView.php b/src/applications/differential/view/DifferentialRevisionDetailView.php index fad95b5ada..445bdcc177 100644 --- a/src/applications/differential/view/DifferentialRevisionDetailView.php +++ b/src/applications/differential/view/DifferentialRevisionDetailView.php @@ -45,7 +45,7 @@ final class DifferentialRevisionDetailView extends AphrontView { public function render() { - require_celerity_resource('differential-core-view-css'); + $this->requireResource('differential-core-view-css'); $revision = $this->revision; $user = $this->getUser(); @@ -169,8 +169,8 @@ final class DifferentialRevisionDetailView extends AphrontView { $status_name = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status); - return id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) + return id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) ->setName($status_name); } diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index bc70460b26..0addfa5b62 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -102,8 +102,8 @@ final class DifferentialRevisionListView extends AphrontView { -$stale); } - Javelin::initBehavior('phabricator-tooltips', array()); - require_celerity_resource('aphront-tooltip-css'); + $this->initBehavior('phabricator-tooltips', array()); + $this->requireResource('aphront-tooltip-css'); $flagged = mpull($this->flags, null, 'getObjectPHID'); @@ -114,11 +114,6 @@ final class DifferentialRevisionListView extends AphrontView { $list = new PHUIObjectItemListView(); $list->setCards(true); - $do_not_display_age = array( - ArcanistDifferentialRevisionStatus::CLOSED => true, - ArcanistDifferentialRevisionStatus::ABANDONED => true, - ); - foreach ($this->revisions as $revision) { $item = id(new PHUIObjectItemView()) ->setUser($user); @@ -146,8 +141,7 @@ final class DifferentialRevisionListView extends AphrontView { $status = $revision->getStatus(); $show_age = ($fresh || $stale) && $this->highlightAge && - empty($do_not_display_age[$status]); - + !$revision->isClosed(); $object_age = PHUIObjectItemView::AGE_FRESH; foreach ($this->fields as $field) { diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php index 4dde5a4781..e5cf2a6338 100644 --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -30,8 +30,8 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { public function render() { - require_celerity_resource('differential-core-view-css'); - require_celerity_resource('differential-revision-history-css'); + $this->requireResource('differential-core-view-css'); + $this->requireResource('differential-revision-history-css'); $data = array( array( @@ -208,23 +208,25 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { $use_lint ? phutil_tag('th', array(), pht('Unit')) : '', ))); - $content = hsprintf( - '
'. - '
'. - ''. - '%s'. - ''. - ''. - ''. - '
'. - ''. - ''. - '
'. - '
'. - '
', - phutil_implode_html("\n", $rows), - pht('Whitespace Changes: %s', $select), - pht('Show Diff')); + $label = pht('Whitespace Changes: %s', $select); + + $content = phutil_tag_div( + 'differential-revision-history differential-panel', + phutil_tag( + 'form', + array('action' => '#toc'), + phutil_tag( + 'table', + array('class' => 'differential-revision-history-table'), array( + phutil_implode_html("\n", $rows), + phutil_tag('tr', array(), phutil_tag( + 'td', + array('colspan' => 9, 'class' => 'diff-differ-submit'), + array( + phutil_tag('label', array(), $label), + phutil_tag('button', array(), pht('Show Diff')), + ))) + )))); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Update History')) diff --git a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php index ea809aa4dc..ecb0b6fc28 100644 --- a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php +++ b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php @@ -32,7 +32,8 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication { public function getRemarkupRules() { return array( - new DiffusionRemarkupRule(), + new DiffusionRepositoryRemarkupRule(), + new DiffusionCommitRemarkupRule(), ); } @@ -46,6 +47,9 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication { 'new/' => 'DiffusionRepositoryNewController', '(?Pcreate)/' => 'DiffusionRepositoryCreateController', '(?Pimport)/' => 'DiffusionRepositoryCreateController', + 'pushlog/(?:query/(?P[^/]+)/)?' + => 'DiffusionPushLogListController', + '(?P[A-Z]+)/' => array( '' => 'DiffusionRepositoryController', @@ -58,7 +62,6 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication { 'tags/(?P.*)' => 'DiffusionTagListController', 'branches/(?P.*)' => 'DiffusionBranchTableController', 'lint/(?P.*)' => 'DiffusionLintController', - 'commit/(?P[a-z0-9]+)/branches/' => 'DiffusionCommitBranchesController', 'commit/(?P[a-z0-9]+)/tags/' @@ -70,16 +73,21 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication { 'basic/' => 'DiffusionRepositoryEditBasicController', 'encoding/' => 'DiffusionRepositoryEditEncodingController', 'activate/' => 'DiffusionRepositoryEditActivateController', - 'policy/' => 'DiffusionRepositoryEditPolicyController', + 'dangerous/' => 'DiffusionRepositoryEditDangerousController', 'branches/' => 'DiffusionRepositoryEditBranchesController', 'subversion/' => 'DiffusionRepositoryEditSubversionController', 'actions/' => 'DiffusionRepositoryEditActionsController', '(?Premote)/' => 'DiffusionRepositoryCreateController', + '(?Ppolicy)/' => 'DiffusionRepositoryCreateController', 'local/' => 'DiffusionRepositoryEditLocalController', 'delete/' => 'DiffusionRepositoryEditDeleteController', 'hosting/' => 'DiffusionRepositoryEditHostingController', '(?Pserve)/' => 'DiffusionRepositoryEditHostingController', ), + 'mirror/' => array( + 'edit/(?:(?P\d+)/)?' => 'DiffusionMirrorEditController', + 'delete/(?P\d+)/' => 'DiffusionMirrorDeleteController', + ), ), // NOTE: This must come after the rule above; it just gives us a diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_branchquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_branchquery_Method.php index 92f3b14165..22f1bb1e4c 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_branchquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_branchquery_Method.php @@ -1,82 +1,135 @@ '; } protected function defineCustomParamTypes() { return array( 'limit' => 'optional int', - 'offset' => 'optional int' + 'offset' => 'optional int', + 'contains' => 'optional string', ); } protected function getGitResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); - $limit = $request->getValue('limit'); - $offset = $request->getValue('offset'); - $refs = id(new DiffusionLowLevelGitRefQuery()) - ->setRepository($repository) - ->withIsOriginBranch(true) - ->execute(); - - $branches = array(); - foreach ($refs as $ref) { - $branch = id(new DiffusionBranchInformation()) - ->setName($ref->getShortName()) - ->setHeadCommitIdentifier($ref->getCommitIdentifier()); - - if (!$repository->shouldTrackBranch($branch->getName())) { - continue; + $contains = $request->getValue('contains'); + if (strlen($contains)) { + // NOTE: We can't use DiffusionLowLevelGitRefQuery here because + // `git for-each-ref` does not support `--contains`. + if ($repository->isWorkingCopyBare()) { + list($stdout) = $repository->execxLocalCommand( + 'branch --verbose --no-abbrev --contains %s --', + $contains); + $ref_map = DiffusionGitBranch::parseLocalBranchOutput( + $stdout); + } else { + list($stdout) = $repository->execxLocalCommand( + 'branch -r --verbose --no-abbrev --contains %s --', + $contains); + $ref_map = DiffusionGitBranch::parseRemoteBranchOutput( + $stdout, + DiffusionGitBranch::DEFAULT_GIT_REMOTE); } - $branches[] = $branch->toDictionary(); + $refs = array(); + foreach ($ref_map as $ref => $commit) { + $refs[] = id(new DiffusionRepositoryRef()) + ->setShortName($ref) + ->setCommitIdentifier($commit); + } + } else { + $refs = id(new DiffusionLowLevelGitRefQuery()) + ->setRepository($repository) + ->withIsOriginBranch(true) + ->execute(); + } + + return $this->processBranchRefs($request, $refs); + } + + protected function getMercurialResult(ConduitAPIRequest $request) { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + $refs = id(new DiffusionLowLevelMercurialBranchesQuery()) + ->setRepository($repository) + ->execute(); + + // If we have a 'contains' query, filter these branches down to just the + // ones which contain the commit. + $contains = $request->getValue('contains'); + if (strlen($contains)) { + list($branches_raw) = $repository->execxLocalCommand( + 'log --template %s --limit 1 --rev %s --', + '{branches}', + hgsprintf('%s', $contains)); + + $branches_raw = trim($branches_raw); + if (!strlen($branches_raw)) { + $containing_branches = array('default'); + } else { + $containing_branches = explode(' ', $branches_raw); + } + + $containing_branches = array_fuse($containing_branches); + + // NOTE: We get this very slightly wrong: a branch may have multiple + // heads and we'll keep all of the heads of the branch, even if the + // commit is only on some of the heads. This should be rare, is probably + // more clear to users as is, and would potentially be expensive to get + // right since we'd have to do additional checks. + + foreach ($refs as $key => $ref) { + if (empty($containing_branches[$ref->getShortName()])) { + unset($refs[$key]); + } + } + } + + return $this->processBranchRefs($request, $refs); + } + + protected function getSVNResult(ConduitAPIRequest $request) { + // Since SVN doesn't have meaningful branches, just return nothing for all + // queries. + return array(); + } + + private function processBranchRefs(ConduitAPIRequest $request, array $refs) { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + $offset = $request->getValue('offset'); + $limit = $request->getValue('limit'); + + foreach ($refs as $key => $ref) { + if (!$repository->shouldTrackBranch($ref->getShortName())) { + unset($refs[$key]); + } } // NOTE: We can't apply the offset or limit until here, because we may have // filtered untrackable branches out of the result set. if ($offset) { - $branches = array_slice($branches, $offset); + $refs = array_slice($refs, $offset); } if ($limit) { - $branches = array_slice($branches, 0, $limit); + $refs = array_slice($refs, 0, $limit); } - return $branches; + return mpull($refs, 'toDictionary'); } - protected function getMercurialResult(ConduitAPIRequest $request) { - $drequest = $this->getDiffusionRequest(); - $repository = $drequest->getRepository(); - $offset = $request->getValue('offset'); - $limit = $request->getValue('limit'); - - $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) - ->setRepository($repository) - ->execute(); - - if ($offset) { - $branches = array_slice($branches, $offset); - } - - if ($limit) { - $branches = array_slice($branches, 0, $limit); - } - - return mpull($branches, 'toDictionary'); - } } diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_browsequery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_browsequery_Method.php index b000584cfe..7c696df1b4 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_browsequery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_browsequery_Method.php @@ -100,10 +100,22 @@ final class ConduitAPI_diffusion_browsequery_Method $results = array(); foreach (explode("\0", rtrim($stdout)) as $line) { - // NOTE: Limit to 5 components so we parse filenames with spaces in them // correctly. - list($mode, $type, $hash, $size, $name) = preg_split('/\s+/', $line, 5); + // NOTE: The output uses a mixture of tabs and one-or-more spaces to + // delimit fields. + $parts = preg_split('/\s+/', $line, 5); + if (count($parts) < 5) { + throw new Exception( + pht( + 'Expected " \t", for ls-tree of '. + '"%s:%s", got: %s', + $commit, + $path, + $line)); + } + + list($mode, $type, $hash, $size, $name) = $parts; $path_result = new DiffusionRepositoryPath(); diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitbranchesquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitbranchesquery_Method.php deleted file mode 100644 index a8e9000787..0000000000 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitbranchesquery_Method.php +++ /dev/null @@ -1,61 +0,0 @@ - 'required string', - ); - } - - protected function getGitResult(ConduitAPIRequest $request) { - $drequest = $this->getDiffusionRequest(); - $repository = $drequest->getRepository(); - $commit = $request->getValue('commit'); - - // NOTE: We can't use DiffusionLowLevelGitRefQuery here because - // `git for-each-ref` does not support `--contains`. - if ($repository->isWorkingCopyBare()) { - list($contains) = $repository->execxLocalCommand( - 'branch --verbose --no-abbrev --contains %s', - $commit); - return DiffusionGitBranch::parseLocalBranchOutput( - $contains); - } else { - list($contains) = $repository->execxLocalCommand( - 'branch -r --verbose --no-abbrev --contains %s', - $commit); - return DiffusionGitBranch::parseRemoteBranchOutput( - $contains, - DiffusionBranchInformation::DEFAULT_GIT_REMOTE); - } - } - - protected function getMercurialResult(ConduitAPIRequest $request) { - $drequest = $this->getDiffusionRequest(); - $repository = $drequest->getRepository(); - $commit = $request->getValue('commit'); - - list($contains) = $repository->execxLocalCommand( - 'log --template %s --limit 1 --rev %s --', - '{branch}', - $commit); - - return array( - trim($contains) => $commit, - ); - - } -} diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitparentsquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitparentsquery_Method.php index f0315e46f2..82ff91a0ce 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitparentsquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_commitparentsquery_Method.php @@ -7,12 +7,12 @@ final class ConduitAPI_diffusion_commitparentsquery_Method extends ConduitAPI_diffusion_abstractquery_Method { public function getMethodDescription() { - return - 'Commit parent(s) information for a commit in a repository.'; + return pht( + "Get the commit identifiers for a commit's parent or parents."); } public function defineReturnType() { - return 'array'; + return 'list'; } protected function defineCustomParamTypes() { @@ -22,10 +22,12 @@ final class ConduitAPI_diffusion_commitparentsquery_Method } protected function getResult(ConduitAPIRequest $request) { - $drequest = $this->getDiffusionRequest(); + $repository = $this->getRepository($request); - $query = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest); - $parents = $query->loadParents(); - return $parents; + return id(new DiffusionLowLevelParentsQuery()) + ->setRepository($repository) + ->withIdentifier($request->getValue('commit')) + ->execute(); } + } diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_createcomment_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_createcomment_Method.php index 692246497f..e4020a0d06 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_createcomment_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_createcomment_Method.php @@ -21,6 +21,7 @@ final class ConduitAPI_diffusion_createcomment_Method 'phid' => 'required string', 'action' => 'optional string', 'message' => 'required string', + 'silent' => 'optional bool', ); } @@ -73,6 +74,7 @@ final class ConduitAPI_diffusion_createcomment_Method id(new PhabricatorAuditCommentEditor($commit)) ->setActor($request->getUser()) + ->setNoEmail($request->getValue('silent')) ->addComment($comment); return true; diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_diffquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_diffquery_Method.php index 055e2adf75..35cb59854e 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_diffquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_diffquery_Method.php @@ -193,10 +193,8 @@ final class ConduitAPI_diffusion_diffquery_Method list($ref, $rev) = $spec; return $repository->getRemoteCommandFuture( - 'cat %s%s@%d', - $repository->getRemoteURI(), - phutil_escape_uri($ref), - $rev); + 'cat %s', + $repository->getSubversionPathURI($ref, $rev)); } private function getGitOrMercurialResult(ConduitAPIRequest $request) { diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php index 173699fa80..1ccc6a50a6 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_getcommits_Method.php @@ -7,7 +7,15 @@ final class ConduitAPI_diffusion_getcommits_Method extends ConduitAPI_diffusion_Method { public function getMethodDescription() { - return "Retrieve Diffusion commit information."; + return pht('Retrieve Diffusion commit information.'); + } + + public function getMethodStatus() { + return self::METHOD_STATUS_DEPRECATED; + } + + public function getMethodStatusDescription() { + return pht('Obsoleted by diffusion.querycommits.'); } public function defineParamTypes() { @@ -129,6 +137,7 @@ final class ConduitAPI_diffusion_getcommits_Method $commits = $this->addRepositoryCommitDataInformation($commits); $commits = $this->addDifferentialInformation($commits); + $commits = $this->addManiphestInformation($commits); foreach ($commits as $name => $commit) { $results[$name] = $commit; @@ -261,4 +270,31 @@ final class ConduitAPI_diffusion_getcommits_Method return $commits; } + /** + * Enhances the commits list with Maniphest information. + */ + private function addManiphestInformation(array $commits) { + $task_type = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK; + + $commit_phids = ipull($commits, 'commitPHID'); + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($commit_phids) + ->withEdgeTypes(array($task_type)); + + $edges = $edge_query->execute(); + + foreach ($commits as $name => $commit) { + $task_phids = $edge_query->getDestinationPHIDs( + array($commit['commitPHID']), + array($task_type)); + + $commits[$name] += array( + 'taskPHIDs' => $task_phids, + ); + } + + return $commits; + } + } diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_historyquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_historyquery_Method.php index d6c6aab39e..8cf211509b 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_historyquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_historyquery_Method.php @@ -125,9 +125,10 @@ extends ConduitAPI_diffusion_abstractquery_Method { '{node};{parents}\\n', ($offset + $limit), // No '--skip' in Mercurial. $branch_arg, - hgsprintf('reverse(%s::%s)', '0', $commit_hash), + hgsprintf('reverse(ancestors(%s))', $commit_hash), $path_arg); + $stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); $lines = explode("\n", trim($stdout)); $lines = array_slice($lines, $offset); diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_lastmodifiedquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_lastmodifiedquery_Method.php index 26e679aec3..8cfa1f6f55 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_lastmodifiedquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_lastmodifiedquery_Method.php @@ -87,7 +87,7 @@ final class ConduitAPI_diffusion_lastmodifiedquery_Method list($hash) = $repository->execxLocalCommand( 'log --template %s --limit 1 --removed --rev %s -- %s', '{node}', - hgsprintf('reverse(%s::%s)', '0', $drequest->getCommit()), + hgsprintf('reverse(ancestors(%s))', $drequest->getCommit()), nonempty(ltrim($path, '/'), '.')); return $this->loadDataFromHash($hash); diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_querycommits_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_querycommits_Method.php new file mode 100644 index 0000000000..d4df1673ef --- /dev/null +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_querycommits_Method.php @@ -0,0 +1,83 @@ +'; + } + + public function defineParamTypes() { + return array( + 'ids' => 'optional list', + 'phids' => 'optional list', + 'names' => 'optional list', + 'repositoryPHID' => 'optional phid', + ) + $this->getPagerParamTypes(); + } + + public function defineErrorTypes() { + return array(); + } + + protected function execute(ConduitAPIRequest $request) { + $query = id(new DiffusionCommitQuery()) + ->setViewer($request->getUser()); + + $repository_phid = $request->getValue('repositoryPHID'); + if ($repository_phid) { + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($request->getUser()) + ->withPHIDs(array($repository_phid)) + ->executeOne(); + if ($repository) { + $query->withRepository($repository); + } + } + + $names = $request->getValue('names'); + if ($names) { + $query->withIdentifiers($names); + } + + $ids = $request->getValue('ids'); + if ($ids) { + $query->withIDs($ids); + } + + $phids = $request->getValue('phids'); + if ($phids) { + $query->withPHIDs($phids); + } + + $pager = $this->newPager($request); + $commits = $query->executeWithCursorPager($pager); + + $map = $query->getIdentifierMap(); + $map = mpull($map, 'getPHID'); + + $data = array(); + foreach ($commits as $commit) { + $data[$commit->getPHID()] = array( + 'id' => $commit->getID(), + 'phid' => $commit->getPHID(), + 'repositoryPHID' => $commit->getRepository()->getPHID(), + 'identifier' => $commit->getCommitIdentifier(), + 'epoch' => $commit->getEpoch(), + 'isImporting' => !$commit->isImported(), + ); + } + + $result = array( + 'data' => $data, + 'identifierMap' => nonempty($map, (object)array()), + ); + + return $this->addPagerResults($result, $pager); + } + +} diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php new file mode 100644 index 0000000000..43169aea5c --- /dev/null +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php @@ -0,0 +1,87 @@ +'; + } + + protected function defineCustomParamTypes() { + return array( + 'path' => 'required string', + 'commit' => 'required string', + 'pattern' => 'required string', + 'limit' => 'optional int', + 'offset' => 'optional int', + ); + } + + protected function getResult(ConduitAPIRequest $request) { + $results = parent::getResult($request); + $offset = $request->getValue('offset'); + return array_slice($results, $offset); + } + + protected function getGitResult(ConduitAPIRequest $request) { + $drequest = $this->getDiffusionRequest(); + $path = $drequest->getPath(); + $commit = $request->getValue('commit'); + $repository = $drequest->getRepository(); + + // http://comments.gmane.org/gmane.comp.version-control.git/197735 + + $future = $repository->getLocalCommandFuture( + 'ls-tree --name-only -r -z %s -- %s', + $commit, + $path); + + $lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0"); + return $this->filterResults($lines, $request); + } + + protected function getMercurialResult(ConduitAPIRequest $request) { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + $path = $request->getValue('path'); + $commit = $request->getValue('commit'); + + // Adapted from diffusion.browsequery. + list($entire_manifest) = $repository->execxLocalCommand( + 'manifest --rev %s', + hgsprintf('%s', $commit)); + $entire_manifest = explode("\n", $entire_manifest); + + $match_against = trim($path, '/'); + $match_len = strlen($match_against); + + $lines = array(); + foreach ($entire_manifest as $path) { + if (strlen($path) && !strncmp($path, $match_against, $match_len)) { + $lines[] = $path; + } + } + return $this->filterResults($lines, $request); + } + + protected function filterResults($lines, ConduitAPIRequest $request) { + $pattern = $request->getValue('pattern'); + $limit = $request->getValue('limit'); + $offset = $request->getValue('offset'); + + $results = array(); + foreach ($lines as $line) { + if (preg_match('#'.str_replace('#', '\#', $pattern).'#', $line)) { + $results[] = $line; + if (count($results) >= $offset + $limit) { + break; + } + } + } + return $results; + } +} diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_rawdiffquery_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_rawdiffquery_Method.php index 63701277be..ffced64cac 100644 --- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_rawdiffquery_Method.php +++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_rawdiffquery_Method.php @@ -21,6 +21,7 @@ final class ConduitAPI_diffusion_rawdiffquery_Method 'commit' => 'required string', 'path' => 'optional string', 'timeout' => 'optional int', + 'byteLimit' => 'optional int', 'linesOfContext' => 'optional int', 'againstCommit' => 'optional string', ); @@ -28,20 +29,29 @@ final class ConduitAPI_diffusion_rawdiffquery_Method protected function getResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); - $timeout = $request->getValue('timeout'); - $lines_of_context = $request->getValue('linesOfContext'); - $against_commit = $request->getValue('againstCommit'); $raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest); + + $timeout = $request->getValue('timeout'); if ($timeout !== null) { $raw_query->setTimeout($timeout); } + + $lines_of_context = $request->getValue('linesOfContext'); if ($lines_of_context !== null) { $raw_query->setLinesOfContext($lines_of_context); } + + $against_commit = $request->getValue('againstCommit'); if ($against_commit !== null) { $raw_query->setAgainstCommit($against_commit); } + + $byte_limit = $request->getValue('byteLimit'); + if ($byte_limit !== null) { + $raw_query->setByteLimit($byte_limit); + } + return $raw_query->loadRawDiff(); } } diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php index 477836fdb6..40821e3f89 100644 --- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php +++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php @@ -109,6 +109,13 @@ final class PhabricatorDiffusionConfigOptions 'When constructing clone URIs to show to users, Diffusion will '. 'fill in this login username. If you have configured a VCS user '. 'like `git`, you should provide it here.')), + $this->newOption('diffusion.ssh-port', 'int', null) + ->setSummary(pht('Port for SSH connections to repositories.')) + ->setDescription( + pht( + 'When constructing clone URIs to show to users, Diffusion by '. + 'default will not display a port assuming the default for your '. + 'VCS. Explicitly declare when running on a non-standard port.')), ); } diff --git a/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php index 622e7080e2..d6507e6746 100644 --- a/src/applications/diffusion/controller/DiffusionBranchTableController.php +++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php @@ -18,15 +18,16 @@ final class DiffusionBranchTableController extends DiffusionController { $pager->setOffset($request->getInt('offset')); // TODO: Add support for branches that contain commit - $branches = DiffusionBranchInformation::newFromConduit( - $this->callConduitWithDiffusionRequest( - 'diffusion.branchquery', - array( - 'offset' => $pager->getOffset(), - 'limit' => $pager->getPageSize() + 1 - ))); + $branches = $this->callConduitWithDiffusionRequest( + 'diffusion.branchquery', + array( + 'offset' => $pager->getOffset(), + 'limit' => $pager->getPageSize() + 1 + )); $branches = $pager->sliceResults($branches); + $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); + $content = null; if (!$branches) { $content = $this->renderStatusMessage( @@ -35,8 +36,8 @@ final class DiffusionBranchTableController extends DiffusionController { } else { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) - ->withIdentifiers(mpull($branches, 'getHeadCommitIdentifier')) - ->withRepositoryIDs(array($repository->getID())) + ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) + ->withRepository($repository) ->execute(); $view = id(new DiffusionBranchTableView()) diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index b75e465776..73e0e73ef4 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -8,37 +8,45 @@ abstract class DiffusionBrowseController extends DiffusionController { protected function renderSearchForm($collapsed) { $drequest = $this->getDiffusionRequest(); + + $forms = array(); $form = id(new AphrontFormView()) ->setUser($this->getRequest()->getUser()) ->setMethod('GET'); switch ($drequest->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $form->appendChild(pht('Search is not available in Subversion.')); + $forms[] = id(clone $form) + ->appendChild(pht('Search is not available in Subversion.')); break; - default: - $form + $forms[] = id(clone $form) ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Search Here')) + id(new AphrontFormTextWithSubmitControl()) + ->setLabel(pht('File Name')) + ->setSubmitLabel(pht('Search File Names')) + ->setName('find') + ->setValue($this->getRequest()->getStr('find'))); + $forms[] = id(clone $form) + ->appendChild( + id(new AphrontFormTextWithSubmitControl()) + ->setLabel(pht('Pattern')) + ->setSubmitLabel(pht('Grep File Content')) ->setName('grep') - ->setValue($this->getRequest()->getStr('grep')) - ->setCaption(pht('Enter a regular expression.'))) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Search File Content'))); + ->setValue($this->getRequest()->getStr('grep'))); break; } + $filter = new AphrontListFilterView(); - $filter->appendChild($form); + $filter->appendChild($forms); + if ($collapsed) { $filter->setCollapsed( pht('Show Search'), pht('Hide Search'), - pht('Search for file content in this directory.'), + pht('Search for file names or content in this directory.'), '#'); } diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php index 74c645cf58..e8a3b9e832 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php @@ -555,7 +555,7 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { if ($commits) { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) - ->withRepositoryIDs(array($drequest->getRepository()->getID())) + ->withRepository($drequest->getRepository()) ->withIdentifiers($commits) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); @@ -789,7 +789,7 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { "\xE2\x80\x8B", // TODO: [HTML] Not ideal. - phutil_safe_html(str_replace("\t", ' ', $line['data'])), + phutil_safe_html(str_replace("\t", ' ', $line['data'])), )); $rows[] = phutil_tag( @@ -825,12 +825,21 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { } private function loadFileForData($path, $data) { - return PhabricatorFile::buildFromFileDataOrHash( + $file = PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => basename($path), 'ttl' => time() + 60 * 60 * 24, + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file->attachToObject( + $this->getRequest()->getUser(), + $this->getDiffusionRequest()->getRepository()->getPHID()); + unset($unguarded); + + return $file; } private function buildRawResponse($path, $data) { @@ -872,17 +881,16 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. - $parent = $this->loadParentRevisionOf($before); + $parent = $this->loadParentCommitOf($before); $old_filename = null; $was_created = false; if ($parent) { - $grandparent = $this->loadParentRevisionOf( - $parent->getCommitIdentifier()); + $grandparent = $this->loadParentCommitOf($parent); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); - $rename_query->setOldCommit($grandparent->getCommitIdentifier()); + $rename_query->setOldCommit($grandparent); $rename_query->setViewer($request->getUser()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); @@ -970,7 +978,7 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { return $line; } - private function loadParentRevisionOf($commit) { + private function loadParentCommitOf($commit) { $drequest = $this->getDiffusionRequest(); $user = $this->getRequest()->getUser(); @@ -978,7 +986,7 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { array( 'user' => $user, 'repository' => $drequest->getRepository(), - 'commit' => $commit, + 'commit' => $commit, )); $parents = DiffusionQuery::callConduitWithDiffusionRequest( @@ -986,7 +994,8 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { $before_req, 'diffusion.commitparentsquery', array( - 'commit' => $commit)); + 'commit' => $commit, + )); return head($parents); } diff --git a/src/applications/diffusion/controller/DiffusionBrowseMainController.php b/src/applications/diffusion/controller/DiffusionBrowseMainController.php index 0eb0e08330..43d12a7b2f 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseMainController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseMainController.php @@ -9,8 +9,9 @@ final class DiffusionBrowseMainController extends DiffusionBrowseController { // Figure out if we're browsing a directory, a file, or a search result // list. Then delegate to the appropriate controller. - $search = $request->getStr('grep'); - if (strlen($search)) { + $grep = $request->getStr('grep'); + $find = $request->getStr('find'); + if (strlen($grep) || strlen($find)) { $controller = new DiffusionBrowseSearchController($request); } else { $results = DiffusionBrowseResultSet::newFromConduit( diff --git a/src/applications/diffusion/controller/DiffusionBrowseSearchController.php b/src/applications/diffusion/controller/DiffusionBrowseSearchController.php index 6c770f1a0e..0a79f709ed 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseSearchController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseSearchController.php @@ -43,7 +43,6 @@ final class DiffusionBrowseSearchController extends DiffusionBrowseController { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $results = array(); - $no_data = pht('No results found.'); $limit = 100; $page = $this->getRequest()->getInt('page', 0); @@ -52,17 +51,34 @@ final class DiffusionBrowseSearchController extends DiffusionBrowseController { $pager->setOffset($page); $pager->setURI($this->getRequest()->getRequestURI(), 'page'); + $search_mode = null; + try { - - $results = $this->callConduitWithDiffusionRequest( - 'diffusion.searchquery', - array( - 'grep' => $this->getRequest()->getStr('grep'), - 'stableCommitName' => $drequest->getStableCommitName(), - 'path' => $drequest->getPath(), - 'limit' => $limit + 1, - 'offset' => $page)); - + if (strlen($this->getRequest()->getStr('grep'))) { + $search_mode = 'grep'; + $query_string = $this->getRequest()->getStr('grep'); + $results = $this->callConduitWithDiffusionRequest( + 'diffusion.searchquery', + array( + 'grep' => $query_string, + 'stableCommitName' => $drequest->getStableCommitName(), + 'path' => $drequest->getPath(), + 'limit' => $limit + 1, + 'offset' => $page, + )); + } else { // Filename search. + $search_mode = 'find'; + $query_string = $this->getRequest()->getStr('find'); + $results = $this->callConduitWithDiffusionRequest( + 'diffusion.querypaths', + array( + 'pattern' => $query_string, + 'commit' => $drequest->getStableCommitName(), + 'path' => $drequest->getPath(), + 'limit' => $limit + 1, + 'offset' => $page, + )); + } } catch (ConduitException $ex) { $err = $ex->getErrorDescription(); if ($err != '') { @@ -74,6 +90,34 @@ final class DiffusionBrowseSearchController extends DiffusionBrowseController { $results = $pager->sliceResults($results); + if ($search_mode == 'grep') { + $table = $this->renderGrepResults($results); + $header = pht( + 'File content matching "%s" under "%s"', + $query_string, + nonempty($drequest->getPath(), '/')); + } else { + $table = $this->renderFindResults($results); + $header = pht( + 'Paths matching "%s" under "%s"', + $query_string, + nonempty($drequest->getPath(), '/')); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->appendChild($table); + + $pager_box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($pager); + + return array($box, $pager_box); + } + + private function renderGrepResults(array $results) { + $drequest = $this->getDiffusionRequest(); + require_celerity_resource('syntax-highlighting-css'); // NOTE: This can be wrong because we may find the string inside the @@ -126,12 +170,40 @@ final class DiffusionBrowseSearchController extends DiffusionBrowseController { ->setClassName('remarkup-code') ->setHeaders(array(pht('Path'), pht('Line'), pht('String'))) ->setColumnClasses(array('', 'n', 'wide')) - ->setNoDataString($no_data); + ->setNoDataString( + pht( + 'The pattern you searched for was not found in the content of any '. + 'files.')); - return id(new AphrontPanelView()) - ->setNoBackground(true) - ->appendChild($table) - ->appendChild($pager); + return $table; + } + + private function renderFindResults(array $results) { + $drequest = $this->getDiffusionRequest(); + + $rows = array(); + foreach ($results as $result) { + $href = $drequest->generateURI(array( + 'action' => 'browse', + 'path' => $result, + )); + + $readable = Filesystem::readablePath($result, $drequest->getPath()); + + $rows[] = array( + phutil_tag('a', array('href' => $href), $readable), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders(array(pht('Path'))) + ->setColumnClasses(array('wide')) + ->setNoDataString( + pht( + 'The pattern you searched for did not match the names of any '. + 'files.')); + + return $table; } } diff --git a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php index 069a353a8f..df60c0563d 100644 --- a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php +++ b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php @@ -17,26 +17,30 @@ final class DiffusionCommitBranchesController extends DiffusionController { $branches = array(); try { $branches = $this->callConduitWithDiffusionRequest( - 'diffusion.commitbranchesquery', - array('commit' => $request->getCommit())); + 'diffusion.branchquery', + array( + 'contains' => $request->getCommit(), + )); } catch (ConduitException $ex) { if ($ex->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $ex; } } + $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); + $branch_links = array(); - foreach ($branches as $branch => $commit) { + foreach ($branches as $branch) { $branch_links[] = phutil_tag( 'a', array( 'href' => $request->generateURI( array( 'action' => 'browse', - 'branch' => $branch, + 'branch' => $branch->getShortName(), )), ), - $branch); + $branch->getShortName()); } return id(new AphrontAjaxResponse()) diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index d52bda931b..f572a86401 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -102,6 +102,14 @@ final class DiffusionCommitController extends DiffusionController { 'diffusion.commitparentsquery', array('commit' => $drequest->getCommit())); + if ($parents) { + $parents = id(new DiffusionCommitQuery()) + ->setViewer($user) + ->withRepository($repository) + ->withIdentifiers($parents) + ->execute(); + } + $headsup_view = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))); @@ -239,17 +247,21 @@ final class DiffusionCommitController extends DiffusionController { // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); - $change_panel = new AphrontPanelView(); - $change_panel->setHeader("Changes (".number_format($count).")"); + $change_panel = new PHUIObjectBoxView(); + $header = new PHUIHeaderView(); + $header->setHeader("Changes (".number_format($count).")"); $change_panel->setID('toc'); if ($count > self::CHANGES_LIMIT && !$show_all_details) { - $show_all_button = phutil_tag( - 'a', - array( - 'class' => 'button green', - 'href' => '?show_all=true', - ), - pht('Show All Changes')); + + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('transcript'); + + $button = id(new PHUIButtonView()) + ->setText(pht('Show All Changes')) + ->setHref('?show_all=true') + ->setTag('a') + ->setIcon($icon); $warning_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) @@ -257,12 +269,12 @@ final class DiffusionCommitController extends DiffusionController { ->appendChild( pht("This commit is very large. Load each file individually.")); - $change_panel->appendChild($warning_view); - $change_panel->addButton($show_all_button); + $change_panel->setErrorView($warning_view); + $header->addActionLink($button); } $change_panel->appendChild($change_table); - $change_panel->setNoBackground(); + $change_panel->setHeader($header); $content[] = $change_panel; @@ -336,10 +348,8 @@ final class DiffusionCommitController extends DiffusionController { $change_list->setRenderURI('/diffusion/'.$callsign.'/diff/'); $change_list->setRepository($repository); $change_list->setUser($user); - // pick the first branch for "Browse in Diffusion" View Option - $branches = $commit_data->getCommitDetail('seenOnBranches', array()); - $first_branch = reset($branches); - $change_list->setBranch($first_branch); + + // TODO: Try to setBranch() to something reasonable here? $change_list->setStandaloneURI( '/diffusion/'.$callsign.'/diff/'); @@ -403,8 +413,10 @@ final class DiffusionCommitController extends DiffusionController { array $audit_requests) { assert_instances_of($parents, 'PhabricatorRepositoryCommit'); - $user = $this->getRequest()->getUser(); + $viewer = $this->getRequest()->getUser(); $commit_phid = $commit->getPHID(); + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) @@ -440,6 +452,29 @@ final class DiffusionCommitController extends DiffusionController { } } + // NOTE: We should never normally have more than a single push log, but + // it can occur naturally if a commit is pushed, then the branch it was + // on is deleted, then the commit is pushed again (or through other similar + // chains of events). This should be rare, but does not indicate a bug + // or data issue. + + // NOTE: We never query push logs in SVN because the commiter is always + // the pusher and the commit time is always the push time; the push log + // is redundant and we save a query by skipping it. + + $push_logs = array(); + if ($repository->isHosted() && !$repository->isSVN()) { + $push_logs = id(new PhabricatorRepositoryPushLogQuery()) + ->setViewer($viewer) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->withNewRefs(array($commit->getCommitIdentifier())) + ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) + ->execute(); + foreach ($push_logs as $log) { + $phids[] = $log->getPusherPHID(); + } + } + $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); @@ -450,22 +485,22 @@ final class DiffusionCommitController extends DiffusionController { if ($commit->getAuditStatus()) { $status = PhabricatorAuditCommitStatusConstants::getStatusName( $commit->getAuditStatus()); - $tag = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) ->setName($status); switch ($commit->getAuditStatus()) { case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_ORANGE); + $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE); break; case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_RED); + $tag->setBackgroundColor(PHUITagView::COLOR_RED); break; case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_BLUE); + $tag->setBackgroundColor(PHUITagView::COLOR_BLUE); break; case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: - $tag->setBackgroundColor(PhabricatorTagView::COLOR_GREEN); + $tag->setBackgroundColor(PHUITagView::COLOR_GREEN); break; } @@ -494,13 +529,53 @@ final class DiffusionCommitController extends DiffusionController { } } - $props['Committed'] = phabricator_datetime($commit->getEpoch(), $user); - $author_phid = $data->getCommitDetail('authorPHID'); - if ($data->getCommitDetail('authorPHID')) { - $props['Author'] = $handles[$author_phid]->renderLink(); - } else { - $props['Author'] = $data->getAuthorName(); + $author_name = $data->getAuthorName(); + + if (!$repository->isSVN()) { + $authored_info = id(new PHUIStatusItemView()); + // TODO: In Git, a distinct authorship date is available. When present, + // we should show it here. + + if ($author_phid) { + $authored_info->setTarget($handles[$author_phid]->renderLink()); + } else if (strlen($author_name)) { + $authored_info->setTarget($author_name); + } + + $props['Authored'] = id(new PHUIStatusListView()) + ->addItem($authored_info); + } + + $committed_info = id(new PHUIStatusItemView()) + ->setNote(phabricator_datetime($commit->getEpoch(), $viewer)); + + $committer_phid = $data->getCommitDetail('committerPHID'); + $committer_name = $data->getCommitDetail('committer'); + if ($committer_phid) { + $committed_info->setTarget($handles[$committer_phid]->renderLink()); + } else if (strlen($committer_name)) { + $committed_info->setTarget($committer_name); + } else if ($author_phid) { + $committed_info->setTarget($handles[$author_phid]->renderLink()); + } else if (strlen($author_name)) { + $committed_info->setTarget($author_name); + } + + $props['Committed'] = id(new PHUIStatusListView()) + ->addItem($committed_info); + + if ($push_logs) { + $pushed_list = new PHUIStatusListView(); + + foreach ($push_logs as $push_log) { + $pushed_item = id(new PHUIStatusItemView()) + ->setTarget($handles[$push_log->getPusherPHID()]->renderLink()) + ->setNote(phabricator_datetime($push_log->getEpoch(), $viewer)); + $pushed_list->addItem($pushed_item); + } + + $props['Pushed'] = $pushed_list; } $reviewer_phid = $data->getCommitDetail('reviewerPHID'); @@ -508,16 +583,6 @@ final class DiffusionCommitController extends DiffusionController { $props['Reviewer'] = $handles[$reviewer_phid]->renderLink(); } - $committer = $data->getCommitDetail('committer'); - if ($committer) { - $committer_phid = $data->getCommitDetail('committerPHID'); - if ($data->getCommitDetail('committerPHID')) { - $props['Committer'] = $handles[$committer_phid]->renderLink(); - } else { - $props['Committer'] = $committer; - } - } - if ($revision_phid) { $props['Differential Revision'] = $handles[$revision_phid]->renderLink(); } @@ -530,8 +595,6 @@ final class DiffusionCommitController extends DiffusionController { $props['Parents'] = phutil_implode_html(" \xC2\xB7 ", $parent_links); } - $request = $this->getDiffusionRequest(); - $props['Branches'] = phutil_tag( 'span', array( @@ -545,7 +608,7 @@ final class DiffusionCommitController extends DiffusionController { ), pht('Unknown')); - $callsign = $request->getRepository()->getCallsign(); + $callsign = $repository->getCallsign(); $root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier(); Javelin::initBehavior( 'diffusion-commit-branches', @@ -554,7 +617,7 @@ final class DiffusionCommitController extends DiffusionController { $root.'/tags/' => 'commit-tags', )); - $refs = $this->buildRefs($request); + $refs = $this->buildRefs($drequest); if ($refs) { $props['References'] = $refs; } @@ -744,21 +807,26 @@ final class DiffusionCommitController extends DiffusionController { 'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/', )); - $preview_panel = hsprintf( - '
-
-
- Loading preview... -
-
-
-
-
'); + $loading = phutil_tag_div( + 'aphront-panel-preview-loading-text', + pht('Loading preview...')); + + $preview_panel = phutil_tag_div( + 'aphront-panel-preview aphront-panel-flush', + array( + phutil_tag('div', array('id' => 'audit-preview'), $loading), + phutil_tag('div', array('id' => 'inline-comment-preview')) + )); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); + $anchor = id(new PhabricatorAnchorView()) + ->setAnchorName('comment') + ->setNavigationMarker(true) + ->render(); + $comment_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($form); @@ -768,14 +836,9 @@ final class DiffusionCommitController extends DiffusionController { array( 'id' => $pane_id, ), - hsprintf( - '
%s%s%s
', - id(new PhabricatorAnchorView()) - ->setAnchorName('comment') - ->setNavigationMarker(true) - ->render(), - $comment_box, - $preview_panel)); + phutil_tag_div( + 'differential-add-comment-panel', + array($anchor, $comment_box, $preview_panel))); } /** @@ -988,8 +1051,16 @@ final class DiffusionCommitController extends DiffusionController { $raw_diff, array( 'name' => $drequest->getCommit().'.diff', + 'ttl' => (60 * 60 * 24), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file->attachToObject( + $this->getRequest()->getUser(), + $drequest->getRepository()->getPHID()); + unset($unguarded); + return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } diff --git a/src/applications/diffusion/controller/DiffusionCommitEditController.php b/src/applications/diffusion/controller/DiffusionCommitEditController.php index 13e78ea1a0..9d1024c60c 100644 --- a/src/applications/diffusion/controller/DiffusionCommitEditController.php +++ b/src/applications/diffusion/controller/DiffusionCommitEditController.php @@ -45,7 +45,7 @@ final class DiffusionCommitEditController extends DiffusionController { $editor->save(); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($commit->getPHID()); + ->queueDocumentForIndexing($commit->getPHID()); return id(new AphrontRedirectResponse()) ->setURI('/r'.$callsign.$commit->getCommitIdentifier()); diff --git a/src/applications/diffusion/controller/DiffusionExternalController.php b/src/applications/diffusion/controller/DiffusionExternalController.php index 50a8e2aff1..cc322a3034 100644 --- a/src/applications/diffusion/controller/DiffusionExternalController.php +++ b/src/applications/diffusion/controller/DiffusionExternalController.php @@ -33,7 +33,7 @@ final class DiffusionExternalController extends DiffusionController { if ($remote_uri->getPath() == $uri_path) { $matches[$key] = 1; } - if ($repository->getPublicRemoteURI() == $uri) { + if ($repository->getPublicCloneURI() == $uri) { $matches[$key] = 2; } if ($repository->getRemoteURI() == $uri) { diff --git a/src/applications/diffusion/controller/DiffusionLintController.php b/src/applications/diffusion/controller/DiffusionLintController.php index 3280f60c26..f826f33aa8 100644 --- a/src/applications/diffusion/controller/DiffusionLintController.php +++ b/src/applications/diffusion/controller/DiffusionLintController.php @@ -69,25 +69,20 @@ final class DiffusionLintController extends DiffusionController { $total += $code['n']; + $href_lint = $drequest->generateURI(array( + 'action' => 'lint', + 'lint' => $code['code'], + )); + $href_browse = $drequest->generateURI(array( + 'action' => 'browse', + 'lint' => $code['code'], + )); + $href_repo = $drequest->generateURI(array('action' => 'lint')); + $rows[] = array( - hsprintf( - '%s', - $drequest->generateURI(array( - 'action' => 'lint', - 'lint' => $code['code'], - )), - $code['n']), - hsprintf( - '%s', - $drequest->generateURI(array( - 'action' => 'browse', - 'lint' => $code['code'], - )), - $code['files']), - hsprintf( - '%s', - $drequest->generateURI(array('action' => 'lint')), - $drequest->getCallsign()), + phutil_tag('a', array('href' => $href_lint), $code['n']), + phutil_tag('a', array('href' => $href_browse), $code['files']), + phutil_tag('a', array('href' => $href_repo), $drequest->getCallsign()), ArcanistLintSeverity::getStringForSeverity($code['maxSeverity']), $code['code'], $code['maxName'], @@ -144,9 +139,7 @@ final class DiffusionLintController extends DiffusionController { if ($this->diffusionRequest) { $title[] = $drequest->getCallsign(); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('All Lint'))); + $crumbs->addTextCrumb(pht('All Lint')); } if ($this->diffusionRequest) { @@ -323,7 +316,7 @@ final class DiffusionLintController extends DiffusionController { $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActions($actions); + ->setActionList($actions); $callsign = $drequest->getRepository()->getCallsign(); $lint_commit = $branch->getLintCommit(); diff --git a/src/applications/diffusion/controller/DiffusionLintDetailsController.php b/src/applications/diffusion/controller/DiffusionLintDetailsController.php index d9437e6f12..6225fd68bb 100644 --- a/src/applications/diffusion/controller/DiffusionLintDetailsController.php +++ b/src/applications/diffusion/controller/DiffusionLintDetailsController.php @@ -15,22 +15,22 @@ final class DiffusionLintDetailsController extends DiffusionController { $rows = array(); foreach ($messages as $message) { - $path = hsprintf( - '%s', - $drequest->generateURI(array( + $path = phutil_tag( + 'a', + array('href' => $drequest->generateURI(array( 'action' => 'lint', 'path' => $message['path'], - )), + ))), substr($message['path'], strlen($drequest->getPath()) + 1)); - $line = hsprintf( - '%s', - $drequest->generateURI(array( + $line = phutil_tag( + 'a', + array('href' => $drequest->generateURI(array( 'action' => 'browse', 'path' => $message['path'], 'line' => $message['line'], 'commit' => $branch->getLintCommit(), - )), + ))), $message['line']); $author = $message['authorPHID']; diff --git a/src/applications/diffusion/controller/DiffusionMirrorDeleteController.php b/src/applications/diffusion/controller/DiffusionMirrorDeleteController.php new file mode 100644 index 0000000000..76b824b556 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionMirrorDeleteController.php @@ -0,0 +1,47 @@ +id = $data['id']; + parent::willProcessRequest($data); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $drequest = $this->diffusionRequest; + $repository = $drequest->getRepository(); + + $mirror = id(new PhabricatorRepositoryMirrorQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$mirror) { + return new Aphront404Response(); + } + + $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/#mirrors'); + + if ($request->isFormPost()) { + $mirror->delete(); + return id(new AphrontReloadResponse())->setURI($edit_uri); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Really delete mirror?')) + ->appendChild( + pht('Phabricator will stop pushing updates to this mirror.')) + ->addSubmitButton(pht('Delete Mirror')) + ->addCancelButton($edit_uri); + + return id(new AphrontDialogResponse()) + ->setDialog($dialog); + } + + +} diff --git a/src/applications/diffusion/controller/DiffusionMirrorEditController.php b/src/applications/diffusion/controller/DiffusionMirrorEditController.php new file mode 100644 index 0000000000..a3a6471184 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionMirrorEditController.php @@ -0,0 +1,121 @@ +id = idx($data, 'id'); + parent::willProcessRequest($data); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $drequest = $this->diffusionRequest; + $repository = $drequest->getRepository(); + + if ($this->id) { + $mirror = id(new PhabricatorRepositoryMirrorQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$mirror) { + return new Aphront404Response(); + } + $is_new = false; + } else { + $mirror = PhabricatorRepositoryMirror::initializeNewMirror($viewer) + ->setRepositoryPHID($repository->getPHID()) + ->attachRepository($repository); + $is_new = true; + } + + $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/#mirrors'); + + $v_remote = $mirror->getRemoteURI(); + $e_remote = true; + + $v_credentials = $mirror->getCredentialPHID(); + $e_credentials = null; + + $credentials = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIsDestroyed(false) + ->execute(); + + $errors = array(); + if ($request->isFormPost()) { + $v_remote = $request->getStr('remoteURI'); + if (strlen($v_remote)) { + $e_remote = null; + } else { + $e_remote = pht('Required'); + $errors[] = pht('You must provide a remote URI.'); + } + + $v_credentials = $request->getStr('credential'); + if ($v_credentials) { + $phids = mpull($credentials, null, 'getPHID'); + if (empty($phids[$v_credentials])) { + $e_credentials = pht('Invalid'); + $errors[] = pht( + 'You do not have permission to use those credentials.'); + } + } + + if (!$errors) { + $mirror + ->setRemoteURI($v_remote) + ->setCredentialPHID($v_credentials) + ->save(); + return id(new AphrontReloadResponse())->setURI($edit_uri); + } + } + + $form_errors = null; + if ($errors) { + $form_errors = id(new AphrontErrorView()) + ->setErrors($errors); + } + + if ($is_new) { + $title = pht('Create Mirror'); + $submit = pht('Create Mirror'); + } else { + $title = pht('Edit Mirror'); + $submit = pht('Save Changes'); + } + + $form = id(new PHUIFormLayoutView()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Remote URI')) + ->setName('remoteURI') + ->setValue($v_remote) + ->setError($e_remote)) + ->appendChild( + id(new PassphraseCredentialControl()) + ->setLabel(pht('Credentials')) + ->setName('credential') + ->setAllowNull(true) + ->setValue($v_credentials) + ->setError($e_credentials) + ->setOptions($credentials)); + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($form_errors) + ->appendChild($form) + ->addSubmitButton($submit) + ->addCancelButton($edit_uri); + + return id(new AphrontDialogResponse()) + ->setDialog($dialog); + } + + +} diff --git a/src/applications/diffusion/controller/DiffusionPushLogListController.php b/src/applications/diffusion/controller/DiffusionPushLogListController.php new file mode 100644 index 0000000000..f0a6414198 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionPushLogListController.php @@ -0,0 +1,144 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PhabricatorRepositoryPushLogSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function renderResultsList( + array $logs, + PhabricatorSavedQuery $query) { + $viewer = $this->getRequest()->getUser(); + + $this->loadHandles(mpull($logs, 'getPusherPHID')); + + // Figure out which repositories are editable. We only let you see remote + // IPs if you have edit capability on a repository. + $editable_repos = array(); + if ($logs) { + $editable_repos = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withPHIDs(mpull($logs, 'getRepositoryPHID')) + ->execute(); + $editable_repos = mpull($editable_repos, null, 'getPHID'); + } + + $rows = array(); + foreach ($logs as $log) { + + // Reveal this if it's valid and the user can edit the repository. + $remote_addr = '-'; + if (isset($editable_repos[$log->getRepositoryPHID()])) { + $remote_long = $log->getRemoteAddress(); + if ($remote_long) { + $remote_addr = long2ip($remote_long); + } + } + + $callsign = $log->getRepository()->getCallsign(); + $rows[] = array( + phutil_tag( + 'a', + array( + 'href' => $this->getApplicationURI($callsign.'/'), + ), + $callsign), + $this->getHandle($log->getPusherPHID())->renderLink(), + $remote_addr, + $log->getRemoteProtocol(), + $log->getRefType(), + $log->getRefName(), + phutil_tag( + 'a', + array( + 'href' => '/r'.$callsign.$log->getRefOld(), + ), + $log->getRefOldShort()), + phutil_tag( + 'a', + array( + 'href' => '/r'.$callsign.$log->getRefNew(), + ), + $log->getRefNewShort()), + + // TODO: Make these human-readable. + $log->getChangeFlags(), + $log->getRejectCode(), + phabricator_datetime($log->getEpoch(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Repository'), + pht('Pusher'), + pht('From'), + pht('Via'), + pht('Type'), + pht('Name'), + pht('Old'), + pht('New'), + pht('Flags'), + pht('Code'), + pht('Date'), + )) + ->setColumnClasses( + array( + '', + '', + '', + '', + '', + 'wide', + 'n', + 'n', + 'date', + )); + + $box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($table); + + return $box; + } + + public function buildSideNavView($for_app = false) { + $viewer = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new PhabricatorRepositoryPushLogSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + +} diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index 42465ab0bb..95841e54bf 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -123,13 +123,11 @@ final class DiffusionRepositoryController extends DiffusionController { if ($readme) { $box = new PHUIBoxView(); - $box->setShadow(true); $box->appendChild($readme); $box->addPadding(PHUI::PADDING_LARGE); - $panel = new AphrontPanelView(); - $panel->setHeader(pht('README')); - $panel->setNoBackground(); + $panel = new PHUIObjectBoxView(); + $panel->setHeaderText(pht('README')); $panel->appendChild($box); $content[] = $panel; } @@ -165,62 +163,64 @@ final class DiffusionRepositoryController extends DiffusionController { ->setUser($user); $view->addProperty(pht('Callsign'), $repository->getCallsign()); + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $repository->getPHID(), + PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT); + if ($project_phids) { + $this->loadHandles($project_phids); + $view->addProperty( + pht('Projects'), + $this->renderHandlesForPHIDs($project_phids)); + } + if ($repository->isHosted()) { - $serve_off = PhabricatorRepository::SERVE_OFF; - $callsign = $repository->getCallsign(); - $repo_path = '/diffusion/'.$callsign.'/'; - - $serve_ssh = $repository->getServeOverSSH(); - if ($serve_ssh !== $serve_off) { - $uri = new PhutilURI(PhabricatorEnv::getProductionURI($repo_path)); - $uri->setProtocol('ssh'); - - $ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); - if ($ssh_user) { - $uri->setUser($ssh_user); - } - - // This isn't quite right, but for now it's probably better to drop - // the port than sometimes show ":8080", etc. Using most VCS commands - // against nonstandard ports is a huge pain so few installs are likely - // to configure SSH on a nonstandard port. - $uri->setPort(null); - - $clone_uri = $this->renderCloneURI( - $uri, - $serve_ssh, + $ssh_uri = $repository->getSSHCloneURIObject(); + if ($ssh_uri) { + $clone_uri = $this->renderCloneCommand( + $repository, + $ssh_uri, + $repository->getServeOverSSH(), '/settings/panel/ssh/'); - $view->addProperty(pht('Clone URI (SSH)'), $clone_uri); + $view->addProperty( + $repository->isSVN() + ? pht('Checkout (SSH)') + : pht('Clone (SSH)'), + $clone_uri); } - $serve_http = $repository->getServeOverHTTP(); - if ($serve_http !== $serve_off) { - $http_uri = PhabricatorEnv::getProductionURI($repo_path); - - $clone_uri = $this->renderCloneURI( + $http_uri = $repository->getHTTPCloneURIObject(); + if ($http_uri) { + $clone_uri = $this->renderCloneCommand( + $repository, $http_uri, - $serve_http, + $repository->getServeOverHTTP(), PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth') ? '/settings/panel/vcspassword/' : null); - $view->addProperty(pht('Clone URI (HTTP)'), $clone_uri); + $view->addProperty( + $repository->isSVN() + ? pht('Checkout (HTTP)') + : pht('Clone (HTTP)'), + $clone_uri); } } else { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $view->addProperty( - pht('Clone URI'), - $this->renderCloneURI( - $repository->getPublicRemoteURI())); + pht('Clone'), + $this->renderCloneCommand( + $repository, + $repository->getPublicCloneURI())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $view->addProperty( - pht('Repository Root'), - $this->renderCloneURI( - $repository->getPublicRemoteURI())); + pht('Checkout'), + $this->renderCloneCommand( + $repository, + $repository->getPublicCloneURI())); break; } } @@ -252,12 +252,11 @@ final class DiffusionRepositoryController extends DiffusionController { $limit = 15; - $branches = DiffusionBranchInformation::newFromConduit( - $this->callConduitWithDiffusionRequest( - 'diffusion.branchquery', - array( - 'limit' => $limit + 1, - ))); + $branches = $this->callConduitWithDiffusionRequest( + 'diffusion.branchquery', + array( + 'limit' => $limit + 1, + )); if (!$branches) { return null; } @@ -265,10 +264,12 @@ final class DiffusionRepositoryController extends DiffusionController { $more_branches = (count($branches) > $limit); $branches = array_slice($branches, 0, $limit); + $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); + $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) - ->withIdentifiers(mpull($branches, 'getHeadCommitIdentifier')) - ->withRepositoryIDs(array($drequest->getRepository()->getID())) + ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) + ->withRepository($drequest->getRepository()) ->execute(); $table = id(new DiffusionBranchTableView()) @@ -277,26 +278,29 @@ final class DiffusionRepositoryController extends DiffusionController { ->setBranches($branches) ->setCommits($commits); - $panel = id(new AphrontPanelView()) - ->setHeader(pht('Branches')) - ->setNoBackground(); + $panel = new PHUIObjectBoxView(); + $header = new PHUIHeaderView(); + $header->setHeader(pht('Branches')); if ($more_branches) { - $panel->setCaption(pht('Showing %d branches.', $limit)); + $header->setSubHeader(pht('Showing %d branches.', $limit)); } - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => $drequest->generateURI( + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('fork'); + + $button = new PHUIButtonView(); + $button->setText(pht("Show All Branches")); + $button->setTag('a'); + $button->setIcon($icon); + $button->setHref($drequest->generateURI( array( 'action' => 'branches', - )), - 'class' => 'grey button', - ), - pht("Show All Branches \xC2\xBB"))); + ))); + $header->addActionLink($button); + $panel->setHeader($header); $panel->appendChild($table); return $panel; @@ -332,7 +336,7 @@ final class DiffusionRepositoryController extends DiffusionController { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) - ->withRepositoryIDs(array($drequest->getRepository()->getID())) + ->withRepository($drequest->getRepository()) ->needCommitData(true) ->execute(); @@ -346,25 +350,31 @@ final class DiffusionRepositoryController extends DiffusionController { $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); - $panel = id(new AphrontPanelView()) - ->setHeader(pht('Tags')) - ->setNoBackground(true); + $panel = new PHUIObjectBoxView(); + $header = new PHUIHeaderView(); + $header->setHeader(pht('Tags')); if ($more_tags) { - $panel->setCaption(pht('Showing the %d most recent tags.', $tag_limit)); + $header->setSubHeader( + pht('Showing the %d most recent tags.', $tag_limit)); } - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => $drequest->generateURI( + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('tag'); + + $button = new PHUIButtonView(); + $button->setText(pht("Show All Tags")); + $button->setTag('a'); + $button->setIcon($icon); + $button->setHref($drequest->generateURI( array( 'action' => 'tags', - )), - 'class' => 'grey button', - ), - pht("Show All Tags \xC2\xBB"))); + ))); + + $header->addActionLink($button); + + $panel->setHeader($header); $panel->appendChild($view); return $panel; @@ -394,6 +404,18 @@ final class DiffusionRepositoryController extends DiffusionController { ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); + if ($repository->isHosted()) { + $callsign = $repository->getCallsign(); + $push_uri = $this->getApplicationURI( + 'pushlog/?repositories=r'.$callsign); + + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Push Logs')) + ->setIcon('transcript') + ->setHref($push_uri)); + } + return $view; } @@ -436,22 +458,27 @@ final class DiffusionRepositoryController extends DiffusionController { } $history_table->setIsHead(true); - $callsign = $drequest->getRepository()->getCallsign(); - $all = phutil_tag( - 'a', - array( - 'href' => $drequest->generateURI( - array( - 'action' => 'history', - )), - ), - pht('View Full Commit History')); - $panel = new AphrontPanelView(); - $panel->setHeader(pht("Recent Commits · %s", $all)); + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('transcript'); + + $button = id(new PHUIButtonView()) + ->setText(pht('View Full History')) + ->setHref($drequest->generateURI( + array( + 'action' => 'history', + ))) + ->setTag('a') + ->setIcon($icon); + + $panel = new PHUIObjectBoxView(); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Commits')) + ->addActionLink($button); + $panel->setHeader($header); $panel->appendChild($history_table); - $panel->setNoBackground(); return $panel; } @@ -490,19 +517,29 @@ final class DiffusionRepositoryController extends DiffusionController { $browse_uri = $drequest->generateURI(array('action' => 'browse')); - $browse_panel = new AphrontPanelView(); - $browse_panel->setHeader( - phutil_tag( - 'a', - array('href' => $browse_uri), - pht('Browse Repository'))); + $browse_panel = new PHUIObjectBoxView(); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Repository')); + + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('data'); + + $button = new PHUIButtonView(); + $button->setText(pht('Browse Repository')); + $button->setTag('a'); + $button->setIcon($icon); + $button->setHref($browse_uri); + + $header->addActionLink($button); + $browse_panel->setHeader($header); $browse_panel->appendChild($browse_table); - $browse_panel->setNoBackground(); return $browse_panel; } - private function renderCloneURI( + private function renderCloneCommand( + PhabricatorRepository $repository, $uri, $serve_mode = null, $manage_uri = null) { @@ -511,13 +548,33 @@ final class DiffusionRepositoryController extends DiffusionController { Javelin::initBehavior('select-on-click'); + switch ($repository->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $command = csprintf( + 'git clone %R', + $uri); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $command = csprintf( + 'hg clone %R', + $uri); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $command = csprintf( + 'svn checkout %R %R', + $uri, + $repository->getCloneName()); + break; + } + $input = javelin_tag( 'input', array( 'type' => 'text', - 'value' => (string)$uri, + 'value' => (string)$command, 'class' => 'diffusion-clone-uri', 'sigil' => 'select-on-click', + 'readonly' => 'true', )); $extras = array(); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index 36ad1feb83..7e77dce058 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -16,12 +16,13 @@ final class DiffusionRepositoryCreateController $viewer = $request->getUser(); // NOTE: We can end up here via either "Create Repository", or via - // "Import Repository", or via "Edit Remote". In the latter case, we show - // only a few of the pages. + // "Import Repository", or via "Edit Remote", or via "Edit Policies". In + // the latter two cases, we show only a few of the pages. $repository = null; switch ($this->edit) { case 'remote': + case 'policy': $repository = $this->getDiffusionRequest()->getRepository(); // Make sure we have CAN_EDIT. @@ -56,11 +57,17 @@ final class DiffusionRepositoryCreateController ->addPage('remote-uri', $this->buildRemoteURIPage()) ->addPage('auth', $this->buildAuthPage()); break; + case 'policy': + $title = pht('Edit Policies'); + $form + ->addPage('policy', $this->buildPolicyPage()); + break; case 'create': $title = pht('Create Repository'); $form ->addPage('vcs', $this->buildVCSPage()) ->addPage('name', $this->buildNamePage()) + ->addPage('policy', $this->buildPolicyPage()) ->addPage('done', $this->buildDonePage()); break; case 'import': @@ -70,6 +77,7 @@ final class DiffusionRepositoryCreateController ->addPage('name', $this->buildNamePage()) ->addPage('remote-uri', $this->buildRemoteURIPage()) ->addPage('auth', $this->buildAuthPage()) + ->addPage('policy', $this->buildPolicyPage()) ->addPage('done', $this->buildDonePage()); break; } @@ -80,6 +88,7 @@ final class DiffusionRepositoryCreateController $is_create = ($this->edit === 'import' || $this->edit === 'create'); $is_auth = ($this->edit == 'import' || $this->edit == 'remote'); + $is_policy = ($this->edit != 'remote'); $is_init = ($this->edit == 'create'); if ($is_create) { @@ -94,12 +103,11 @@ final class DiffusionRepositoryCreateController $type_activate = PhabricatorRepositoryTransaction::TYPE_ACTIVATE; $type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH; $type_remote_uri = PhabricatorRepositoryTransaction::TYPE_REMOTE_URI; - $type_ssh_login = PhabricatorRepositoryTransaction::TYPE_SSH_LOGIN; - $type_ssh_key = PhabricatorRepositoryTransaction::TYPE_SSH_KEY; - $type_ssh_keyfile = PhabricatorRepositoryTransaction::TYPE_SSH_KEYFILE; - $type_http_login = PhabricatorRepositoryTransaction::TYPE_HTTP_LOGIN; - $type_http_pass = PhabricatorRepositoryTransaction::TYPE_HTTP_PASS; $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING; + $type_credential = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL; + $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; + $type_push = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY; $xactions = array(); @@ -159,29 +167,28 @@ final class DiffusionRepositoryCreateController ->getValue()); $xactions[] = id(clone $template) - ->setTransactionType($type_ssh_login) + ->setTransactionType($type_credential) ->setNewValue( - $form->getPage('auth')->getControl('ssh-login')->getValue()); + $form->getPage('auth')->getControl('credential')->getValue()); + } + + if ($is_policy) { + $xactions[] = id(clone $template) + ->setTransactionType($type_view) + ->setNewValue( + $form->getPage('policy')->getControl('viewPolicy')->getValue()); $xactions[] = id(clone $template) - ->setTransactionType($type_ssh_key) + ->setTransactionType($type_edit) ->setNewValue( - $form->getPage('auth')->getControl('ssh-key')->getValue()); + $form->getPage('policy')->getControl('editPolicy')->getValue()); - $xactions[] = id(clone $template) - ->setTransactionType($type_ssh_keyfile) - ->setNewValue( - $form->getPage('auth')->getControl('ssh-keyfile')->getValue()); - - $xactions[] = id(clone $template) - ->setTransactionType($type_http_login) - ->setNewValue( - $form->getPage('auth')->getControl('http-login')->getValue()); - - $xactions[] = id(clone $template) - ->setTransactionType($type_http_pass) - ->setNewValue( - $form->getPage('auth')->getControl('http-pass')->getValue()); + { + $xactions[] = id(clone $template) + ->setTransactionType($type_push) + ->setNewValue( + $form->getPage('policy')->getControl('pushPolicy')->getValue()); + } } id(new PhabricatorRepositoryEditor()) @@ -198,20 +205,17 @@ final class DiffusionRepositoryCreateController if ($repository) { $dict = array( 'remoteURI' => $repository->getRemoteURI(), - 'ssh-login' => $repository->getDetail('ssh-login'), - 'ssh-key' => $repository->getDetail('ssh-key'), - 'ssh-keyfile' => $repository->getDetail('ssh-keyfile'), - 'http-login' => $repository->getDetail('http-login'), - 'http-pass' => $repository->getDetail('http-pass'), + 'credential' => $repository->getCredentialPHID(), + 'viewPolicy' => $repository->getViewPolicy(), + 'editPolicy' => $repository->getEditPolicy(), + 'pushPolicy' => $repository->getPushPolicy(), ); } $form->readFromObject($dict); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( @@ -469,8 +473,10 @@ final class DiffusionRepositoryCreateController "| `svn://svn.example.net/svnroot/` |\n". "| `file:///local/path/to/svnroot/` |\n". "\n\n". - "Make sure you specify the root of the repository, not a ". - "subdirectory."); + "You **MUST** specify the root of the repository, not a ". + "subdirectory. (If you want to import only part of a Subversion ". + "repository, use the //Import Only// option at the end of this ". + "workflow.)"); } else { throw new Exception("Unsupported VCS!"); } @@ -550,105 +556,88 @@ final class DiffusionRepositoryCreateController ->setUser($this->getRequest()->getUser()) ->setAdjustFormPageCallback(array($this, 'adjustAuthPage')) ->addControl( - id(new AphrontFormTextControl()) - ->setName('ssh-login') - ->setLabel('SSH User')) - ->addControl( - id(new AphrontFormTextAreaControl()) - ->setName('ssh-key') - ->setLabel('SSH Private Key') - ->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT) - ->setCaption( - hsprintf('Specify the entire private key, or...'))) - ->addControl( - id(new AphrontFormTextControl()) - ->setName('ssh-keyfile') - ->setLabel('SSH Private Key Path') - ->setCaption( - '...specify a path on disk where the daemon should '. - 'look for a private key.')) - ->addControl( - id(new AphrontFormTextControl()) - ->setName('http-login') - ->setLabel('Username')) - ->addControl( - id(new AphrontFormPasswordControl()) - ->setName('http-pass') - ->setLabel('Password')); + id(new PassphraseCredentialControl()) + ->setName('credential')); } public function adjustAuthPage($page) { $form = $page->getForm(); - $remote_uri = $form->getPage('remote-uri') - ->getControl('remoteURI') - ->getValue(); - if ($this->getRepository()) { $vcs = $this->getRepository()->getVersionControlSystem(); } else { $vcs = $form->getPage('vcs')->getControl('vcs')->getValue(); } + $remote_uri = $form->getPage('remote-uri') + ->getControl('remoteURI') + ->getValue(); $proto = $this->getRemoteURIProtocol($remote_uri); $remote_user = $this->getRemoteURIUser($remote_uri); - $page->getControl('ssh-login')->setHidden(true); - $page->getControl('ssh-key')->setHidden(true); - $page->getControl('ssh-keyfile')->setHidden(true); - $page->getControl('http-login')->setHidden(true); - $page->getControl('http-pass')->setHidden(true); + $c_credential = $page->getControl('credential'); + $c_credential->setDefaultUsername($remote_user); if ($this->isSSHProtocol($proto)) { - $page->getControl('ssh-login')->setHidden(false); - $page->getControl('ssh-key')->setHidden(false); - $page->getControl('ssh-keyfile')->setHidden(false); - - $c_login = $page->getControl('ssh-login'); - if (!strlen($c_login->getValue())) { - $c_login->setValue($remote_user); - } + $c_credential->setLabel(pht('SSH Key')); + $c_credential->setCredentialType( + PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE); + $provides_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE; $page->addRemarkupInstructions( pht( - 'Enter the username and private key to use to connect to the '. - 'the repository hosted at:'. + 'Choose or add the SSH credentials to use to connect to the the '. + 'repository hosted at:'. "\n\n". " lang=text\n". - " %s". - "\n\n". - 'You can either copy/paste the entire private key, or put it '. - 'somewhere on disk and provide the path to it.', + " %s", $remote_uri), - 'ssh-login'); - + 'credential'); } else if ($this->isUsernamePasswordProtocol($proto)) { - $page->getControl('http-login')->setHidden(false); - $page->getControl('http-pass')->setHidden(false); + $c_credential->setLabel(pht('Password')); + $c_credential->setAllowNull(true); + $c_credential->setCredentialType( + PassphraseCredentialTypePassword::CREDENTIAL_TYPE); + $provides_type = PassphraseCredentialTypePassword::PROVIDES_TYPE; $page->addRemarkupInstructions( pht( - 'Enter the a username and pasword used to connect to the '. + 'Choose the a username and pasword used to connect to the '. 'repository hosted at:'. "\n\n". " lang=text\n". " %s". "\n\n". "If this repository does not require a username or password, ". - "you can leave these fields blank.", + "you can continue to the next step.", $remote_uri), - 'http-login'); + 'credential'); } else if ($proto == 'file') { + $c_credential->setHidden(true); + $provides_type = null; + $page->addRemarkupInstructions( pht( - 'You do not need to configure any authentication options for '. - 'repositories accessed over the `file://` protocol. Continue '. - 'to the next step.'), - 'ssh-login'); + 'You do not need to configure any credentials for repositories '. + 'accessed over the `file://` protocol. Continue to the next step.'), + 'credential'); } else { throw new Exception("Unknown URI protocol!"); } + + if ($provides_type) { + $viewer = $this->getRequest()->getUser(); + + $options = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIsDestroyed(false) + ->withProvidesTypes(array($provides_type)) + ->execute(); + + $c_credential->setOptions($options); + } + } public function validateAuthPage(PHUIFormPageView $page) { @@ -656,54 +645,167 @@ final class DiffusionRepositoryCreateController $remote_uri = $form->getPage('remote')->getControl('remoteURI')->getValue(); $proto = $this->getRemoteURIProtocol($remote_uri); + $c_credential = $page->getControl('credential'); + $v_credential = $c_credential->getValue(); + + // NOTE: We're using the omnipotent user here because the viewer might be + // editing a repository they're allowed to edit which uses a credential they + // are not allowed to see. This is fine, as long as they don't change it. + $credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($v_credential)) + ->executeOne(); + if ($this->isSSHProtocol($proto)) { - $c_user = $page->getControl('ssh-login'); - $c_key = $page->getControl('ssh-key'); - $c_file = $page->getControl('ssh-keyfile'); - - $v_user = $c_user->getValue(); - $v_key = $c_key->getValue(); - $v_file = $c_file->getValue(); - - if (!strlen($v_user)) { - $c_user->setError(pht('Required')); + if (!$credential) { + $c_credential->setError(pht('Required')); $page->addPageError( - pht('You must provide an SSH login username to connect over SSH.')); + pht('You must choose an SSH credential to connect over SSH.')); } - if (!strlen($v_key) && !strlen($v_file)) { - $c_key->setError(pht('No Key')); - $c_file->setError(pht('No Key')); + $ssh_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE; + if ($credential->getProvidesType() !== $ssh_type) { + $c_credential->setError(pht('Invalid')); $page->addPageError( pht( - 'You must provide either a private key or the path to a private '. - 'key to connect over SSH.')); - } else if (strlen($v_key) && strlen($v_file)) { - $c_key->setError(pht('Ambiguous')); - $c_file->setError(pht('Ambiguous')); - $page->addPageError( - pht( - 'Provide either a private key or the path to a private key, not '. - 'both.')); - } else if (!preg_match('/PRIVATE KEY/', $v_key)) { - $c_key->setError(pht('Invalid')); - $page->addPageError( - pht( - 'The private key you provided is missing the PRIVATE KEY header. '. - 'You should include the header and footer. (Did you provide a '. - 'public key by mistake?)')); + 'You must choose an SSH credential, not some other type '. + 'of credential.')); } - return $c_user->isValid() && - $c_key->isValid() && - $c_file->isValid(); } else if ($this->isUsernamePasswordProtocol($proto)) { - return true; + if ($credential) { + $password_type = PassphraseCredentialTypePassword::PROVIDES_TYPE; + if ($credential->getProvidesType() !== $password_type) { + $c_credential->setError(pht('Invalid')); + $page->addPageError( + pht( + 'You must choose a username/password credential, not some other '. + 'type of credential.')); + } + } + + return $c_credential->isValid(); } else { return true; } } + +/* -( Page: Policy )------------------------------------------------------- */ + + + private function buildPolicyPage() { + $viewer = $this->getRequest()->getUser(); + if ($this->getRepository()) { + $repository = $this->getRepository(); + } else { + $repository = PhabricatorRepository::initializeNewRepository($viewer); + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($repository) + ->execute(); + + $view_policy = id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicyObject($repository) + ->setPolicies($policies) + ->setName('viewPolicy'); + + $edit_policy = id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicyObject($repository) + ->setPolicies($policies) + ->setName('editPolicy'); + + $push_policy = id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setCapability(DiffusionCapabilityPush::CAPABILITY) + ->setPolicyObject($repository) + ->setPolicies($policies) + ->setName('pushPolicy'); + + return id(new PHUIFormPageView()) + ->setPageName(pht('Policies')) + ->setValidateFormPageCallback(array($this, 'validatePolicyPage')) + ->setAdjustFormPageCallback(array($this, 'adjustPolicyPage')) + ->setUser($viewer) + ->addRemarkupInstructions( + pht( + "Select access policies for this repository.")) + ->addControl($view_policy) + ->addControl($edit_policy) + ->addControl($push_policy); + } + + public function adjustPolicyPage(PHUIFormPageView $page) { + if ($this->getRepository()) { + $repository = $this->getRepository(); + $show_push = true; + } else { + $show_push = ($this->edit == 'create'); + } + + if (!$show_push) { + $c_push = $page->getControl('pushPolicy'); + $c_push->setHidden(true); + } + } + + public function validatePolicyPage(PHUIFormPageView $page) { + $form = $page->getForm(); + $viewer = $this->getRequest()->getUser(); + + $c_view = $page->getControl('viewPolicy'); + $c_edit = $page->getControl('editPolicy'); + $c_push = $page->getControl('pushPolicy'); + $v_view = $c_view->getValue(); + $v_edit = $c_edit->getValue(); + $v_push = $c_push->getValue(); + + if ($this->getRepository()) { + $repository = $this->getRepository(); + } else { + $repository = PhabricatorRepository::initializeNewRepository($viewer); + } + + $proxy = clone $repository; + $proxy->setViewPolicy($v_view); + $proxy->setEditPolicy($v_edit); + + $can_view = PhabricatorPolicyFilter::hasCapability( + $viewer, + $proxy, + PhabricatorPolicyCapability::CAN_VIEW); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $proxy, + PhabricatorPolicyCapability::CAN_EDIT); + + if (!$can_view) { + $c_view->setError(pht('Invalid')); + $page->addPageError( + pht( + 'You can not use the selected policy, because you would be unable '. + 'to see the repository.')); + } + + if (!$can_edit) { + $c_edit->setError(pht('Invalid')); + $page->addPageError( + pht( + 'You can not use the selected edit policy, because you would be '. + 'unable to edit the repository.')); + } + + return $c_view->isValid() && + $c_edit->isValid(); + } + /* -( Page: Done )--------------------------------------------------------- */ diff --git a/src/applications/diffusion/controller/DiffusionRepositoryDefaultController.php b/src/applications/diffusion/controller/DiffusionRepositoryDefaultController.php index e939d0e54b..f81c8f9bbe 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryDefaultController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryDefaultController.php @@ -6,6 +6,12 @@ final class DiffusionRepositoryDefaultController extends DiffusionController { // NOTE: This controller is just here to make sure we call // willBeginExecution() on any /diffusion/X/ URI, so we can intercept // `git`, `hg` and `svn` HTTP protocol requests. - return new Aphront404Response(); + + // If we made it here, it's probably because the user copy-pasted a + // clone URI with "/anything.git" at the end into their web browser. + // Send them to the canonical repository URI. + + return id(new AphrontRedirectResponse()) + ->setURI($this->getDiffusionRequest()->getRepository()->getURI()); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php index 67e3b7aa7e..fe2e2f818b 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php @@ -59,9 +59,7 @@ final class DiffusionRepositoryEditActionsController $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Actions'))); + $crumbs->addTextCrumb(pht('Edit Actions')); $title = pht('Edit Actions (%s)', $repository->getName()); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php index 62b58da016..039edcb9a8 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php @@ -16,6 +16,7 @@ final class DiffusionRepositoryEditBasicController PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) + ->needProjectPHIDs(true) ->withIDs(array($repository->getID())) ->executeOne(); @@ -27,12 +28,15 @@ final class DiffusionRepositoryEditBasicController $v_name = $repository->getName(); $v_desc = $repository->getDetail('description'); + $v_clone_name = $repository->getDetail('clone-name'); $e_name = true; $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); + $v_projects = $request->getArr('projectPHIDs'); + $v_clone_name = $request->getStr('cloneName'); if (!strlen($v_name)) { $e_name = pht('Required'); @@ -47,6 +51,8 @@ final class DiffusionRepositoryEditBasicController $type_name = PhabricatorRepositoryTransaction::TYPE_NAME; $type_desc = PhabricatorRepositoryTransaction::TYPE_DESCRIPTION; + $type_edge = PhabricatorTransactions::TYPE_EDGE; + $type_clone_name = PhabricatorRepositoryTransaction::TYPE_CLONE_NAME; $xactions[] = id(clone $template) ->setTransactionType($type_name) @@ -56,6 +62,20 @@ final class DiffusionRepositoryEditBasicController ->setTransactionType($type_desc) ->setNewValue($v_desc); + $xactions[] = id(clone $template) + ->setTransactionType($type_clone_name) + ->setNewValue($v_clone_name); + + $xactions[] = id(clone $template) + ->setTransactionType($type_edge) + ->setMetadataValue( + 'edge:type', + PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT) + ->setNewValue( + array( + '=' => array_fuse($v_projects), + )); + id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) @@ -67,18 +87,10 @@ final class DiffusionRepositoryEditBasicController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Basics'))); + $crumbs->addTextCrumb(pht('Edit Basics')); $title = pht('Edit %s', $repository->getName()); - - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } + $project_handles = $this->loadViewerHandles($repository->getProjectPHIDs()); $form = id(new AphrontFormView()) ->setUser($user) @@ -88,11 +100,26 @@ final class DiffusionRepositoryEditBasicController ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('cloneName') + ->setLabel(pht('Clone/Checkout As')) + ->setValue($v_clone_name) + ->setCaption( + pht( + 'Optional directory name to use when cloning or checking out '. + 'this repository.'))) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/projects/') + ->setName('projectPHIDs') + ->setLabel(pht('Projects')) + ->setValue($project_handles)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) @@ -103,7 +130,7 @@ final class DiffusionRepositoryEditBasicController $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) - ->setFormError($error_view); + ->setFormErrors($errors); return $this->buildApplicationPage( array( diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php index 9cd546c92a..27ada2d132 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php @@ -22,9 +22,15 @@ final class DiffusionRepositoryEditBranchesController return new Aphront404Response(); } + $is_git = false; + $is_hg = false; + switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $is_git = true; + break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $is_hg = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: throw new Exception( @@ -64,9 +70,11 @@ final class DiffusionRepositoryEditBranchesController ->setTransactionType($type_track) ->setNewValue($v_track); - $xactions[] = id(clone $template) - ->setTransactionType($type_autoclose) - ->setNewValue($v_autoclose); + if (!$is_hg) { + $xactions[] = id(clone $template) + ->setTransactionType($type_autoclose) + ->setNewValue($v_autoclose); + } id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) @@ -80,9 +88,7 @@ final class DiffusionRepositoryEditBranchesController $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Branches'))); + $crumbs->addTextCrumb(pht('Edit Branches')); $title = pht('Edit Branches (%s)', $repository->getName()); @@ -119,18 +125,22 @@ final class DiffusionRepositoryEditBranchesController ->setLabel(pht('Track Only')) ->setValue($v_track) ->setCaption( - pht('Example: %s', phutil_tag('tt', array(), 'master, develop')))) - ->appendChild( + pht('Example: %s', phutil_tag('tt', array(), 'master, develop')))); + + if (!$is_hg) { + $form->appendChild( id(new AphrontFormTextControl()) ->setName('autoclose') ->setLabel(pht('Autoclose Only')) ->setValue($v_autoclose) ->setCaption( - pht('Example: %s', phutil_tag('tt', array(), 'master, release')))) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Branches')) - ->addCancelButton($edit_uri)); + pht('Example: %s', phutil_tag('tt', array(), 'master, release')))); + } + + $form->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save Branches')) + ->addCancelButton($edit_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditController.php index 292597cbf5..6b8deca4f4 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditController.php @@ -11,20 +11,12 @@ abstract class DiffusionRepositoryEditController $repo_uri = $this->getRepositoryControllerURI($repository, ''); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('r'.$repository->getCallsign()) - ->setHref($repo_uri)); + $crumbs->addTextCrumb('r'.$repository->getCallsign(), $repo_uri); if ($is_main) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Repository'))); + $crumbs->addTextCrumb(pht('Edit Repository')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit')) - ->setHref($edit_uri)); + $crumbs->addTextCrumb(pht('Edit'), $edit_uri); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php new file mode 100644 index 0000000000..97779a8839 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php @@ -0,0 +1,78 @@ +getRequest(); + $viewer = $request->getUser(); + $drequest = $this->diffusionRequest; + $repository = $drequest->getRepository(); + + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($repository->getID())) + ->executeOne(); + + if (!$repository) { + return new Aphront404Response(); + } + + if (!$repository->canAllowDangerousChanges()) { + return new Aphront400Response(); + } + + $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); + + if ($request->isFormPost()) { + $xaction = id(new PhabricatorRepositoryTransaction()) + ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS) + ->setNewValue(!$repository->shouldAllowDangerousChanges()); + + $editor = id(new PhabricatorRepositoryEditor()) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setActor($viewer) + ->applyTransactions($repository, array($xaction)); + + return id(new AphrontReloadResponse())->setURI($edit_uri); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer); + + $force = phutil_tag('tt', array(), '--force'); + + if ($repository->shouldAllowDangerousChanges()) { + $dialog + ->setTitle(pht('Prevent Dangerous changes?')) + ->appendChild( + pht( + 'It will no longer be possible to delete branches from this '. + 'repository, or %s push to this repository.', + $force)) + ->addSubmitButton(pht('Prevent Dangerous Changes')) + ->addCancelButton($edit_uri); + } else { + $dialog + ->setTitle(pht('Allow Dangerous Changes?')) + ->appendChild( + pht( + 'If you allow dangerous changes, it will be possible to delete '. + 'branches and %s push this repository. These operations can '. + 'alter a repository in a way that is difficult to recover from.', + $force)) + ->addSubmitButton(pht('Allow Dangerous Changes')) + ->addCancelButton($edit_uri); + } + + return id(new AphrontDialogResponse()) + ->setDialog($dialog); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php index 24ad783e09..55fa5f1420 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php @@ -57,19 +57,10 @@ final class DiffusionRepositoryEditEncodingController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Encoding'))); + $crumbs->addTextCrumb(pht('Edit Encoding')); $title = pht('Edit %s', $repository->getName()); - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions($this->getEncodingInstructions()) @@ -87,7 +78,7 @@ final class DiffusionRepositoryEditEncodingController $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) - ->setFormError($error_view); + ->setFormErrors($errors); return $this->buildApplicationPage( array( diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php index 7b39aa3602..4665aa8438 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php @@ -67,9 +67,7 @@ final class DiffusionRepositoryEditHostingController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Hosting'))); + $crumbs->addTextCrumb(pht('Edit Hosting')); $title = pht('Edit Hosting (%s)', $repository->getName()); @@ -92,15 +90,18 @@ final class DiffusionRepositoryEditHostingController 'Users will not be able to push commits to this repository.')) ->setValue($v_hosting); + $doc_href = PhabricatorEnv::getDoclink( + 'article/Diffusion_User_Guide_Repository_Hosting.html'); + $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( - 'NOTE: Hosting is extremely new and barely works! Use it at '. - 'your own risk.'. - "\n\n". 'Phabricator can host repositories, or it can track repositories '. - 'hosted elsewhere (like on GitHub or Bitbucket).')) + 'hosted elsewhere (like on GitHub or Bitbucket). For information '. + 'on configuring hosting, see [[ %s | Diffusion User Guide: '. + 'Repository Hosting]]', + $doc_href)) ->appendChild($hosted_control) ->appendChild( id(new AphrontFormSubmitControl()) @@ -126,6 +127,9 @@ final class DiffusionRepositoryEditHostingController $request = $this->getRequest(); $user = $request->getUser(); + $type = $repository->getVersionControlSystem(); + $is_svn = ($type == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); + $v_http_mode = $repository->getDetail( 'serve-over-http', PhabricatorRepository::SERVE_OFF); @@ -146,9 +150,11 @@ final class DiffusionRepositoryEditHostingController $type_http = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP; $type_ssh = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH; - $xactions[] = id(clone $template) - ->setTransactionType($type_http) - ->setNewValue($v_http_mode); + if (!$is_svn) { + $xactions[] = id(clone $template) + ->setTransactionType($type_http) + ->setNewValue($v_http_mode); + } $xactions[] = id(clone $template) ->setTransactionType($type_ssh) @@ -164,9 +170,7 @@ final class DiffusionRepositoryEditHostingController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Protocols'))); + $crumbs->addTextCrumb(pht('Edit Protocols')); $title = pht('Edit Protocols (%s)', $repository->getName()); @@ -232,6 +236,18 @@ final class DiffusionRepositoryEditHostingController PhabricatorRepository::SERVE_READWRITE), $rw_message); + if ($is_svn) { + $http_control = id(new AphrontFormMarkupControl()) + ->setLabel(pht('HTTP')) + ->setValue( + phutil_tag( + 'em', + array(), + pht( + 'Phabricator does not currently support HTTP access to '. + 'Subversion repositories.'))); + } + $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php index 33c004c847..79519c788a 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php @@ -26,76 +26,35 @@ final class DiffusionRepositoryEditLocalController $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_local = $repository->getHumanReadableDetail('local-path'); - $e_local = true; $errors = array(); - if ($request->isFormPost()) { - $v_local = $request->getStr('local'); - - if (!strlen($v_local)) { - $e_local = pht('Required'); - $errors[] = pht('You must specify a local path.'); - } - - if (!$errors) { - $xactions = array(); - $template = id(new PhabricatorRepositoryTransaction()); - - $type_local = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH; - - $xactions[] = id(clone $template) - ->setTransactionType($type_local) - ->setNewValue($v_local); - - try { - id(new PhabricatorRepositoryEditor()) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->setActor($user) - ->applyTransactions($repository, $xactions); - - return id(new AphrontRedirectResponse())->setURI($edit_uri); - } catch (Exception $ex) { - $errors[] = $ex->getMessage(); - } - } - } - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Local'))); + $crumbs->addTextCrumb(pht('Edit Local')); $title = pht('Edit %s', $repository->getName()); - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( - 'You can adjust the local path for this repository here. This is '. - 'an advanced setting and you usually should not change it.')) + "You can not adjust the local path for this repository from the ". + "web interface. To edit it, run this command:\n\n". + " phabricator/ $ ./bin/repository edit %s --as %s --local-path ...", + $repository->getCallsign(), + $user->getUsername())) ->appendChild( - id(new AphrontFormTextControl()) + id(new AphrontFormMarkupControl()) ->setName('local') ->setLabel(pht('Local Path')) - ->setValue($v_local) - ->setError($e_local)) + ->setValue($v_local)) ->appendChild( id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Local')) - ->addCancelButton($edit_uri)); + ->addCancelButton($edit_uri, pht('Done'))); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) - ->setFormError($error_view); + ->setFormErrors($errors); return $this->buildApplicationPage( array( diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php index 7b2b549c18..15905968f0 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php @@ -67,6 +67,7 @@ final class DiffusionRepositoryEditMainController $repository, $this->buildHostingActions($repository)); + $branches_properties = null; if ($has_branches) { $branches_properties = $this->buildBranchesProperties( @@ -114,36 +115,78 @@ final class DiffusionRepositoryEditMainController ->setTransactions($xactions) ->setMarkupEngine($engine); - $obj_box = id(new PHUIObjectBoxView()) + $boxes = array(); + + $boxes[] = id(new PHUIObjectBoxView()) ->setHeader($header) - ->addPropertyList($basic_properties) - ->addPropertyList($policy_properties) + ->addPropertyList($basic_properties); + + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Policies')) + ->addPropertyList($policy_properties); + + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Hosting')) ->addPropertyList($hosting_properties); + if ($repository->canMirror()) { + $mirror_actions = $this->buildMirrorActions($repository); + $mirror_properties = $this->buildMirrorProperties( + $repository, + $mirror_actions); + + $mirrors = id(new PhabricatorRepositoryMirrorQuery()) + ->setViewer($viewer) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->execute(); + + $mirror_list = $this->buildMirrorList($repository, $mirrors); + + $boxes[] = id(new PhabricatorAnchorView())->setAnchorName('mirrors'); + + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Mirrors')) + ->addPropertyList($mirror_properties); + + $boxes[] = $mirror_list; + } + if ($remote_properties) { - $obj_box->addPropertyList($remote_properties); + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Remote')) + ->addPropertyList($remote_properties); } if ($local_properties) { - $obj_box->addPropertyList($local_properties); + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Local')) + ->addPropertyList($local_properties); } - $obj_box->addPropertyList($encoding_properties); + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Text Encoding')) + ->addPropertyList($encoding_properties); if ($branches_properties) { - $obj_box->addPropertyList($branches_properties); + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Branches')) + ->addPropertyList($branches_properties); } if ($subversion_properties) { - $obj_box->addPropertyList($subversion_properties); + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Subversion')) + ->addPropertyList($subversion_properties); } - $obj_box->addPropertyList($actions_properties); + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Actions')) + ->addPropertyList($actions_properties); return $this->buildApplicationPage( array( $crumbs, - $obj_box, + $boxes, $xaction_view, ), array( @@ -188,6 +231,7 @@ final class DiffusionRepositoryEditMainController ->setIcon('delete') ->setHref( $this->getRepositoryControllerURI($repository, 'edit/delete/')) + ->setDisabled(true) ->setWorkflow(true)); return $view; @@ -209,6 +253,25 @@ final class DiffusionRepositoryEditMainController $view->addProperty(pht('Type'), $type); $view->addProperty(pht('Callsign'), $repository->getCallsign()); + + $clone_name = $repository->getDetail('clone-name'); + + $view->addProperty( + pht('Clone/Checkout As'), + $clone_name + ? $clone_name.'/' + : phutil_tag('em', array(), $repository->getCloneName().'/')); + + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $repository->getPHID(), + PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT); + if ($project_phids) { + $this->loadHandles($project_phids); + $view->addProperty( + pht('Projects'), + $this->renderHandlesForPHIDs($project_phids)); + } + $view->addProperty( pht('Status'), $this->buildRepositoryStatus($repository)); @@ -253,8 +316,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Text Encoding')); + ->setActionList($actions); $encoding = $repository->getDetail('encoding'); if (!$encoding) { @@ -291,8 +353,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Policies')); + ->setActionList($actions); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, @@ -337,8 +398,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Branches')); + ->setActionList($actions); $default_branch = nonempty( $repository->getHumanReadableDetail('default-branch'), @@ -388,8 +448,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Subversion')); + ->setActionList($actions); $svn_uuid = nonempty( $repository->getUUID(), @@ -429,8 +488,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Actions')); + ->setActionList($actions); $notify = $repository->getDetail('herald-disabled') ? pht('Off') @@ -472,13 +530,20 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Remote')); + ->setActionList($actions); $view->addProperty( pht('Remote URI'), $repository->getHumanReadableDetail('remote-uri')); + $credential_phid = $repository->getCredentialPHID(); + if ($credential_phid) { + $this->loadHandles(array($credential_phid)); + $view->addProperty( + pht('Credential'), + $this->getHandle($credential_phid)->renderLink()); + } + return $view; } @@ -507,8 +572,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setActionList($actions) - ->addSectionHeader(pht('Local')); + ->setActionList($actions); $view->addProperty( pht('Local Path'), @@ -531,6 +595,25 @@ final class DiffusionRepositoryEditMainController $this->getRepositoryControllerURI($repository, 'edit/hosting/')); $view->addAction($edit); + if ($repository->canAllowDangerousChanges()) { + if ($repository->shouldAllowDangerousChanges()) { + $changes = id(new PhabricatorActionView()) + ->setIcon('blame') + ->setName(pht('Prevent Dangerous Changes')) + ->setHref( + $this->getRepositoryControllerURI($repository, 'edit/dangerous/')) + ->setWorkflow(true); + } else { + $changes = id(new PhabricatorActionView()) + ->setIcon('warning') + ->setName(pht('Allow Dangerous Changes')) + ->setHref( + $this->getRepositoryControllerURI($repository, 'edit/dangerous/')) + ->setWorkflow(true); + } + $view->addAction($changes); + } + return $view; } @@ -542,8 +625,7 @@ final class DiffusionRepositoryEditMainController $view = id(new PHUIPropertyListView()) ->setUser($user) - ->setActionList($actions) - ->addSectionHeader(pht('Hosting')); + ->setActionList($actions); $hosting = $repository->isHosted() ? pht('Hosted on Phabricator') @@ -566,6 +648,18 @@ final class DiffusionRepositoryEditMainController PhabricatorRepository::getProtocolAvailabilityName( $repository->getServeOverSSH()))); + if ($repository->canAllowDangerousChanges()) { + if ($repository->shouldAllowDangerousChanges()) { + $description = pht('Allowed'); + } else { + $description = pht('Not Allowed'); + } + + $view->addProperty( + pht('Dangerous Changes'), + $description); + } + return $view; } @@ -867,7 +961,13 @@ final class DiffusionRepositoryEditMainController $percentage = 0; } - $percentage = sprintf('%.1f%%', $percentage); + // Cap this at "99.99%", because it's confusing to users when the actual + // fraction is "99.996%" and it rounds up to "100.00%". + if ($percentage > 99.99) { + $percentage = 99.99; + } + + $percentage = sprintf('%.2f%%', $percentage); $view->addItem( id(new PHUIStatusItemView()) @@ -893,4 +993,86 @@ final class DiffusionRepositoryEditMainController return $view; } + private function buildMirrorActions( + PhabricatorRepository $repository) { + + $viewer = $this->getRequest()->getUser(); + + $mirror_actions = id(new PhabricatorActionListView()) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setUser($viewer); + + $new_mirror_uri = $this->getRepositoryControllerURI( + $repository, + 'mirror/edit/'); + + $mirror_actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Add Mirror')) + ->setIcon('new') + ->setHref($new_mirror_uri) + ->setWorkflow(true)); + + return $mirror_actions; + } + + private function buildMirrorProperties( + PhabricatorRepository $repository, + PhabricatorActionListView $actions) { + + $viewer = $this->getRequest()->getUser(); + + $mirror_properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setActionList($actions); + + $mirror_properties->addProperty( + '', + phutil_tag( + 'em', + array(), + pht('Automatically push changes into other remotes.'))); + + return $mirror_properties; + } + + private function buildMirrorList( + PhabricatorRepository $repository, + array $mirrors) { + assert_instances_of($mirrors, 'PhabricatorRepositoryMirror'); + + $mirror_list = id(new PHUIObjectItemListView()) + ->setNoDataString(pht('This repository has no configured mirrors.')); + + foreach ($mirrors as $mirror) { + $item = id(new PHUIObjectItemView()) + ->setHeader($mirror->getRemoteURI()); + + $edit_uri = $this->getRepositoryControllerURI( + $repository, + 'mirror/edit/'.$mirror->getID().'/'); + + $delete_uri = $this->getRepositoryControllerURI( + $repository, + 'mirror/delete/'.$mirror->getID().'/'); + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('edit') + ->setHref($edit_uri) + ->setWorkflow(true)); + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('delete') + ->setHref($delete_uri) + ->setWorkflow(true)); + + $mirror_list->addItem($item); + } + + return $mirror_list; + } + + } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php deleted file mode 100644 index c18dc0e2b8..0000000000 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php +++ /dev/null @@ -1,125 +0,0 @@ -getRequest(); - $viewer = $request->getUser(); - $drequest = $this->diffusionRequest; - $repository = $drequest->getRepository(); - - $repository = id(new PhabricatorRepositoryQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withIDs(array($repository->getID())) - ->executeOne(); - - if (!$repository) { - return new Aphront404Response(); - } - - $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); - - $v_view = $repository->getViewPolicy(); - $v_edit = $repository->getEditPolicy(); - $v_push = $repository->getPushPolicy(); - - if ($request->isFormPost()) { - $v_view = $request->getStr('viewPolicy'); - $v_edit = $request->getStr('editPolicy'); - $v_push = $request->getStr('pushPolicy'); - - $xactions = array(); - $template = id(new PhabricatorRepositoryTransaction()); - - $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; - $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; - $type_push = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY; - - $xactions[] = id(clone $template) - ->setTransactionType($type_view) - ->setNewValue($v_view); - - $xactions[] = id(clone $template) - ->setTransactionType($type_edit) - ->setNewValue($v_edit); - - $xactions[] = id(clone $template) - ->setTransactionType($type_push) - ->setNewValue($v_push); - - id(new PhabricatorRepositoryEditor()) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->setActor($viewer) - ->applyTransactions($repository, $xactions); - - return id(new AphrontRedirectResponse())->setURI($edit_uri); - } - - $content = array(); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Policies'))); - - $title = pht('Edit Policies (%s)', $repository->getName()); - - $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($repository) - ->execute(); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicyObject($repository) - ->setPolicies($policies) - ->setName('viewPolicy')) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) - ->setPolicyObject($repository) - ->setPolicies($policies) - ->setName('editPolicy')); - - $form->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(DiffusionCapabilityPush::CAPABILITY) - ->setPolicyObject($repository) - ->setPolicies($policies) - ->setName('pushPolicy')); - - $form - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Policies')) - ->addCancelButton($edit_uri)); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setForm($form); - - return $this->buildApplicationPage( - array( - $crumbs, - $form_box, - ), - array( - 'title' => $title, - 'device' => true, - )); - } - -} diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php index 425d28d38b..a4985e7f33 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php @@ -70,9 +70,7 @@ final class DiffusionRepositoryEditSubversionController $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Subversion Info'))); + $crumbs->addTextCrumb(pht('Edit Subversion Info')); $title = pht('Edit Subversion Info (%s)', $repository->getName()); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryListController.php b/src/applications/diffusion/controller/DiffusionRepositoryListController.php index 5dbe816dae..2e6f8f4c4b 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryListController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryListController.php @@ -17,7 +17,6 @@ final class DiffusionRepositoryListController extends DiffusionController $request = $this->getRequest(); $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->queryKey) - ->setPreface($this->buildShortcuts()) ->setSearchEngine(new PhabricatorRepositorySearchEngine()) ->setNavigation($this->buildSideNavView()); @@ -31,13 +30,20 @@ final class DiffusionRepositoryListController extends DiffusionController $viewer = $this->getRequest()->getUser(); + $project_phids = array_fuse( + array_mergev( + mpull($repositories, 'getProjectPHIDs'))); + $project_handles = $this->loadViewerHandles($project_phids); + $list = new PHUIObjectItemListView(); + $list->setCards(true); foreach ($repositories as $repository) { $id = $repository->getID(); $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setHeader($repository->getName()) + ->setObjectName('r'.$repository->getCallsign()) ->setHref($this->getApplicationURI($repository->getCallsign().'/')); $commit = $repository->getMostRecentCommit(); @@ -50,7 +56,8 @@ final class DiffusionRepositoryListController extends DiffusionController $item->setEpoch($commit->getEpoch()); } - $item->addAttribute( + $item->addIcon( + 'none', PhabricatorRepositoryType::getNameForRepositoryType( $repository->getVersionControlSystem())); @@ -73,6 +80,15 @@ final class DiffusionRepositoryListController extends DiffusionController $item->addAttribute(pht('No Commits')); } + $handles = array_select_keys( + $project_handles, + $repository->getProjectPHIDs()); + if ($handles) { + $item->addAttribute( + id(new ManiphestTaskProjectsView()) + ->setHandles($handles)); + } + if (!$repository->isTracked()) { $item->setDisabled(true); $item->addIcon('disable-grey', pht('Inactive')); @@ -115,33 +131,4 @@ final class DiffusionRepositoryListController extends DiffusionController return $crumbs; } - private function buildShortcuts() { - $shortcuts = id(new PhabricatorRepositoryShortcut())->loadAll(); - if ($shortcuts) { - $shortcuts = msort($shortcuts, 'getSequence'); - - $rows = array(); - foreach ($shortcuts as $shortcut) { - $rows[] = array( - $shortcut->getName(), - $shortcut->getHref(), - $shortcut->getDescription(), - ); - } - - $list = new PHUIObjectItemListView(); - foreach ($rows as $row) { - $item = id(new PHUIObjectItemView()) - ->setHeader($row[0]) - ->setHref($row[1]) - ->setSubhead(($row[2] ? $row[2] : pht('No Description'))); - $list->addItem($item); - } - $shortcut_panel = array($list, phutil_tag('hr')); - } else { - $shortcut_panel = null; - } - return $shortcut_panel; - } - } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryNewController.php b/src/applications/diffusion/controller/DiffusionRepositoryNewController.php index 337a0a41f0..ecc4b5d1e3 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryNewController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryNewController.php @@ -26,6 +26,17 @@ final class DiffusionRepositoryNewController } } + $doc_href = PhabricatorEnv::getDoclink( + 'article/Diffusion_User_Guide_Repository_Hosting.html'); + + $doc_link = phutil_tag( + 'a', + array( + 'href' => $doc_href, + 'target' => '_blank', + ), + pht('Diffusion User Guide: Repository Hosting')); + $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( @@ -36,13 +47,10 @@ final class DiffusionRepositoryNewController pht('Create a New Hosted Repository'), array( pht( - 'Create a new, empty repository which Phabricator will host.'), - phutil_tag('br'), - pht( - '%s: This feature is very new and barely works. Use it '. - 'at your own risk! By choosing this option, you accept great '. - 'mortal peril.', - phutil_tag('strong', array(), pht('BEWARE'))), + 'Create a new, empty repository which Phabricator will host. '. + 'For instructions on configuring repository hosting, see %s. '. + 'This feature is new and in beta!', + $doc_link), )) ->addButton( 'import', @@ -59,9 +67,7 @@ final class DiffusionRepositoryNewController ->addCancelButton($this->getApplicationURI())); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Repository'))); + $crumbs->addTextCrumb(pht('New Repository')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create or Import Repository')) diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 74b57b7594..9cd7b2aee0 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -51,6 +51,7 @@ final class DiffusionServeController extends DiffusionController { if (!preg_match($regex, (string)$uri, $matches)) { return null; } + return $matches['callsign']; } @@ -172,18 +173,53 @@ final class DiffusionServeController extends DiffusionController { pht('This repository is not available over HTTP.')); } - switch ($repository->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $result = $this->serveGitRequest($repository, $viewer); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: - $result = $this->serveMercurialRequest($repository, $viewer); - break; - default: - $result = new PhabricatorVCSResponse( - 999, - pht('TODO: Implement meaningful responses.')); - break; + $vcs_type = $repository->getVersionControlSystem(); + $req_type = $this->isVCSRequest($request); + + if ($vcs_type != $req_type) { + switch ($req_type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $result = new PhabricatorVCSResponse( + 500, + pht('This is not a Git repository.')); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $result = new PhabricatorVCSResponse( + 500, + pht('This is not a Mercurial repository.')); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $result = new PhabricatorVCSResponse( + 500, + pht('This is not a Subversion repository.')); + break; + default: + $result = new PhabricatorVCSResponse( + 500, + pht('Unknown request type.')); + break; + } + } else { + switch ($vcs_type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $result = $this->serveGitRequest($repository, $viewer); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $result = $this->serveMercurialRequest($repository, $viewer); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $result = new PhabricatorVCSResponse( + 500, + pht( + 'Phabricator does not support HTTP access to Subversion '. + 'repositories.')); + break; + default: + $result = new PhabricatorVCSResponse( + 500, + pht('Unknown version control system.')); + break; + } } $code = $result->getHTTPResponseCode(); @@ -209,7 +245,7 @@ final class DiffusionServeController extends DiffusionController { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $service = $request->getStr('service'); - $path = $this->getRequestDirectoryPath(); + $path = $this->getRequestDirectoryPath($repository); // NOTE: Service names are the reverse of what you might expect, as they // are from the point of view of the server. The main read service is // "git-upload-pack", and the main write service is "git-receive-pack". @@ -228,43 +264,11 @@ final class DiffusionServeController extends DiffusionController { case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $cmd = $request->getStr('cmd'); if ($cmd == 'batch') { - // For "batch" we get a "cmds" argument like - // - // heads ;known nodes= - // - // We need to examine the commands (here, "heads" and "known") to - // make sure they're all read-only. - - $args = $this->getMercurialArguments(); - $cmds = idx($args, 'cmds'); - if ($cmds) { - - // NOTE: Mercurial has some code to escape semicolons, but it does - // not actually function for command separation. For example, these - // two batch commands will produce completely different results (the - // former will run the lookup; the latter will fail with a parser - // error): - // - // lookup key=a:xb;lookup key=z* 0 - // lookup key=a:;b;lookup key=z* 0 - // ^ - // | - // +-- Note semicolon. - // - // So just split unconditionally. - - $cmds = explode(';', $cmds); - foreach ($cmds as $sub_cmd) { - $name = head(explode(' ', $sub_cmd, 2)); - if (!DiffusionMercurialWireProtocol::isReadOnlyCommand($name)) { - return false; - } - } - return true; - } + $cmds = idx($this->getMercurialArguments(), 'cmds'); + return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds); } return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd); - case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION: + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; } @@ -279,7 +283,7 @@ final class DiffusionServeController extends DiffusionController { PhabricatorUser $viewer) { $request = $this->getRequest(); - $request_path = $this->getRequestDirectoryPath(); + $request_path = $this->getRequestDirectoryPath($repository); $repository_root = $repository->getLocalPath(); // Rebuild the query string to strip `__magic__` parameters and prevent @@ -319,15 +323,26 @@ final class DiffusionServeController extends DiffusionController { // TODO: Set these correctly. // GIT_COMMITTER_NAME // GIT_COMMITTER_EMAIL - ); + ) + $this->getCommonEnvironment($viewer); $input = PhabricatorStartup::getRawInput(); - list($err, $stdout, $stderr) = id(new ExecFuture('%s', $bin)) + $command = csprintf('%s', $bin); + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); + + list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) ->setEnv($env, true) ->write($input) ->resolve(); + if ($err) { + if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) { + // Ignore the error if the response passes this special check for + // validity. + $err = 0; + } + } + if ($err) { return new PhabricatorVCSResponse( 500, @@ -337,10 +352,33 @@ final class DiffusionServeController extends DiffusionController { return id(new DiffusionGitResponse())->setGitData($stdout); } - private function getRequestDirectoryPath() { + private function getRequestDirectoryPath(PhabricatorRepository $repository) { $request = $this->getRequest(); $request_path = $request->getRequestURI()->getPath(); - return preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); + $base_path = preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); + + // For Git repositories, strip an optional directory component if it + // isn't the name of a known Git resource. This allows users to clone + // repositories as "/diffusion/X/anything.git", for example. + if ($repository->isGit()) { + $known = array( + 'info', + 'git-upload-pack', + 'git-receive-pack', + ); + + foreach ($known as $key => $path) { + $known[$key] = preg_quote($path, '@'); + } + + $known = implode('|', $known); + + if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) { + $base_path = preg_replace('@^/([^/]+)@', '', $base_path); + } + } + + return $base_path; } private function authenticateHTTPRepositoryUser( @@ -371,6 +409,11 @@ final class DiffusionServeController extends DiffusionController { return null; } + if (!$user->isUserActivated()) { + // User is not activated. + return null; + } + $password_entry = id(new PhabricatorRepositoryVCSPassword()) ->loadOneWhere('userPHID = %s', $user->getPHID()); if (!$password_entry) { @@ -383,15 +426,12 @@ final class DiffusionServeController extends DiffusionController { return null; } - if ($user->getIsDisabled()) { - // User is disabled. - return null; - } - return $user; } - private function serveMercurialRequest(PhabricatorRepository $repository) { + private function serveMercurialRequest( + PhabricatorRepository $repository, + PhabricatorUser $viewer) { $request = $this->getRequest(); $bin = Filesystem::resolveBinary('hg'); @@ -399,7 +439,7 @@ final class DiffusionServeController extends DiffusionController { throw new Exception("Unable to find `hg` in PATH!"); } - $env = array(); + $env = $this->getCommonEnvironment($viewer); $input = PhabricatorStartup::getRawInput(); $cmd = $request->getStr('cmd'); @@ -411,7 +451,10 @@ final class DiffusionServeController extends DiffusionController { $input = strlen($input)."\n".$input."0\n"; } - list($err, $stdout, $stderr) = id(new ExecFuture('%s serve --stdio', $bin)) + $command = csprintf('%s serve --stdio', $bin); + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); + + list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) ->setEnv($env, true) ->setCWD($repository->getLocalPath()) ->write("{$cmd}\n{$args}{$input}") @@ -512,5 +555,38 @@ final class DiffusionServeController extends DiffusionController { return implode('', $out); } + private function isValidGitShallowCloneResponse($stdout, $stderr) { + // If you execute `git clone --depth N ...`, git sends a request which + // `git-http-backend` responds to by emitting valid output and then exiting + // with a failure code and an error message. If we ignore this error, + // everything works. + + // This is a pretty funky fix: it would be nice to more precisely detect + // that a request is a `--depth N` clone request, but we don't have any code + // to decode protocol frames yet. Instead, look for reasonable evidence + // in the error and output that we're looking at a `--depth` clone. + + // For evidence this isn't completely crazy, see: + // https://github.com/schacon/grack/pull/7 + + $stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m'; + $stderr_regexp = '(The remote end hung up unexpectedly)'; + + $has_pack = preg_match($stdout_regexp, $stdout); + $is_hangup = preg_match($stderr_regexp, $stderr); + + return $has_pack && $is_hangup; + } + + private function getCommonEnvironment(PhabricatorUser $viewer) { + $remote_addr = $this->getRequest()->getRemoteAddr(); + + return array( + DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), + DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_addr, + DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', + ); + } + } diff --git a/src/applications/diffusion/controller/DiffusionTagListController.php b/src/applications/diffusion/controller/DiffusionTagListController.php index 9daa82e70c..b6e0710630 100644 --- a/src/applications/diffusion/controller/DiffusionTagListController.php +++ b/src/applications/diffusion/controller/DiffusionTagListController.php @@ -50,7 +50,7 @@ final class DiffusionTagListController extends DiffusionController { } else { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) - ->withRepositoryIDs(array($repository->getID())) + ->withRepository($repository) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) ->needCommitData(true) ->execute(); diff --git a/src/applications/diffusion/data/DiffusionBranchInformation.php b/src/applications/diffusion/data/DiffusionBranchInformation.php deleted file mode 100644 index 26163da740..0000000000 --- a/src/applications/diffusion/data/DiffusionBranchInformation.php +++ /dev/null @@ -1,45 +0,0 @@ -name = $name; - return $this; - } - - public function getName() { - return $this->name; - } - - public function setHeadCommitIdentifier($head_commit_identifier) { - $this->headCommitIdentifier = $head_commit_identifier; - return $this; - } - - public function getHeadCommitIdentifier() { - return $this->headCommitIdentifier; - } - - public static function newFromConduit(array $dicts) { - $branches = array(); - foreach ($dicts as $dict) { - $branches[] = id(new DiffusionBranchInformation()) - ->setName($dict['name']) - ->setHeadCommitIdentifier($dict['headCommitIdentifier']); - } - return $branches; - } - - public function toDictionary() { - return array( - 'name' => $this->getName(), - 'headCommitIdentifier' => $this->getHeadCommitIdentifier() - ); - } - -} diff --git a/src/applications/diffusion/data/DiffusionCommitHash.php b/src/applications/diffusion/data/DiffusionCommitHash.php new file mode 100644 index 0000000000..8605a99a34 --- /dev/null +++ b/src/applications/diffusion/data/DiffusionCommitHash.php @@ -0,0 +1,26 @@ +hashValue = $hash_value; + return $this; + } + + public function getHashValue() { + return $this->hashValue; + } + + public function setHashType($hash_type) { + $this->hashType = $hash_type; + return $this; + } + + public function getHashType() { + return $this->hashType; + } + +} diff --git a/src/applications/diffusion/data/DiffusionCommitRef.php b/src/applications/diffusion/data/DiffusionCommitRef.php new file mode 100644 index 0000000000..5088eef00e --- /dev/null +++ b/src/applications/diffusion/data/DiffusionCommitRef.php @@ -0,0 +1,89 @@ +hashes = $hashes; + return $this; + } + + public function getHashes() { + return $this->hashes; + } + + public function setCommitterEmail($committer_email) { + $this->committerEmail = $committer_email; + return $this; + } + + public function getCommitterEmail() { + return $this->committerEmail; + } + + + public function setCommitterName($committer_name) { + $this->committerName = $committer_name; + return $this; + } + + public function getCommitterName() { + return $this->committerName; + } + + + public function setAuthorEmail($author_email) { + $this->authorEmail = $author_email; + return $this; + } + + public function getAuthorEmail() { + return $this->authorEmail; + } + + + public function setAuthorName($author_name) { + $this->authorName = $author_name; + return $this; + } + + public function getAuthorName() { + return $this->authorName; + } + + public function setMessage($message) { + $this->message = $message; + return $this; + } + + public function getMessage() { + return $this->message; + } + + public function getAuthor() { + return $this->formatUser($this->authorName, $this->authorEmail); + } + + public function getCommitter() { + return $this->formatUser($this->committerName, $this->committerEmail); + } + + private function formatUser($name, $email) { + if (strlen($name) && strlen($email)) { + return "{$name} <{$email}>"; + } else if (strlen($email)) { + return $email; + } else if (strlen($name)) { + return $name; + } else { + return null; + } + } + +} diff --git a/src/applications/diffusion/data/DiffusionGitBranch.php b/src/applications/diffusion/data/DiffusionGitBranch.php index b1456c6102..925ede568c 100644 --- a/src/applications/diffusion/data/DiffusionGitBranch.php +++ b/src/applications/diffusion/data/DiffusionGitBranch.php @@ -2,6 +2,8 @@ final class DiffusionGitBranch { + const DEFAULT_GIT_REMOTE = 'origin'; + /** * Parse the output of 'git branch -r --verbose --no-abbrev' or similar into * a map. For instance: diff --git a/src/applications/diffusion/data/DiffusionRepositoryRef.php b/src/applications/diffusion/data/DiffusionRepositoryRef.php index b19c6e5cdc..45c92c8c0a 100644 --- a/src/applications/diffusion/data/DiffusionRepositoryRef.php +++ b/src/applications/diffusion/data/DiffusionRepositoryRef.php @@ -1,10 +1,13 @@ rawFields = $raw_fields; @@ -33,4 +36,31 @@ final class DiffusionRepositoryRef { return $this->shortName; } + +/* -( Serialization )------------------------------------------------------ */ + + + public function toDictionary() { + return array( + 'shortName' => $this->shortName, + 'commitIdentifier' => $this->commitIdentifier, + 'rawFields' => $this->rawFields, + ); + } + + public static function newFromDictionary(array $dict) { + return id(new DiffusionRepositoryRef()) + ->setShortName($dict['shortName']) + ->setCommitIdentifier($dict['commitIdentifier']) + ->setRawFields($dict['rawFields']); + } + + public static function loadAllFromDictionaries(array $dictionaries) { + $refs = array(); + foreach ($dictionaries as $dictionary) { + $refs[] = self::newFromDictionary($dictionary); + } + return $refs; + } + } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php new file mode 100644 index 0000000000..a3fa60477b --- /dev/null +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -0,0 +1,1104 @@ +remoteProtocol = $remote_protocol; + return $this; + } + + public function getRemoteProtocol() { + return $this->remoteProtocol; + } + + public function setRemoteAddress($remote_address) { + $this->remoteAddress = $remote_address; + return $this; + } + + public function getRemoteAddress() { + return $this->remoteAddress; + } + + private function getRemoteAddressForLog() { + // If whatever we have here isn't a valid IPv4 address, just store `null`. + // Older versions of PHP return `-1` on failure instead of `false`. + $remote_address = $this->getRemoteAddress(); + $remote_address = max(0, ip2long($remote_address)); + $remote_address = nonempty($remote_address, null); + return $remote_address; + } + + private function getTransactionKey() { + if (!$this->transactionKey) { + $entropy = Filesystem::readRandomBytes(64); + $this->transactionKey = PhabricatorHash::digestForIndex($entropy); + } + return $this->transactionKey; + } + + public function setSubversionTransactionInfo($transaction, $repository) { + $this->subversionTransaction = $transaction; + $this->subversionRepository = $repository; + return $this; + } + + public function setStdin($stdin) { + $this->stdin = $stdin; + return $this; + } + + public function getStdin() { + return $this->stdin; + } + + public function setOriginalArgv(array $original_argv) { + $this->originalArgv = $original_argv; + return $this; + } + + public function getOriginalArgv() { + return $this->originalArgv; + } + + public function setRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->repository; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setMercurialHook($mercurial_hook) { + $this->mercurialHook = $mercurial_hook; + return $this; + } + + public function getMercurialHook() { + return $this->mercurialHook; + } + + +/* -( Hook Execution )----------------------------------------------------- */ + + + public function execute() { + $ref_updates = $this->findRefUpdates(); + $all_updates = $ref_updates; + + $caught = null; + try { + + try { + $this->rejectDangerousChanges($ref_updates); + } catch (DiffusionCommitHookRejectException $ex) { + // If we're rejecting dangerous changes, flag everything that we've + // seen as rejected so it's clear that none of it was accepted. + foreach ($all_updates as $update) { + $update->setRejectCode( + PhabricatorRepositoryPushLog::REJECT_DANGEROUS); + } + throw $ex; + } + + $this->applyHeraldRefRules($ref_updates, $all_updates); + + $content_updates = $this->findContentUpdates($ref_updates); + $all_updates = array_merge($all_updates, $content_updates); + + $this->applyHeraldContentRules($content_updates, $all_updates); + + // Run custom scripts in `hook.d/` directories. + $this->applyCustomHooks($all_updates); + + // If we make it this far, we're accepting these changes. Mark all the + // logs as accepted. + foreach ($all_updates as $update) { + $update->setRejectCode(PhabricatorRepositoryPushLog::REJECT_ACCEPT); + } + } catch (Exception $ex) { + // We'll throw this again in a minute, but we want to save all the logs + // first. + $caught = $ex; + } + + // Save all the logs no matter what the outcome was. + foreach ($all_updates as $update) { + $update->save(); + } + + if ($caught) { + throw $caught; + } + + return 0; + } + + private function findRefUpdates() { + $type = $this->getRepository()->getVersionControlSystem(); + switch ($type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + return $this->findGitRefUpdates(); + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + return $this->findMercurialRefUpdates(); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + return $this->findSubversionRefUpdates(); + default: + throw new Exception(pht('Unsupported repository type "%s"!', $type)); + } + } + + private function rejectDangerousChanges(array $ref_updates) { + assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); + + $repository = $this->getRepository(); + if ($repository->shouldAllowDangerousChanges()) { + return; + } + + $flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; + + foreach ($ref_updates as $ref_update) { + if (!$ref_update->hasChangeFlags($flag_dangerous)) { + // This is not a dangerous change. + continue; + } + + // We either have a branch deletion or a non fast-forward branch update. + // Format a message and reject the push. + + $message = pht( + "DANGEROUS CHANGE: %s\n". + "Dangerous change protection is enabled for this repository.\n". + "Edit the repository configuration before making dangerous changes.", + $ref_update->getDangerousChangeDescription()); + + throw new DiffusionCommitHookRejectException($message); + } + } + + private function findContentUpdates(array $ref_updates) { + assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); + + $type = $this->getRepository()->getVersionControlSystem(); + switch ($type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + return $this->findGitContentUpdates($ref_updates); + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + return $this->findMercurialContentUpdates($ref_updates); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + return $this->findSubversionContentUpdates($ref_updates); + default: + throw new Exception(pht('Unsupported repository type "%s"!', $type)); + } + } + + +/* -( Herald )------------------------------------------------------------- */ + + private function applyHeraldRefRules( + array $ref_updates, + array $all_updates) { + $this->applyHeraldRules( + $ref_updates, + new HeraldPreCommitRefAdapter(), + $all_updates); + } + + private function applyHeraldContentRules( + array $content_updates, + array $all_updates) { + $this->applyHeraldRules( + $content_updates, + new HeraldPreCommitContentAdapter(), + $all_updates); + } + + private function applyHeraldRules( + array $updates, + HeraldAdapter $adapter_template, + array $all_updates) { + + if (!$updates) { + return; + } + + $adapter_template->setHookEngine($this); + + $engine = new HeraldEngine(); + $rules = null; + $blocking_effect = null; + $blocked_update = null; + foreach ($updates as $update) { + $adapter = id(clone $adapter_template) + ->setPushLog($update); + + if ($rules === null) { + $rules = $engine->loadRulesForAdapter($adapter); + } + + $effects = $engine->applyRules($rules, $adapter); + $engine->applyEffects($effects, $adapter, $rules); + $xscript = $engine->getTranscript(); + + if ($blocking_effect === null) { + foreach ($effects as $effect) { + if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { + $blocking_effect = $effect; + $blocked_update = $update; + break; + } + } + } + } + + if ($blocking_effect) { + foreach ($all_updates as $update) { + $update->setRejectCode(PhabricatorRepositoryPushLog::REJECT_HERALD); + $update->setRejectDetails($blocking_effect->getRulePHID()); + } + + $message = $blocking_effect->getTarget(); + if (!strlen($message)) { + $message = pht('(None.)'); + } + + $rules = mpull($rules, null, 'getID'); + $rule = idx($rules, $effect->getRuleID()); + if ($rule && strlen($rule->getName())) { + $rule_name = $rule->getName(); + } else { + $rule_name = pht('Unnamed Herald Rule'); + } + + $blocked_ref_name = coalesce( + $blocked_update->getRefName(), + $blocked_update->getRefNewShort()); + $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name; + + throw new DiffusionCommitHookRejectException( + pht( + "This push was rejected by Herald push rule %s.\n". + "Change: %s\n". + " Rule: %s\n". + "Reason: %s", + 'H'.$blocking_effect->getRuleID(), + $blocked_name, + $rule_name, + $message)); + } + } + + public function loadViewerProjectPHIDsForHerald() { + // This just caches the viewer's projects so we don't need to load them + // over and over again when applying Herald rules. + if ($this->heraldViewerProjects === null) { + $this->heraldViewerProjects = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withMemberPHIDs(array($this->getViewer()->getPHID())) + ->execute(); + } + + return mpull($this->heraldViewerProjects, 'getPHID'); + } + + +/* -( Git )---------------------------------------------------------------- */ + + + private function findGitRefUpdates() { + $ref_updates = array(); + + // First, parse stdin, which lists all the ref changes. The input looks + // like this: + // + // + + $stdin = $this->getStdin(); + $lines = phutil_split_lines($stdin, $retain_endings = false); + foreach ($lines as $line) { + $parts = explode(' ', $line, 3); + if (count($parts) != 3) { + throw new Exception(pht('Expected "old new ref", got "%s".', $line)); + } + + $ref_old = $parts[0]; + $ref_new = $parts[1]; + $ref_raw = $parts[2]; + + if (preg_match('(^refs/heads/)', $ref_raw)) { + $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; + $ref_raw = substr($ref_raw, strlen('refs/heads/')); + } else if (preg_match('(^refs/tags/)', $ref_raw)) { + $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG; + $ref_raw = substr($ref_raw, strlen('refs/tags/')); + } else { + throw new Exception( + pht( + "Unable to identify the reftype of '%s'. Rejecting push.", + $ref_raw)); + } + + $ref_update = $this->newPushLog() + ->setRefType($ref_type) + ->setRefName($ref_raw) + ->setRefOld($ref_old) + ->setRefNew($ref_new); + + $ref_updates[] = $ref_update; + } + + $this->findGitMergeBases($ref_updates); + $this->findGitChangeFlags($ref_updates); + + return $ref_updates; + } + + + private function findGitMergeBases(array $ref_updates) { + assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); + + $futures = array(); + foreach ($ref_updates as $key => $ref_update) { + // If the old hash is "00000...", the ref is being created (either a new + // branch, or a new tag). If the new hash is "00000...", the ref is being + // deleted. If both are nonempty, the ref is being updated. For updates, + // we'll figure out the `merge-base` of the old and new objects here. This + // lets us reject non-FF changes cheaply; later, we'll figure out exactly + // which commits are new. + $ref_old = $ref_update->getRefOld(); + $ref_new = $ref_update->getRefNew(); + + if (($ref_old === self::EMPTY_HASH) || + ($ref_new === self::EMPTY_HASH)) { + continue; + } + + $futures[$key] = $this->getRepository()->getLocalCommandFuture( + 'merge-base %s %s', + $ref_old, + $ref_new); + } + + foreach (Futures($futures)->limit(8) as $key => $future) { + + // If 'old' and 'new' have no common ancestors (for example, a force push + // which completely rewrites a ref), `git merge-base` will exit with + // an error and no output. It would be nice to find a positive test + // for this instead, but I couldn't immediately come up with one. See + // T4224. Assume this means there are no ancestors. + + list($err, $stdout) = $future->resolve(); + + if ($err) { + $merge_base = null; + } else { + $merge_base = rtrim($stdout, "\n"); + } + + $ref_update = $ref_updates[$key]; + $ref_update->setMergeBase($merge_base); + } + + return $ref_updates; + } + + + private function findGitChangeFlags(array $ref_updates) { + assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); + + foreach ($ref_updates as $key => $ref_update) { + $ref_old = $ref_update->getRefOld(); + $ref_new = $ref_update->getRefNew(); + $ref_type = $ref_update->getRefType(); + + $ref_flags = 0; + $dangerous = null; + + if ($ref_old === self::EMPTY_HASH) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; + } else if ($ref_new === self::EMPTY_HASH) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; + if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; + $dangerous = pht( + "The change you're attempting to push deletes the branch '%s'.", + $ref_update->getRefName()); + } + } else { + $merge_base = $ref_update->getMergeBase(); + if ($merge_base == $ref_old) { + // This is a fast-forward update to an existing branch. + // These are safe. + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; + } else { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; + + // For now, we don't consider deleting or moving tags to be a + // "dangerous" update. It's way harder to get wrong and should be easy + // to recover from once we have better logging. Only add the dangerous + // flag if this ref is a branch. + + if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; + + $dangerous = pht( + "The change you're attempting to push updates the branch '%s' ". + "from '%s' to '%s', but this is not a fast-forward. Pushes ". + "which rewrite published branch history are dangerous.", + $ref_update->getRefName(), + $ref_update->getRefOldShort(), + $ref_update->getRefNewShort()); + } + } + } + + $ref_update->setChangeFlags($ref_flags); + if ($dangerous !== null) { + $ref_update->attachDangerousChangeDescription($dangerous); + } + } + + return $ref_updates; + } + + + private function findGitContentUpdates(array $ref_updates) { + $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; + + $futures = array(); + foreach ($ref_updates as $key => $ref_update) { + if ($ref_update->hasChangeFlags($flag_delete)) { + // Deleting a branch or tag can never create any new commits. + continue; + } + + // NOTE: This piece of magic finds all new commits, by walking backward + // from the new value to the value of *any* existing ref in the + // repository. Particularly, this will cover the cases of a new branch, a + // completely moved tag, etc. + $futures[$key] = $this->getRepository()->getLocalCommandFuture( + 'log --format=%s %s --not --all', + '%H', + $ref_update->getRefNew()); + } + + $content_updates = array(); + foreach (Futures($futures)->limit(8) as $key => $future) { + list($stdout) = $future->resolvex(); + + if (!strlen(trim($stdout))) { + // This change doesn't have any new commits. One common case of this + // is creating a new tag which points at an existing commit. + continue; + } + + $commits = phutil_split_lines($stdout, $retain_newlines = false); + + // If we're looking at a branch, mark all of the new commits as on that + // branch. It's only possible for these commits to be on updated branches, + // since any other branch heads are necessarily behind them. + $branch_name = null; + $ref_update = $ref_updates[$key]; + $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; + if ($ref_update->getRefType() == $type_branch) { + $branch_name = $ref_update->getRefName(); + } + + foreach ($commits as $commit) { + if ($branch_name) { + $this->gitCommits[$commit][] = $branch_name; + } + $content_updates[$commit] = $this->newPushLog() + ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) + ->setRefNew($commit) + ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); + } + } + + return $content_updates; + } + +/* -( Custom )------------------------------------------------------------- */ + + private function applyCustomHooks(array $updates) { + $args = $this->getOriginalArgv(); + $stdin = $this->getStdin(); + $console = PhutilConsole::getConsole(); + + $env = array( + 'PHABRICATOR_REPOSITORY' => $this->getRepository()->getCallsign(), + self::ENV_USER => $this->getViewer()->getUsername(), + self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), + self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), + ); + + $directories = $this->getRepository()->getHookDirectories(); + foreach ($directories as $directory) { + $hooks = $this->getExecutablesInDirectory($directory); + sort($hooks); + foreach ($hooks as $hook) { + // NOTE: We're explicitly running the hooks in sequential order to + // make this more predictable. + $future = id(new ExecFuture('%s %Ls', $hook, $args)) + ->setEnv($env, $wipe_process_env = false) + ->write($stdin); + + list($err, $stdout, $stderr) = $future->resolve(); + if (!$err) { + // This hook ran OK, but echo its output in case there was something + // informative. + $console->writeOut("%s", $stdout); + $console->writeErr("%s", $stderr); + continue; + } + + // Mark everything as rejected by this hook. + foreach ($updates as $update) { + $update->setRejectCode( + PhabricatorRepositoryPushLog::REJECT_EXTERNAL); + $update->setRejectDetails(basename($hook)); + } + + throw new DiffusionCommitHookRejectException( + pht( + "This push was rejected by custom hook script '%s':\n\n%s%s", + basename($hook), + $stdout, + $stderr)); + } + } + } + + private function getExecutablesInDirectory($directory) { + $executables = array(); + + if (!Filesystem::pathExists($directory)) { + return $executables; + } + + foreach (Filesystem::listDirectory($directory) as $path) { + $full_path = $directory.DIRECTORY_SEPARATOR.$path; + if (is_executable($full_path)) { + $executables[] = $full_path; + } + } + + return $executables; + } + + +/* -( Mercurial )---------------------------------------------------------- */ + + + private function findMercurialRefUpdates() { + $hook = $this->getMercurialHook(); + switch ($hook) { + case 'pretxnchangegroup': + return $this->findMercurialChangegroupRefUpdates(); + case 'prepushkey': + return $this->findMercurialPushKeyRefUpdates(); + default: + throw new Exception(pht('Unrecognized hook "%s"!', $hook)); + } + } + + private function findMercurialChangegroupRefUpdates() { + $hg_node = getenv('HG_NODE'); + if (!$hg_node) { + throw new Exception(pht('Expected HG_NODE in environment!')); + } + + // NOTE: We need to make sure this is passed to subprocesses, or they won't + // be able to see new commits. Mercurial uses this as a marker to determine + // whether the pending changes are visible or not. + $_ENV['HG_PENDING'] = getenv('HG_PENDING'); + $repository = $this->getRepository(); + + $futures = array(); + + foreach (array('old', 'new') as $key) { + $futures[$key] = $repository->getLocalCommandFuture( + 'heads --template %s', + '{node}\1{branches}\2'); + } + // Wipe HG_PENDING out of the old environment so we see the pre-commit + // state of the repository. + $futures['old']->updateEnv('HG_PENDING', null); + + $futures['commits'] = $repository->getLocalCommandFuture( + "log --rev %s --rev tip --template %s", + hgsprintf('%s', $hg_node), + '{node}\1{branches}\2'); + + // Resolve all of the futures now. We don't need the 'commits' future yet, + // but it simplifies the logic to just get it out of the way. + foreach (Futures($futures) as $future) { + $future->resolve(); + } + + list($commit_raw) = $futures['commits']->resolvex(); + $commit_map = $this->parseMercurialCommits($commit_raw); + $this->mercurialCommits = $commit_map; + + // NOTE: `hg heads` exits with an error code and no output if the repository + // has no heads. Most commonly this happens on a new repository. We know + // we can run `hg` successfully since the `hg log` above didn't error, so + // just ignore the error code. + + list($err, $old_raw) = $futures['old']->resolve(); + $old_refs = $this->parseMercurialHeads($old_raw); + + list($err, $new_raw) = $futures['new']->resolve(); + $new_refs = $this->parseMercurialHeads($new_raw); + + $all_refs = array_keys($old_refs + $new_refs); + + $ref_updates = array(); + foreach ($all_refs as $ref) { + $old_heads = idx($old_refs, $ref, array()); + $new_heads = idx($new_refs, $ref, array()); + + sort($old_heads); + sort($new_heads); + + if ($old_heads === $new_heads) { + // No changes to this branch, so skip it. + continue; + } + + if (!$new_heads) { + if ($old_heads) { + // It looks like this push deletes a branch, but that isn't possible + // in Mercurial, so something is going wrong here. Bail out. + throw new Exception( + pht( + 'Mercurial repository has no new head for branch "%s" after '. + 'push. This is unexpected; rejecting change.')); + } else { + // Obviously, this should never be possible either, as it makes + // no sense. Explode. + throw new Exception( + pht( + 'Mercurial repository has no new or old heads for branch "%s" '. + 'after push. This makes no sense; rejecting change.')); + } + } + + $stray_heads = array(); + if (count($old_heads) > 1) { + // HORRIBLE: In Mercurial, branches can have multiple heads. If the + // old branch had multiple heads, we need to figure out which new + // heads descend from which old heads, so we can tell whether you're + // actively creating new heads (dangerous) or just working in a + // repository that's already full of garbage (strongly discouraged but + // not as inherently dangerous). These cases should be very uncommon. + + $dfutures = array(); + foreach ($old_heads as $old_head) { + $dfutures[$old_head] = $repository->getLocalCommandFuture( + 'log --rev %s --template %s', + hgsprintf('(descendants(%s) and head())', $old_head), + '{node}\1'); + } + + $head_map = array(); + foreach (Futures($dfutures) as $future_head => $dfuture) { + list($stdout) = $dfuture->resolvex(); + $head_map[$future_head] = array_filter(explode("\1", $stdout)); + } + + // Now, find all the new stray heads this push creates, if any. These + // are new heads which do not descend from the old heads. + $seen = array_fuse(array_mergev($head_map)); + foreach ($new_heads as $new_head) { + if (empty($seen[$new_head])) { + $head_map[self::EMPTY_HASH][] = $new_head; + } + } + } else if ($old_heads) { + $head_map[head($old_heads)] = $new_heads; + } else { + $head_map[self::EMPTY_HASH] = $new_heads; + } + + foreach ($head_map as $old_head => $child_heads) { + foreach ($child_heads as $new_head) { + if ($new_head === $old_head) { + continue; + } + + $ref_flags = 0; + $dangerous = null; + if ($old_head == self::EMPTY_HASH) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; + } else { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; + } + + $splits_existing_head = (count($child_heads) > 1); + $creates_duplicate_head = ($old_head == self::EMPTY_HASH) && + (count($head_map) > 1); + + if ($splits_existing_head || $creates_duplicate_head) { + $readable_child_heads = array(); + foreach ($child_heads as $child_head) { + $readable_child_heads[] = substr($child_head, 0, 12); + } + + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; + + if ($splits_existing_head) { + // We're splitting an existing head into two or more heads. + // This is dangerous, and a super bad idea. Note that we're only + // raising this if you're actively splitting a branch head. If a + // head split in the past, we don't consider appends to it + // to be dangerous. + $dangerous = pht( + "The change you're attempting to push splits the head of ". + "branch '%s' into multiple heads: %s. This is inadvisable ". + "and dangerous.", + $ref, + implode(', ', $readable_child_heads)); + } else { + // We're adding a second (or more) head to a branch. The new + // head is not a descendant of any old head. + $dangerous = pht( + "The change you're attempting to push creates new, divergent ". + "heads for the branch '%s': %s. This is inadvisable and ". + "dangerous.", + $ref, + implode(', ', $readable_child_heads)); + } + } + + $ref_update = $this->newPushLog() + ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH) + ->setRefName($ref) + ->setRefOld($old_head) + ->setRefNew($new_head) + ->setChangeFlags($ref_flags); + + if ($dangerous !== null) { + $ref_update->attachDangerousChangeDescription($dangerous); + } + + $ref_updates[] = $ref_update; + } + } + } + + return $ref_updates; + } + + private function findMercurialPushKeyRefUpdates() { + $key_namespace = getenv('HG_NAMESPACE'); + + if ($key_namespace === 'phases') { + // Mercurial changes commit phases as part of normal push operations. We + // just ignore these, as they don't seem to represent anything + // interesting. + return array(); + } + + $key_name = getenv('HG_KEY'); + + $key_old = getenv('HG_OLD'); + if (!strlen($key_old)) { + $key_old = null; + } + + $key_new = getenv('HG_NEW'); + if (!strlen($key_new)) { + $key_new = null; + } + + if ($key_namespace !== 'bookmarks') { + throw new Exception( + pht( + "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ". + "Rejecting push.", + $key_namespace, + $key_name, + coalesce($key_old, pht('null')), + coalesce($key_new, pht('null')))); + } + + if ($key_old === $key_new) { + // We get a callback when the bookmark doesn't change. Just ignore this, + // as it's a no-op. + return array(); + } + + $ref_flags = 0; + $merge_base = null; + if ($key_old === null) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; + } else if ($key_new === null) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; + } else { + list($merge_base_raw) = $this->getRepository()->execxLocalCommand( + 'log --template %s --rev %s', + '{node}', + hgsprintf('ancestor(%s, %s)', $key_old, $key_new)); + + if (strlen(trim($merge_base_raw))) { + $merge_base = trim($merge_base_raw); + } + + if ($merge_base && ($merge_base === $key_old)) { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; + } else { + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; + } + } + + $ref_update = $this->newPushLog() + ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK) + ->setRefName($key_name) + ->setRefOld(coalesce($key_old, self::EMPTY_HASH)) + ->setRefNew(coalesce($key_new, self::EMPTY_HASH)) + ->setChangeFlags($ref_flags); + + return array($ref_update); + } + + private function findMercurialContentUpdates(array $ref_updates) { + $content_updates = array(); + + foreach ($this->mercurialCommits as $commit => $branches) { + $content_updates[$commit] = $this->newPushLog() + ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) + ->setRefNew($commit) + ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); + } + + return $content_updates; + } + + private function parseMercurialCommits($raw) { + $commits_lines = explode("\2", $raw); + $commits_lines = array_filter($commits_lines); + $commit_map = array(); + foreach ($commits_lines as $commit_line) { + list($node, $branches_raw) = explode("\1", $commit_line); + + if (!strlen($branches_raw)) { + $branches = array('default'); + } else { + $branches = explode(' ', $branches_raw); + } + + $commit_map[$node] = $branches; + } + + return $commit_map; + } + + private function parseMercurialHeads($raw) { + $heads_map = $this->parseMercurialCommits($raw); + + $heads = array(); + foreach ($heads_map as $commit => $branches) { + foreach ($branches as $branch) { + $heads[$branch][] = $commit; + } + } + + return $heads; + } + + +/* -( Subversion )--------------------------------------------------------- */ + + + private function findSubversionRefUpdates() { + // Subversion doesn't have any kind of mutable ref metadata. + return array(); + } + + private function findSubversionContentUpdates(array $ref_updates) { + list($youngest) = execx( + 'svnlook youngest %s', + $this->subversionRepository); + $ref_new = (int)$youngest + 1; + + $ref_flags = 0; + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; + $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; + + $ref_content = $this->newPushLog() + ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) + ->setRefNew($ref_new) + ->setChangeFlags($ref_flags); + + return array($ref_content); + } + + +/* -( Internals )---------------------------------------------------------- */ + + + private function newPushLog() { + // NOTE: By default, we create these with REJECT_BROKEN as the reject + // code. This indicates a broken hook, and covers the case where we + // encounter some unexpected exception and consequently reject the changes. + + // NOTE: We generate PHIDs up front so the Herald transcripts can pick them + // up. + $phid = id(new PhabricatorRepositoryPushLog())->generatePHID(); + + return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) + ->setPHID($phid) + ->attachRepository($this->getRepository()) + ->setRepositoryPHID($this->getRepository()->getPHID()) + ->setEpoch(time()) + ->setRemoteAddress($this->getRemoteAddressForLog()) + ->setRemoteProtocol($this->getRemoteProtocol()) + ->setTransactionKey($this->getTransactionKey()) + ->setRejectCode(PhabricatorRepositoryPushLog::REJECT_BROKEN) + ->setRejectDetails(null); + } + + public function loadChangesetsForCommit($identifier) { + $byte_limit = HeraldCommitAdapter::getEnormousByteLimit(); + $time_limit = HeraldCommitAdapter::getEnormousTimeLimit(); + + $vcs = $this->getRepository()->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // For git and hg, we can use normal commands. + $drequest = DiffusionRequest::newFromDictionary( + array( + 'repository' => $this->getRepository(), + 'user' => $this->getViewer(), + 'commit' => $identifier, + )); + + $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) + ->setTimeout($time_limit) + ->setByteLimit($byte_limit) + ->setLinesOfContext(0) + ->loadRawDiff(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // TODO: This diff has 3 lines of context, which produces slightly + // incorrect "added file content" and "removed file content" results. + // This may also choke on binaries, but "svnlook diff" does not support + // the "--diff-cmd" flag. + + // For subversion, we need to use `svnlook`. + $future = new ExecFuture( + 'svnlook diff -t %s %s', + $this->subversionTransaction, + $this->subversionRepository); + + $future->setTimeout($time_limit); + $future->setStdoutSizeLimit($byte_limit); + $future->setStderrSizeLimit($byte_limit); + + list($raw_diff) = $future->resolvex(); + break; + default: + throw new Exception(pht("Unknown VCS '%s!'", $vcs)); + } + + if (strlen($raw_diff) >= $byte_limit) { + throw new Exception( + pht( + 'The raw text of this change is enormous (larger than %d '. + 'bytes). Herald can not process it.', + $byte_limit)); + } + + $parser = new ArcanistDiffParser(); + $changes = $parser->parseDiff($raw_diff); + $diff = DifferentialDiff::newFromRawChanges($changes); + return $diff->getChangesets(); + } + + public function loadCommitRefForCommit($identifier) { + $repository = $this->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + return id(new DiffusionLowLevelCommitQuery()) + ->setRepository($repository) + ->withIdentifier($identifier) + ->execute(); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // For subversion, we need to use `svnlook`. + list($message) = execx( + 'svnlook log -t %s %s', + $this->subversionTransaction, + $this->subversionRepository); + + return id(new DiffusionCommitRef()) + ->setMessage($message); + break; + default: + throw new Exception(pht("Unknown VCS '%s!'", $vcs)); + } + } + + public function loadBranches($identifier) { + $repository = $this->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + return idx($this->gitCommits, $identifier, array()); + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + return idx($this->mercurialCommits, $identifier, array()); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // Subversion doesn't have branches. + return array(); + } + } + + +} diff --git a/src/applications/diffusion/exception/DiffusionCommitHookRejectException.php b/src/applications/diffusion/exception/DiffusionCommitHookRejectException.php new file mode 100644 index 0000000000..9860bda7d9 --- /dev/null +++ b/src/applications/diffusion/exception/DiffusionCommitHookRejectException.php @@ -0,0 +1,5 @@ +log = $log; + return $this; + } + + public function setHookEngine(DiffusionCommitHookEngine $engine) { + $this->hookEngine = $engine; + return $this; + } + + public function getHookEngine() { + return $this->hookEngine; + } + + public function getAdapterApplicationClass() { + return 'PhabricatorApplicationDiffusion'; + } + + public function getObject() { + return $this->log; + } + + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + return true; + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + default: + return false; + } + } + + public function canTriggerOnObject($object) { + if ($object instanceof PhabricatorRepository) { + return true; + } + + if ($object instanceof PhabricatorProject) { + return true; + } + + return false; + } + + public function explainValidTriggerObjects() { + return pht( + 'This rule can trigger for **repositories** or **projects**.'); + } + + public function getTriggerObjectPHIDs() { + return array_merge( + array( + $this->hookEngine->getRepository()->getPHID(), + $this->getPHID(), + ), + $this->hookEngine->getRepository()->getProjectPHIDs()); + } + + public function getActions($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + return array( + self::ACTION_BLOCK, + self::ACTION_NOTHING + ); + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return array( + self::ACTION_NOTHING, + ); + } + } + + public function getPHID() { + return $this->getObject()->getPHID(); + } + + public function applyHeraldEffects(array $effects) { + assert_instances_of($effects, 'HeraldEffect'); + + $result = array(); + foreach ($effects as $effect) { + $action = $effect->getAction(); + switch ($action) { + case self::ACTION_NOTHING: + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Did nothing.')); + break; + case self::ACTION_BLOCK: + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Blocked push.')); + break; + default: + throw new Exception(pht('No rules to handle action "%s"!', $action)); + } + } + + return $result; + } + +} diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php new file mode 100644 index 0000000000..a7ea3844ff --- /dev/null +++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php @@ -0,0 +1,317 @@ +getObject(); + switch ($field) { + case self::FIELD_BODY: + return $this->getCommitRef()->getMessage(); + case self::FIELD_AUTHOR: + return $this->getAuthorPHID(); + case self::FIELD_AUTHOR_RAW: + return $this->getAuthorRaw(); + case self::FIELD_COMMITTER: + return $this->getCommitterPHID(); + case self::FIELD_COMMITTER_RAW: + return $this->getCommitterRaw(); + case self::FIELD_BRANCHES: + return $this->getBranches(); + case self::FIELD_DIFF_FILE: + return $this->getDiffContent('name'); + case self::FIELD_DIFF_CONTENT: + return $this->getDiffContent('*'); + case self::FIELD_DIFF_ADDED_CONTENT: + return $this->getDiffContent('+'); + case self::FIELD_DIFF_REMOVED_CONTENT: + return $this->getDiffContent('-'); + case self::FIELD_DIFF_ENORMOUS: + $this->getDiffContent('*'); + return ($this->changesets instanceof Exception); + case self::FIELD_REPOSITORY: + return $this->getHookEngine()->getRepository()->getPHID(); + case self::FIELD_REPOSITORY_PROJECTS: + return $this->getHookEngine()->getRepository()->getProjectPHIDs(); + case self::FIELD_PUSHER: + return $this->getHookEngine()->getViewer()->getPHID(); + case self::FIELD_PUSHER_PROJECTS: + return $this->getHookEngine()->loadViewerProjectPHIDsForHerald(); + case self::FIELD_DIFFERENTIAL_REVISION: + $revision = $this->getRevision(); + if (!$revision) { + return null; + } + return $revision->getPHID(); + case self::FIELD_DIFFERENTIAL_ACCEPTED: + $revision = $this->getRevision(); + if (!$revision) { + return null; + } + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; + if ($revision->getStatus() != $status_accepted) { + return null; + } + return $revision->getPHID(); + case self::FIELD_DIFFERENTIAL_REVIEWERS: + $revision = $this->getRevision(); + if (!$revision) { + return array(); + } + return $revision->getReviewers(); + case self::FIELD_DIFFERENTIAL_CCS: + $revision = $this->getRevision(); + if (!$revision) { + return array(); + } + return $revision->getCCPHIDs(); + case self::FIELD_IS_MERGE_COMMIT: + return $this->getIsMergeCommit(); + } + + return parent::getHeraldField($field); + } + + private function getDiffContent($type) { + if ($this->changesets === null) { + try { + $this->changesets = $this->getHookEngine()->loadChangesetsForCommit( + $this->getObject()->getRefNew()); + } catch (Exception $ex) { + $this->changesets = $ex; + } + } + + if ($this->changesets instanceof Exception) { + $ex_class = get_class($this->changesets); + $ex_message = $this->changesets->getmessage(); + if ($type === 'name') { + return array("<{$ex_class}: {$ex_message}>"); + } else { + return array("<{$ex_class}>" => $ex_message); + } + } + + $result = array(); + if ($type === 'name') { + foreach ($this->changesets as $change) { + $result[] = $change->getFilename(); + } + } else { + foreach ($this->changesets as $change) { + $lines = array(); + foreach ($change->getHunks() as $hunk) { + switch ($type) { + case '-': + $lines[] = $hunk->makeOldFile(); + break; + case '+': + $lines[] = $hunk->makeNewFile(); + break; + case '*': + default: + $lines[] = $hunk->makeChanges(); + break; + } + } + $result[$change->getFilename()] = implode('', $lines); + } + } + + return $result; + } + + private function getCommitRef() { + if ($this->commitRef === null) { + $this->commitRef = $this->getHookEngine()->loadCommitRefForCommit( + $this->getObject()->getRefNew()); + } + return $this->commitRef; + } + + private function getAuthorPHID() { + $repository = $this->getHookEngine()->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $ref = $this->getCommitRef(); + $author = $ref->getAuthor(); + if (!strlen($author)) { + return null; + } + return $this->lookupUser($author); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // In Subversion, the pusher is always the author. + return $this->getHookEngine()->getViewer()->getPHID(); + } + } + + private function getCommitterPHID() { + $repository = $this->getHookEngine()->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // Here, if there's no committer, we're going to return the author + // instead. + $ref = $this->getCommitRef(); + $committer = $ref->getCommitter(); + if (!strlen($committer)) { + return $this->getAuthorPHID(); + } + $phid = $this->lookupUser($committer); + if (!$phid) { + return $this->getAuthorPHID(); + } + return $phid; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // In Subversion, the pusher is always the committer. + return $this->getHookEngine()->getViewer()->getPHID(); + } + } + + private function getAuthorRaw() { + $repository = $this->getHookEngine()->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $ref = $this->getCommitRef(); + return $ref->getAuthor(); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // In Subversion, the pusher is always the author. + return $this->getHookEngine()->getViewer()->getUsername(); + } + } + + private function getCommitterRaw() { + $repository = $this->getHookEngine()->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // Here, if there's no committer, we're going to return the author + // instead. + $ref = $this->getCommitRef(); + $committer = $ref->getCommitter(); + if (strlen($committer)) { + return $committer; + } + return $ref->getAuthor(); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // In Subversion, the pusher is always the committer. + return $this->getHookEngine()->getViewer()->getUsername(); + } + } + + private function lookupUser($author) { + return id(new DiffusionResolveUserQuery()) + ->withName($author) + ->execute(); + } + + private function getCommitFields() { + if ($this->fields === null) { + $this->fields = id(new DiffusionLowLevelCommitFieldsQuery()) + ->setRepository($this->getHookEngine()->getRepository()) + ->withCommitRef($this->getCommitRef()) + ->execute(); + } + return $this->fields; + } + + private function getRevision() { + if ($this->revision === false) { + $fields = $this->getCommitFields(); + $revision_id = idx($fields, 'revisionID'); + if (!$revision_id) { + $this->revision = null; + } else { + $this->revision = id(new DifferentialRevisionQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($revision_id)) + ->needRelationships(true) + ->executeOne(); + } + } + + return $this->revision; + } + + private function getIsMergeCommit() { + $repository = $this->getHookEngine()->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $parents = id(new DiffusionLowLevelParentsQuery()) + ->setRepository($repository) + ->withIdentifier($this->getObject()->getRefNew()) + ->execute(); + + return (count($parents) > 1); + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // NOTE: For now, we ignore "svn:mergeinfo" at all levels. We might + // change this some day, but it's not nearly as clear a signal as + // ancestry is in Git/Mercurial. + return false; + } + } + + private function getBranches() { + return $this->getHookEngine()->loadBranches( + $this->getObject()->getRefNew()); + } + +} diff --git a/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php new file mode 100644 index 0000000000..75ae07a5e9 --- /dev/null +++ b/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php @@ -0,0 +1,109 @@ + pht('Ref type'), + self::FIELD_REF_NAME => pht('Ref name'), + self::FIELD_REF_CHANGE => pht('Ref change type'), + ) + parent::getFieldNameMap(); + } + + public function getFields() { + return array_merge( + array( + self::FIELD_REF_TYPE, + self::FIELD_REF_NAME, + self::FIELD_REF_CHANGE, + self::FIELD_REPOSITORY, + self::FIELD_REPOSITORY_PROJECTS, + self::FIELD_PUSHER, + self::FIELD_PUSHER_PROJECTS, + self::FIELD_RULE, + ), + parent::getFields()); + } + + public function getConditionsForField($field) { + switch ($field) { + case self::FIELD_REF_NAME: + return array( + self::CONDITION_IS, + self::CONDITION_IS_NOT, + self::CONDITION_CONTAINS, + self::CONDITION_REGEXP, + ); + case self::FIELD_REF_TYPE: + return array( + self::CONDITION_IS, + self::CONDITION_IS_NOT, + ); + case self::FIELD_REF_CHANGE: + return array( + self::CONDITION_HAS_BIT, + self::CONDITION_NOT_BIT, + ); + } + return parent::getConditionsForField($field); + } + + public function getValueTypeForFieldAndCondition($field, $condition) { + switch ($field) { + case self::FIELD_REF_TYPE: + return self::VALUE_REF_TYPE; + case self::FIELD_REF_CHANGE: + return self::VALUE_REF_CHANGE; + } + + return parent::getValueTypeForFieldAndCondition($field, $condition); + } + + public function getHeraldName() { + return pht('Push Log (Ref)'); + } + + public function getHeraldField($field) { + $log = $this->getObject(); + switch ($field) { + case self::FIELD_REF_TYPE: + return $log->getRefType(); + case self::FIELD_REF_NAME: + return $log->getRefName(); + case self::FIELD_REF_CHANGE: + return $log->getChangeFlags(); + case self::FIELD_REPOSITORY: + return $this->getHookEngine()->getRepository()->getPHID(); + case self::FIELD_REPOSITORY_PROJECTS: + return $this->getHookEngine()->getRepository()->getProjectPHIDs(); + case self::FIELD_PUSHER: + return $this->getHookEngine()->getViewer()->getPHID(); + case self::FIELD_PUSHER_PROJECTS: + return $this->getHookEngine()->loadViewerProjectPHIDsForHerald(); + } + + return parent::getHeraldField($field); + } + +} diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php index ece9468e40..25684c1c2e 100644 --- a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php +++ b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php @@ -72,9 +72,19 @@ final class DiffusionSetPasswordPanel extends PhabricatorSettingsPanel { $e_password = pht('Not Unique'); $e_confirm = pht('Not Unique'); $errors[] = pht( - 'This password is not unique. You must use a unique password.'); + 'This password is the same as another password associated '. + 'with your account. You must use a unique password for '. + 'VCS access.'); + } else if ( + PhabricatorCommonPasswords::isCommonPassword($new_password)) { + $e_password = pht('Very Weak'); + $e_confirm = pht('Very Weak'); + $errors[] = pht( + 'This password is extremely weak: it is one of the most common '. + 'passwords in use. Choose a stronger password.'); } + if (!$errors) { $vcspassword->setPassword($envelope, $user); $vcspassword->save(); @@ -86,13 +96,6 @@ final class DiffusionSetPasswordPanel extends PhabricatorSettingsPanel { $title = pht('Set VCS Password'); - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( @@ -165,7 +168,7 @@ final class DiffusionSetPasswordPanel extends PhabricatorSettingsPanel { $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) - ->setFormError($error_view); + ->setFormErrors($errors); $remove_form = id(new AphrontFormView()) ->setUser($user); diff --git a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php index 0f6a8ae74e..578196ff69 100644 --- a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php +++ b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php @@ -59,4 +59,44 @@ final class DiffusionMercurialWireProtocol { return isset($read_only[$command]); } + public static function isReadOnlyBatchCommand($cmds) { + if (!strlen($cmds)) { + // We expect a "batch" command to always have a "cmds" string, so err + // on the side of caution and throw if we don't get any data here. This + // either indicates a mangled command from the client or a programming + // error in our code. + throw new Exception("Expected nonempty 'cmds' specification!"); + } + + // For "batch" we get a "cmds" argument like: + // + // heads ;known nodes= + // + // We need to examine the commands (here, "heads" and "known") to make sure + // they're all read-only. + + // NOTE: Mercurial has some code to escape semicolons, but it does not + // actually function for command separation. For example, these two batch + // commands will produce completely different results (the former will run + // the lookup; the latter will fail with a parser error): + // + // lookup key=a:xb;lookup key=z* 0 + // lookup key=a:;b;lookup key=z* 0 + // ^ + // | + // +-- Note semicolon. + // + // So just split unconditionally. + + $cmds = explode(';', $cmds); + foreach ($cmds as $sub_cmd) { + $name = head(explode(' ', $sub_cmd, 2)); + if (!self::isReadOnlyCommand($name)) { + return false; + } + } + + return true; + } + } diff --git a/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php b/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php new file mode 100644 index 0000000000..e5645859a4 --- /dev/null +++ b/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php @@ -0,0 +1,192 @@ +stack[] = $this->list; + $this->list = array(); + } + + private function popList() { + $list = $this->list; + $this->list = array_pop($this->stack); + return $list; + } + + private function pushItem($item, $type) { + $this->list[] = array( + 'type' => $type, + 'value' => $item, + ); + } + + public function writeData($data) { + $this->buffer .= $data; + + $messages = array(); + while (true) { + if ($this->state == 'item') { + $match = null; + $result = null; + $buf = $this->buffer; + if (preg_match('/^([a-z][a-z0-9-]*)\s/i', $buf, $match)) { + $this->pushItem($match[1], 'word'); + } else if (preg_match('/^(\d+)\s/', $buf, $match)) { + $this->pushItem((int)$match[1], 'number'); + } else if (preg_match('/^(\d+):/', $buf, $match)) { + // NOTE: The "+ 1" includes the space after the string. + $this->expectBytes = (int)$match[1] + 1; + $this->state = 'bytes'; + } else if (preg_match('/^(\\()\s/', $buf, $match)) { + $this->pushList(); + } else if (preg_match('/^(\\))\s/', $buf, $match)) { + $list = $this->popList(); + if ($this->stack) { + $this->pushItem($list, 'list'); + } else { + $result = $list; + } + } else { + $match = false; + } + + if ($match !== false) { + $this->raw .= substr($this->buffer, 0, strlen($match[0])); + $this->buffer = substr($this->buffer, strlen($match[0])); + + if ($result !== null) { + $messages[] = array( + 'structure' => $list, + 'raw' => $this->raw, + ); + $this->raw = ''; + } + } else { + // No matches yet, wait for more data. + break; + } + } else if ($this->state == 'bytes') { + $new_data = substr($this->buffer, 0, $this->expectBytes); + if (!strlen($new_data)) { + // No more bytes available yet, wait for more data. + break; + } + $this->buffer = substr($this->buffer, strlen($new_data)); + + $this->expectBytes -= strlen($new_data); + $this->raw .= $new_data; + $this->byteBuffer .= $new_data; + + if (!$this->expectBytes) { + $this->state = 'byte-space'; + // Strip off the terminal space. + $this->pushItem(substr($this->byteBuffer, 0, -1), 'string'); + $this->byteBuffer = ''; + $this->state = 'item'; + } + } else { + throw new Exception("Invalid state '{$this->state}'!"); + } + } + + return $messages; + } + + /** + * Convert a parsed command struct into a wire protocol string. + */ + public function serializeStruct(array $struct) { + $out = array(); + + $out[] = '( '; + foreach ($struct as $item) { + $value = $item['value']; + $type = $item['type']; + switch ($type) { + case 'word': + $out[] = $value; + break; + case 'number': + $out[] = $value; + break; + case 'string': + $out[] = strlen($value).':'.$value; + break; + case 'list': + $out[] = self::serializeStruct($value); + break; + default: + throw new Exception("Unknown SVN wire protocol structure '{$type}'!"); + } + if ($type != 'list') { + $out[] = ' '; + } + } + $out[] = ') '; + + return implode('', $out); + } + + public function isReadOnlyCommand(array $struct) { + if (empty($struct[0]['type']) || ($struct[0]['type'] != 'word')) { + // This isn't what we expect; fail defensively. + throw new Exception( + pht("Unexpected command structure, expected '( word ... )'.")); + } + + switch ($struct[0]['value']) { + // Authentication command set. + case 'EXTERNAL': + + // The "Main" command set. Some of the commands in this command set are + // mutation commands, and are omitted from this list. + case 'reparent': + case 'get-latest-rev': + case 'get-dated-rev': + case 'rev-proplist': + case 'rev-prop': + case 'get-file': + case 'get-dir': + case 'check-path': + case 'stat': + case 'update': + case 'get-mergeinfo': + case 'switch': + case 'status': + case 'diff': + case 'log': + case 'get-file-revs': + case 'get-locations': + + // The "Report" command set. These are not actually mutation + // operations, they just define a request for information. + case 'set-path': + case 'delete-path': + case 'link-path': + case 'finish-report': + case 'abort-report': + + // These are used to report command results. + case 'success': + case 'failure': + + // If we get here, we've matched some known read-only command. + return true; + default: + // Anything else isn't a known read-only command, so require write + // access to use it. + break; + } + + return false; + } + +} diff --git a/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php b/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php new file mode 100644 index 0000000000..088f7222ed --- /dev/null +++ b/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php @@ -0,0 +1,105 @@ +assertSameSubversionMessages( + '( ) ', + array( + array( + ), + )); + + $this->assertSameSubversionMessages( + '( duck 5:quack 42 ( item1 item2 ) ) ', + array( + array( + array( + 'type' => 'word', + 'value' => 'duck', + ), + array( + 'type' => 'string', + 'value' => 'quack', + ), + array( + 'type' => 'number', + 'value' => 42, + ), + array( + 'type' => 'list', + 'value' => array( + array( + 'type' => 'word', + 'value' => 'item1', + ), + array( + 'type' => 'word', + 'value' => 'item2', + ), + ), + ), + ), + )); + + $this->assertSameSubversionMessages( + '( msg1 ) ( msg2 ) ', + array( + array( + array( + 'type' => 'word', + 'value' => 'msg1', + ), + ), + array( + array( + 'type' => 'word', + 'value' => 'msg2', + ), + ), + )); + } + + public function testSubversionWireProtocolPartialFrame() { + $proto = new DiffusionSubversionWireProtocol(); + + // This is primarily a test that we don't hang when we write() a frame + // which straddles a string boundary. + $msg1 = $proto->writeData('( duck 5:qu'); + $msg2 = $proto->writeData('ack ) '); + + $this->assertEqual(array(), ipull($msg1, 'structure')); + $this->assertEqual( + array( + array( + array( + 'type' => 'word', + 'value' => 'duck', + ), + array( + 'type'=> 'string', + 'value' => 'quack', + ), + ), + ), + ipull($msg2, 'structure')); + } + + private function assertSameSubversionMessages($string, array $structs) { + $proto = new DiffusionSubversionWireProtocol(); + + // Verify that the wire message parses into the structs. + $messages = $proto->writeData($string); + $messages = ipull($messages, 'structure'); + $this->assertEqual($structs, $messages, 'parse<'.$string.'>'); + + // Verify that the structs serialize into the wire message. + $serial = array(); + foreach ($structs as $struct) { + $serial[] = $proto->serializeStruct($struct); + } + $serial = implode('', $serial); + $this->assertEqual($string, $serial, 'serialize<'.$string.'>'); + } +} diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index ba981ae9a7..3d3e48b29e 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -52,6 +52,17 @@ final class DiffusionCommitQuery return $this; } + + /** + * Look up commits in a specific repository. This is a shorthand for calling + * @{method:withDefaultRepository} and @{method:withRepositoryIDs}. + */ + public function withRepository(PhabricatorRepository $repository) { + $this->withDefaultRepository($repository); + $this->withRepositoryIDs(array($repository->getID())); + return $this; + } + public function needCommitData($need) { $this->needCommitData = $need; return $this; @@ -65,11 +76,13 @@ final class DiffusionCommitQuery return $this->identifierMap; } - protected function loadPage() { + protected function willExecute() { if ($this->identifierMap === null) { $this->identifierMap = array(); } + } + protected function loadPage() { $table = new PhabricatorRepositoryCommit(); $conn_r = $table->establishConnection('r'); @@ -246,7 +259,8 @@ final class DiffusionCommitQuery // If we discarded all possible identifiers (e.g., they all referenced // bogus repositories or were all too short), make sure the query finds // nothing. - throw new PhabricatorEmptyQueryException('No commit identifiers.'); + throw new PhabricatorEmptyQueryException( + pht('No commit identifiers.')); } $where[] = '('.implode(' OR ', $sql).')'; @@ -273,6 +287,8 @@ final class DiffusionCommitQuery $this->repositoryIDs); } + $where[] = $this->buildPagingClause($conn_r); + return $this->formatWhereClause($where); } diff --git a/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php b/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php index 62d293319e..537364b0ce 100644 --- a/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php +++ b/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php @@ -81,7 +81,7 @@ final class DiffusionRenameHistoryQuery { $commit = id(new DiffusionCommitQuery()) ->setViewer($this->viewer) ->withIdentifiers(array($commit_identifier)) - ->withDefaultRepository($this->request->getRepository()) + ->withRepository($this->request->getRepository()) ->executeOne(); return $commit->getID(); } diff --git a/src/applications/diffusion/query/DiffusionResolveUserQuery.php b/src/applications/diffusion/query/DiffusionResolveUserQuery.php new file mode 100644 index 0000000000..a22408ce16 --- /dev/null +++ b/src/applications/diffusion/query/DiffusionResolveUserQuery.php @@ -0,0 +1,130 @@ +"`, into a valid Phabricator user + * account, like `@alincoln`. + */ +final class DiffusionResolveUserQuery extends Phobject { + + private $name; + private $commit; + + public function withName($name) { + $this->name = $name; + return $this; + } + + public function withCommit($commit) { + $this->commit = $commit; + return $this; + } + + public function execute() { + $user_name = $this->name; + + $phid = $this->findUserPHID($this->name); + $phid = $this->fireLookupEvent($phid); + + return $phid; + } + + private function findUserPHID($user_name) { + if (!strlen($user_name)) { + return null; + } + + $phid = $this->findUserByUserName($user_name); + if ($phid) { + return $phid; + } + + $phid = $this->findUserByEmailAddress($user_name); + if ($phid) { + return $phid; + } + + $phid = $this->findUserByRealName($user_name); + if ($phid) { + return $phid; + } + + // No hits yet, try to parse it as an email address. + + $email = new PhutilEmailAddress($user_name); + + $phid = $this->findUserByEmailAddress($email->getAddress()); + if ($phid) { + return $phid; + } + + $display_name = $email->getDisplayName(); + if ($display_name) { + $phid = $this->findUserByUserName($display_name); + if ($phid) { + return $phid; + } + + $phid = $this->findUserByRealName($display_name); + if ($phid) { + return $phid; + } + } + + return null; + } + + + /** + * Emit an event so installs can do custom lookup of commit authors who may + * not be naturally resolvable. + */ + private function fireLookupEvent($guess) { + + $type = PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER; + $data = array( + 'commit' => $this->commit, + 'query' => $this->name, + 'result' => $guess, + ); + + $event = new PhabricatorEvent($type, $data); + PhutilEventEngine::dispatchEvent($event); + + return $event->getValue('result'); + } + + + private function findUserByUserName($user_name) { + $by_username = id(new PhabricatorUser())->loadOneWhere( + 'userName = %s', + $user_name); + if ($by_username) { + return $by_username->getPHID(); + } + return null; + } + + + private function findUserByRealName($real_name) { + // Note, real names are not guaranteed unique, which is why we do it this + // way. + $by_realname = id(new PhabricatorUser())->loadAllWhere( + 'realName = %s', + $real_name); + if (count($by_realname) == 1) { + return reset($by_realname)->getPHID(); + } + return null; + } + + + private function findUserByEmailAddress($email_address) { + $by_email = PhabricatorUser::loadOneWithEmailAddress($email_address); + if ($by_email) { + return $by_email->getPHID(); + } + return null; + } + +} diff --git a/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php index 8c37389d19..c78b6f9b95 100644 --- a/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php @@ -9,14 +9,10 @@ final class DiffusionSvnFileContentQuery extends DiffusionFileContentQuery { $path = $drequest->getPath(); $commit = $drequest->getCommit(); - $remote_uri = $repository->getRemoteURI(); - return $repository->getRemoteCommandFuture( - '%C %s%s@%s', + '%C %s', $this->getNeedsBlame() ? 'blame --force' : 'cat', - $remote_uri, - phutil_escape_uri($path), - $commit); + $repository->getSubversionPathURI($path, $commit)); } protected function executeQueryFromFuture(Future $future) { diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php new file mode 100644 index 0000000000..64d541f151 --- /dev/null +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php @@ -0,0 +1,86 @@ +ref = $ref; + return $this; + } + + public function executeQuery() { + $ref = $this->ref; + $message = $ref->getMessage(); + $hashes = $ref->getHashes(); + + $params = array( + 'corpus' => $message, + 'partial' => true, + ); + + $result = id(new ConduitCall('differential.parsecommitmessage', $params)) + ->setUser(PhabricatorUser::getOmnipotentUser()) + ->execute(); + $fields = $result['fields']; + + // If there is no "Differential Revision:" field in the message, try to + // identify the revision by doing a hash lookup. + + $revision_id = idx($fields, 'revisionID'); + if (!$revision_id && $hashes) { + $hash_list = array(); + foreach ($hashes as $hash) { + $hash_list[] = array($hash->getHashType(), $hash->getHashValue()); + } + $revisions = id(new DifferentialRevisionQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withCommitHashes($hash_list) + ->execute(); + + if (!empty($revisions)) { + $revision = $this->pickBestRevision($revisions); + $fields['revisionID'] = $revision->getID(); + } + } + + return $fields; + } + + + /** + * When querying for revisions by hash, more than one revision may be found. + * This function identifies the "best" revision from such a set. Typically, + * there is only one revision found. Otherwise, we try to pick an accepted + * revision first, followed by an open revision, and otherwise we go with a + * closed or abandoned revision as a last resort. + */ + private function pickBestRevision(array $revisions) { + assert_instances_of($revisions, 'DifferentialRevision'); + + // If we have more than one revision of a given status, choose the most + // recently updated one. + $revisions = msort($revisions, 'getDateModified'); + $revisions = array_reverse($revisions); + + // Try to find an accepted revision first. + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; + foreach ($revisions as $revision) { + if ($revision->getStatus() == $status_accepted) { + return $revision; + } + } + + // Try to find an open revision. + foreach ($revisions as $revision) { + if (!$revision->isClosed()) { + return $revision; + } + } + + // Settle for whatever's left. + return head($revisions); + } + +} diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php new file mode 100644 index 0000000000..54f42ef7d7 --- /dev/null +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php @@ -0,0 +1,160 @@ +identifier = $identifier; + return $this; + } + + public function executeQuery() { + if (!strlen($this->identifier)) { + throw new Exception( + pht('You must provide an identifier with withIdentifier()!')); + } + + $type = $this->getRepository()->getVersionControlSystem(); + switch ($type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $result = $this->loadGitCommitRef(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $result = $this->loadMercurialCommitRef(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $result = $this->loadSubversionCommitRef(); + break; + default: + throw new Exception(pht('Unsupported repository type "%s"!', $type)); + } + + return $result; + } + + private function loadGitCommitRef() { + $repository = $this->getRepository(); + + // NOTE: %B was introduced somewhat recently in git's history, so pull + // commit message information with %s and %b instead. + + // Even though we pass --encoding here, git doesn't always succeed, so + // we try a little harder, since git *does* tell us what the actual encoding + // is correctly (unless it doesn't; encoding is sometimes empty). + list($info) = $repository->execxLocalCommand( + 'log -n 1 --encoding=%s --format=%s %s --', + 'UTF-8', + implode( + '%x00', + array('%e', '%cn', '%ce', '%an', '%ae', '%T', '%s%n%n%b')), + $this->identifier); + + $parts = explode("\0", $info); + $encoding = array_shift($parts); + + foreach ($parts as $key => $part) { + if ($encoding) { + $part = phutil_utf8_convert($part, 'UTF-8', $encoding); + } + $parts[$key] = phutil_utf8ize($part); + if (!strlen($parts[$key])) { + $parts[$key] = null; + } + } + + $hashes = array( + id(new DiffusionCommitHash()) + ->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT) + ->setHashValue($this->identifier), + id(new DiffusionCommitHash()) + ->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_TREE) + ->setHashValue($parts[4]), + ); + + return id(new DiffusionCommitRef()) + ->setCommitterName($parts[0]) + ->setCommitterEmail($parts[1]) + ->setAuthorName($parts[2]) + ->setAuthorEmail($parts[3]) + ->setHashes($hashes) + ->setMessage($parts[5]); + } + + private function loadMercurialCommitRef() { + $repository = $this->getRepository(); + + list($stdout) = $repository->execxLocalCommand( + 'log --template %s --rev %s', + '{author}\\n{desc}', + hgsprintf('%s', $this->identifier)); + + list($author, $message) = explode("\n", $stdout, 2); + + $author = phutil_utf8ize($author); + $message = phutil_utf8ize($message); + + list($author_name, $author_email) = $this->splitUserIdentifier($author); + + $hashes = array( + id(new DiffusionCommitHash()) + ->setHashType(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT) + ->setHashValue($this->identifier), + ); + + return id(new DiffusionCommitRef()) + ->setAuthorName($author_name) + ->setAuthorEmail($author_email) + ->setMessage($message) + ->setHashes($hashes); + } + + private function loadSubversionCommitRef() { + $repository = $this->getRepository(); + + list($xml) = $repository->execxRemoteCommand( + 'log --xml --limit 1 %s', + $repository->getSubversionPathURI(null, $this->identifier)); + + // Subversion may send us back commit messages which won't parse because + // they have non UTF-8 garbage in them. Slam them into valid UTF-8. + $xml = phutil_utf8ize($xml); + $log = new SimpleXMLElement($xml); + $entry = $log->logentry[0]; + + $author = (string)$entry->author; + $message = (string)$entry->msg; + + list($author_name, $author_email) = $this->splitUserIdentifier($author); + + // No hashes in Subversion. + $hashes = array(); + + return id(new DiffusionCommitRef()) + ->setAuthorName($author_name) + ->setAuthorEmail($author_email) + ->setMessage($message) + ->setHashes($hashes); + } + + private function splitUserIdentifier($user) { + $email = new PhutilEmailAddress($user); + + if ($email->getDisplayName() || $email->getDomainName()) { + $user_name = $email->getDisplayName(); + $user_email = $email->getAddress(); + } else { + $user_name = $email->getAddress(); + $user_email = null; + } + + return array($user_name, $user_email); + } + +} diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php index 9305042868..82de8e404a 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php @@ -32,7 +32,7 @@ final class DiffusionLowLevelGitRefQuery extends DiffusionLowLevelQuery { if ($repository->isWorkingCopyBare()) { $prefix = 'refs/heads/'; } else { - $remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE; + $remote = DiffusionGitBranch::DEFAULT_GIT_REMOTE; $prefix = 'refs/remotes/'.$remote.'/'; } } else { diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php index cd1cbcd720..f779794b51 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php @@ -12,14 +12,15 @@ final class DiffusionLowLevelMercurialBranchesQuery // NOTE: `--debug` gives us 40-character hashes. list($stdout) = $repository->execxLocalCommand( '--debug branches'); + $stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); $branches = array(); $lines = ArcanistMercurialParser::parseMercurialBranches($stdout); foreach ($lines as $name => $spec) { - $branches[] = id(new DiffusionBranchInformation()) - ->setName($name) - ->setHeadCommitIdentifier($spec['rev']); + $branches[] = id(new DiffusionRepositoryRef()) + ->setShortName($name) + ->setCommitIdentifier($spec['rev']); } return $branches; diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php new file mode 100644 index 0000000000..b7ecf5a5c4 --- /dev/null +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php @@ -0,0 +1,84 @@ +identifier = $identifier; + return $this; + } + + public function executeQuery() { + if (!strlen($this->identifier)) { + throw new Exception( + pht('You must provide an identifier with withIdentifier()!')); + } + + $type = $this->getRepository()->getVersionControlSystem(); + switch ($type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $result = $this->loadGitParents(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $result = $this->loadMercurialParents(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $result = $this->loadSubversionParents(); + break; + default: + throw new Exception(pht('Unsupported repository type "%s"!', $type)); + } + + return $result; + } + + private function loadGitParents() { + $repository = $this->getRepository(); + + list($stdout) = $repository->execxLocalCommand( + 'log -n 1 --format=%s %s', + '%P', + $this->identifier); + + return preg_split('/\s+/', trim($stdout)); + } + + private function loadMercurialParents() { + $repository = $this->getRepository(); + + list($stdout) = $repository->execxLocalCommand( + 'log --debug --limit 1 --template={parents} --rev %s', + $this->identifier); + $stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); + + $hashes = preg_split('/\s+/', trim($stdout)); + foreach ($hashes as $key => $value) { + // Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want + // to strip out the local rev part. + list($local, $global) = explode(':', $value); + $hashes[$key] = $global; + + // With --debug we get 40-character hashes but also get the "000000..." + // hash for missing parents; ignore it. + if (preg_match('/^0+$/', $global)) { + unset($hashes[$key]); + } + } + + return $hashes; + } + + private function loadSubversionParents() { + $n = (int)$this->identifier; + if ($n > 1) { + $ids = array($n - 1); + } else { + $ids = array(); + } + + return $ids; + } + +} diff --git a/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php deleted file mode 100644 index a8a9e53af7..0000000000 --- a/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php +++ /dev/null @@ -1,14 +0,0 @@ -executeQuery(); - } - -} diff --git a/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php deleted file mode 100644 index ba10921865..0000000000 --- a/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php +++ /dev/null @@ -1,19 +0,0 @@ -getRequest(); - $repository = $drequest->getRepository(); - - list($stdout) = $repository->execxLocalCommand( - 'log -n 1 --format=%s %s', - '%P', - $drequest->getStableCommitName()); - - $hashes = preg_split('/\s+/', trim($stdout)); - - return self::loadCommitsByIdentifiers($hashes, $drequest); - } -} diff --git a/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php deleted file mode 100644 index ec19f9cbce..0000000000 --- a/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php +++ /dev/null @@ -1,30 +0,0 @@ -getRequest(); - $repository = $drequest->getRepository(); - - list($stdout) = $repository->execxLocalCommand( - 'log --debug --limit 1 --template={parents} --rev %s', - $drequest->getStableCommitName()); - - $hashes = preg_split('/\s+/', trim($stdout)); - foreach ($hashes as $key => $value) { - // Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want - // to strip out the local rev part. - list($local, $global) = explode(':', $value); - $hashes[$key] = $global; - - // With --debug we get 40-character hashes but also get the "000000..." - // hash for missing parents; ignore it. - if (preg_match('/^0+$/', $global)) { - unset($hashes[$key]); - } - } - - return self::loadCommitsByIdentifiers($hashes, $drequest); - } -} diff --git a/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php deleted file mode 100644 index 8b2ae329b2..0000000000 --- a/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php +++ /dev/null @@ -1,22 +0,0 @@ -getRequest(); - $repository = $drequest->getRepository(); - - // TODO: With merge properties in recent versions of SVN, can we do - // a better job of this? - - $n = $drequest->getStableCommitName(); - if ($n > 1) { - $ids = array($n - 1); - } else { - $ids = array(); - } - - return self::loadCommitsByIdentifiers($ids, $drequest); - } -} diff --git a/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php index e589721e73..ee6cbf13a6 100644 --- a/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php +++ b/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php @@ -34,9 +34,7 @@ final class DiffusionGitRawDiffQuery extends DiffusionRawDiffQuery { $commit, $path); - if ($this->getTimeout()) { - $future->setTimeout($this->getTimeout()); - } + $this->configureFuture($future); try { list($raw_diff) = $future->resolvex(); @@ -61,9 +59,7 @@ final class DiffusionGitRawDiffQuery extends DiffusionRawDiffQuery { $commit, $drequest->getPath()); - if ($this->getTimeout()) { - $future->setTimeout($this->getTimeout()); - } + $this->configureFuture($future); list($raw_diff) = $future->resolvex(); } diff --git a/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php index 4a016703ac..f4dee9c840 100644 --- a/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php +++ b/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php @@ -3,26 +3,9 @@ final class DiffusionMercurialRawDiffQuery extends DiffusionRawDiffQuery { protected function executeQuery() { - $raw_diff = $this->executeRawDiffCommand(); - - // the only legitimate case here is if we are looking at the first commit - // in the repository. no parents means first commit. - if (!$raw_diff) { - $drequest = $this->getRequest(); - $parent_query = - DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest); - $parents = $parent_query->loadParents(); - if ($parents === array()) { - // mercurial likes the string null here - $this->setAgainstCommit('null'); - $raw_diff = $this->executeRawDiffCommand(); - } - } - - return $raw_diff; + return $this->executeRawDiffCommand(); } - protected function executeRawDiffCommand() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); @@ -34,19 +17,20 @@ final class DiffusionMercurialRawDiffQuery extends DiffusionRawDiffQuery { $against = $this->getAgainstCommit(); if ($against === null) { - $against = $commit.'^'; + // If `$commit` has no parents (usually because it's the first commit + // in the repository), we want to diff against `null`. This revset will + // do that for us automatically. + $against = '('.$commit.'^ or null)'; } $future = $repository->getLocalCommandFuture( - 'diff -U %d --git --rev %s:%s -- %s', + 'diff -U %d --git --rev %s --rev %s -- %s', $this->getLinesOfContext(), $against, $commit, $path); - if ($this->getTimeout()) { - $future->setTimeout($this->getTimeout()); - } + $this->configureFuture($future); list($raw_diff) = $future->resolvex(); diff --git a/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php index 57c2ab9d07..c6d741ae47 100644 --- a/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php +++ b/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php @@ -6,6 +6,7 @@ abstract class DiffusionRawDiffQuery extends DiffusionQuery { private $timeout; private $linesOfContext = 65535; private $againstCommit; + private $byteLimit; final public static function newFromDiffusionRequest( DiffusionRequest $request) { @@ -25,6 +26,15 @@ abstract class DiffusionRawDiffQuery extends DiffusionQuery { return $this->timeout; } + public function setByteLimit($byte_limit) { + $this->byteLimit = $byte_limit; + return $this; + } + + public function getByteLimit() { + return $this->byteLimit; + } + final public function setLinesOfContext($lines_of_context) { $this->linesOfContext = $lines_of_context; return $this; @@ -43,4 +53,15 @@ abstract class DiffusionRawDiffQuery extends DiffusionQuery { return $this->againstCommit; } + protected function configureFuture(ExecFuture $future) { + if ($this->getTimeout()) { + $future->setTimeout($this->getTimeout()); + } + + if ($this->getByteLimit()) { + $future->setStdoutSizeLimit($this->getByteLimit()); + $future->setStderrSizeLimit($this->getByteLimit()); + } + } + } diff --git a/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php index 14bd664718..c66ec335a7 100644 --- a/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php +++ b/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php @@ -15,17 +15,14 @@ final class DiffusionSvnRawDiffQuery extends DiffusionRawDiffQuery { } $future = $repository->getRemoteCommandFuture( - 'diff --diff-cmd %s -x -U%d -r %d:%d %s%s@', + 'diff --diff-cmd %s -x -U%d -r %d:%d %s', $arc_root.'/../scripts/repository/binary_safe_diff.sh', $this->getLinesOfContext(), $against, $commit, - $repository->getRemoteURI(), - $drequest->getPath()); + $repository->getSubversionPathURI($drequest->getPath())); - if ($this->getTimeout()) { - $future->setTimeout($this->getTimeout()); - } + $this->configureFuture($future); list($raw_diff) = $future->resolvex(); return $raw_diff; diff --git a/src/applications/diffusion/remarkup/DiffusionRemarkupRule.php b/src/applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php similarity index 93% rename from src/applications/diffusion/remarkup/DiffusionRemarkupRule.php rename to src/applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php index 52fd88568e..6169b6e1b1 100644 --- a/src/applications/diffusion/remarkup/DiffusionRemarkupRule.php +++ b/src/applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php @@ -1,6 +1,6 @@ getEngine()->getConfig('viewer'); + + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withCallsigns($ids) + ->execute(); + + return mpull($repositories, null, 'getCallsign'); + } + +} diff --git a/src/applications/diffusion/request/DiffusionGitRequest.php b/src/applications/diffusion/request/DiffusionGitRequest.php index 2f411c6584..584ab3acf3 100644 --- a/src/applications/diffusion/request/DiffusionGitRequest.php +++ b/src/applications/diffusion/request/DiffusionGitRequest.php @@ -39,7 +39,7 @@ final class DiffusionGitRequest extends DiffusionRequest { if ($this->repository->isWorkingCopyBare()) { return $branch; } else { - $remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE; + $remote = DiffusionGitBranch::DEFAULT_GIT_REMOTE; return $remote.'/'.$branch; } } diff --git a/src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php index ac148abbe0..d2c59dee79 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php @@ -14,26 +14,30 @@ final class DiffusionSSHGitReceivePackWorkflow )); } - public function isReadOnly() { - return false; - } - - public function getRequestPath() { + protected function executeRepositoryOperations() { $args = $this->getArgs(); - return head($args->getArg('dir')); - } + $path = head($args->getArg('dir')); + $repository = $this->loadRepository($path); - protected function executeRepositoryOperations( - PhabricatorRepository $repository) { - $future = new ExecFuture( - 'git-receive-pack %s', - $repository->getLocalPath()); - $err = $this->passthruIO($future); + // This is a write, and must have write access. + $this->requireWriteAccess(); + + $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); + + $future = id(new ExecFuture('%C', $command)) + ->setEnv($this->getEnvironment()); + + $err = $this->newPassthruCommand() + ->setIOChannel($this->getIOChannel()) + ->setCommandChannelFromExecFuture($future) + ->execute(); if (!$err) { $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, PhabricatorRepositoryStatusMessage::CODE_OKAY); + $this->waitForGitClient(); } return $err; diff --git a/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php index 915ae45970..242c9d0c7f 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php @@ -14,21 +14,27 @@ final class DiffusionSSHGitUploadPackWorkflow )); } - public function isReadOnly() { - return true; - } - - public function getRequestPath() { + protected function executeRepositoryOperations() { $args = $this->getArgs(); - return head($args->getArg('dir')); - } + $path = head($args->getArg('dir')); + $repository = $this->loadRepository($path); - protected function executeRepositoryOperations( - PhabricatorRepository $repository) { + $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); - $future = new ExecFuture('git-upload-pack %s', $repository->getLocalPath()); + $future = id(new ExecFuture('%C', $command)) + ->setEnv($this->getEnvironment()); - return $this->passthruIO($future); + $err = $this->newPassthruCommand() + ->setIOChannel($this->getIOChannel()) + ->setCommandChannelFromExecFuture($future) + ->execute(); + + if (!$err) { + $this->waitForGitClient(); + } + + return $err; } } diff --git a/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php index ceaf7c146e..d70551607f 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php @@ -7,4 +7,17 @@ abstract class DiffusionSSHGitWorkflow extends DiffusionSSHWorkflow { return parent::writeError($message."\n"); } + protected function waitForGitClient() { + $io_channel = $this->getIOChannel(); + + // If we don't wait for the client to close the connection, `git` will + // consider it an early abort and fail. Sit around until Git is comfortable + // that it really received all the data. + while ($io_channel->isOpenForReading()) { + $io_channel->update(); + $this->getErrorChannel()->flush(); + PhutilChannel::waitForAny(array($io_channel)); + } + } + } diff --git a/src/applications/diffusion/ssh/DiffusionSSHMercurialServeWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHMercurialServeWorkflow.php new file mode 100644 index 0000000000..e7697e6083 --- /dev/null +++ b/src/applications/diffusion/ssh/DiffusionSSHMercurialServeWorkflow.php @@ -0,0 +1,105 @@ +setName('hg'); + $this->setArguments( + array( + array( + 'name' => 'repository', + 'short' => 'R', + 'param' => 'repo', + ), + array( + 'name' => 'stdio', + ), + array( + 'name' => 'command', + 'wildcard' => true, + ), + )); + } + + protected function executeRepositoryOperations() { + $args = $this->getArgs(); + $path = $args->getArg('repository'); + $repository = $this->loadRepository($path); + + $args = $this->getArgs(); + + if (!$args->getArg('stdio')) { + throw new Exception("Expected `hg ... --stdio`!"); + } + + if ($args->getArg('command') !== array('serve')) { + throw new Exception("Expected `hg ... serve`!"); + } + + $command = csprintf('hg -R %s serve --stdio', $repository->getLocalPath()); + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); + + $future = id(new ExecFuture('%C', $command)) + ->setEnv($this->getEnvironment()); + + $io_channel = $this->getIOChannel(); + $protocol_channel = new DiffusionSSHMercurialWireClientProtocolChannel( + $io_channel); + + $err = id($this->newPassthruCommand()) + ->setIOChannel($protocol_channel) + ->setCommandChannelFromExecFuture($future) + ->setWillWriteCallback(array($this, 'willWriteMessageCallback')) + ->execute(); + + // TODO: It's apparently technically possible to communicate errors to + // Mercurial over SSH by writing a special "\n\n-\n" string. However, + // my attempt to implement that resulted in Mercurial closing the socket and + // then hanging, without showing the error. This might be an issue on our + // side (we need to close our half of the socket?), or maybe the code + // for this in Mercurial doesn't actually work, or maybe something else + // is afoot. At some point, we should look into doing this more cleanly. + // For now, when we, e.g., reject writes for policy reasons, the user will + // see "abort: unexpected response: empty string" after the diagnostically + // useful, e.g., "remote: This repository is read-only over SSH." message. + + if (!$err && $this->didSeeWrite) { + $repository->writeStatusMessage( + PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, + PhabricatorRepositoryStatusMessage::CODE_OKAY); + } + + return $err; + } + + public function willWriteMessageCallback( + PhabricatorSSHPassthruCommand $command, + $message) { + + $command = $message['command']; + + // Check if this is a readonly command. + + $is_readonly = false; + if ($command == 'batch') { + $cmds = idx($message['arguments'], 'cmds'); + if (DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds)) { + $is_readonly = true; + } + } else if (DiffusionMercurialWireProtocol::isReadOnlyCommand($command)) { + $is_readonly = true; + } + + if (!$is_readonly) { + $this->requireWriteAccess(); + $this->didSeeWrite = true; + } + + // If we're good, return the raw message data. + return $message['raw']; + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionSSHMercurialWireClientProtocolChannel.php b/src/applications/diffusion/ssh/DiffusionSSHMercurialWireClientProtocolChannel.php new file mode 100644 index 0000000000..ab0a62b85d --- /dev/null +++ b/src/applications/diffusion/ssh/DiffusionSSHMercurialWireClientProtocolChannel.php @@ -0,0 +1,217 @@ +command = ''; + $this->state = 'data-length'; + } else { + $this->state = 'command'; + } + $this->expectArgumentCount = null; + $this->expectBytes = null; + $this->command = null; + $this->argumentName = null; + $this->arguments = array(); + $this->raw = ''; + } + + private function readProtocolLine() { + $pos = strpos($this->buffer, "\n"); + + if ($pos === false) { + return null; + } + + $line = substr($this->buffer, 0, $pos); + + $this->raw .= $line."\n"; + $this->buffer = substr($this->buffer, $pos + 1); + + return $line; + } + + private function readProtocolBytes() { + if (strlen($this->buffer) < $this->expectBytes) { + return null; + } + + $bytes = substr($this->buffer, 0, $this->expectBytes); + $this->raw .= $bytes; + $this->buffer = substr($this->buffer, $this->expectBytes); + + return $bytes; + } + + private function newMessageAndResetState() { + $message = array( + 'command' => $this->command, + 'arguments' => $this->arguments, + 'raw' => $this->raw, + ); + $this->initializeState($this->command); + return $message; + } + + private function newDataMessage($bytes) { + $message = array( + 'command' => '', + 'raw' => strlen($bytes)."\n".$bytes, + ); + return $message; + } + + protected function decodeStream($data) { + $this->buffer .= $data; + + $out = array(); + $messages = array(); + + while (true) { + if ($this->state == 'command') { + $this->initializeState(); + + // We're reading a command. It looks like: + // + // + + $line = $this->readProtocolLine(); + if ($line === null) { + break; + } + + $this->command = $line; + $this->state = 'arguments'; + } else if ($this->state == 'arguments') { + + // Check if we're still waiting for arguments. + $args = DiffusionMercurialWireProtocol::getCommandArgs($this->command); + $have = array_select_keys($this->arguments, $args); + if (count($have) == count($args)) { + // We have all the arguments. Emit a message and read the next + // command. + $messages[] = $this->newMessageAndResetState(); + } else { + // We're still reading arguments. They can either look like: + // + // + // + // ... + // + // ...or like this: + // + // * + // + // + // ... + + $line = $this->readProtocolLine(); + if ($line === null) { + break; + } + + list($arg, $size) = explode(' ', $line, 2); + $size = (int)$size; + + if ($arg != '*') { + $this->expectBytes = $size; + $this->argumentName = $arg; + $this->state = 'value'; + } else { + $this->arguments['*'] = array(); + $this->expectArgumentCount = $size; + $this->state = 'argv'; + } + } + } else if ($this->state == 'value' || $this->state == 'argv-value') { + + // We're reading the value of an argument. We just need to wait for + // the right number of bytes to show up. + + $bytes = $this->readProtocolBytes(); + if ($bytes === null) { + break; + } + + if ($this->state == 'argv-value') { + $this->arguments['*'][$this->argumentName] = $bytes; + $this->state = 'argv'; + } else { + $this->arguments[$this->argumentName] = $bytes; + $this->state = 'arguments'; + } + + + } else if ($this->state == 'argv') { + + // We're reading a variable number of arguments. We need to wait for + // the arguments to arrive. + + if ($this->expectArgumentCount) { + $line = $this->readProtocolLine(); + if ($line === null) { + break; + } + + list($arg, $size) = explode(' ', $line, 2); + $size = (int)$size; + + $this->expectBytes = $size; + $this->argumentName = $arg; + $this->state = 'argv-value'; + + $this->expectArgumentCount--; + } else { + $this->state = 'arguments'; + } + } else if ($this->state == 'data-length') { + $line = $this->readProtocolLine(); + if ($line === null) { + break; + } + $this->expectBytes = (int)$line; + if (!$this->expectBytes) { + $messages[] = $this->newDataMessage(''); + $this->initializeState(); + } else { + $this->state = 'data-bytes'; + } + } else if ($this->state == 'data-bytes') { + $bytes = substr($this->buffer, 0, $this->expectBytes); + $this->buffer = substr($this->buffer, strlen($bytes)); + $this->expectBytes -= strlen($bytes); + + $messages[] = $this->newDataMessage($bytes); + + if (!$this->expectBytes) { + // We've finished reading this chunk, so go read the next chunk. + $this->state = 'data-length'; + } else { + // We're waiting for more data, and have read everything available + // to us so far. + break; + } + } else { + throw new Exception("Bad parser state '{$this->state}'!"); + } + } + + return $messages; + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionSSHMercurialWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHMercurialWorkflow.php new file mode 100644 index 0000000000..e48dcf5eb4 --- /dev/null +++ b/src/applications/diffusion/ssh/DiffusionSSHMercurialWorkflow.php @@ -0,0 +1,5 @@ +setName('svnserve'); + $this->setArguments( + array( + array( + 'name' => 'tunnel', + 'short' => 't', + ), + )); + } + + protected function executeRepositoryOperations() { + $args = $this->getArgs(); + if (!$args->getArg('tunnel')) { + throw new Exception("Expected `svnserve -t`!"); + } + + $command = csprintf( + 'svnserve -t --tunnel-user=%s', + $this->getUser()->getUsername()); + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); + + $future = new ExecFuture('%C', $command); + + $this->inProtocol = new DiffusionSubversionWireProtocol(); + $this->outProtocol = new DiffusionSubversionWireProtocol(); + + $err = id($this->newPassthruCommand()) + ->setIOChannel($this->getIOChannel()) + ->setCommandChannelFromExecFuture($future) + ->setWillWriteCallback(array($this, 'willWriteMessageCallback')) + ->setWillReadCallback(array($this, 'willReadMessageCallback')) + ->execute(); + + if (!$err && $this->didSeeWrite) { + $this->getRepository()->writeStatusMessage( + PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, + PhabricatorRepositoryStatusMessage::CODE_OKAY); + } + + return $err; + } + + public function willWriteMessageCallback( + PhabricatorSSHPassthruCommand $command, + $message) { + + $proto = $this->inProtocol; + $messages = $proto->writeData($message); + + $result = array(); + foreach ($messages as $message) { + $message_raw = $message['raw']; + $struct = $message['structure']; + + if (!$this->inSeenGreeting) { + $this->inSeenGreeting = true; + + // The first message the client sends looks like: + // + // ( version ( cap1 ... ) url ... ) + // + // We want to grab the URL, load the repository, make sure it exists and + // is accessible, and then replace it with the location of the + // repository on disk. + + $uri = $struct[2]['value']; + $struct[2]['value'] = $this->makeInternalURI($uri); + + $message_raw = $proto->serializeStruct($struct); + } else if (isset($struct[0]) && $struct[0]['type'] == 'word') { + + if (!$proto->isReadOnlyCommand($struct)) { + $this->didSeeWrite = true; + $this->requireWriteAccess($struct[0]['value']); + } + + // Several other commands also pass in URLs. We need to translate + // all of these into the internal representation; this also makes sure + // they're valid and accessible. + + switch ($struct[0]['value']) { + case 'reparent': + // ( reparent ( url ) ) + $struct[1]['value'][0]['value'] = $this->makeInternalURI( + $struct[1]['value'][0]['value']); + $message_raw = $proto->serializeStruct($struct); + break; + case 'switch': + // ( switch ( ( rev ) target recurse url ... ) ) + $struct[1]['value'][3]['value'] = $this->makeInternalURI( + $struct[1]['value'][3]['value']); + $message_raw = $proto->serializeStruct($struct); + break; + case 'diff': + // ( diff ( ( rev ) target recurse ignore-ancestry url ... ) ) + $struct[1]['value'][4]['value'] = $this->makeInternalURI( + $struct[1]['value'][4]['value']); + $message_raw = $proto->serializeStruct($struct); + break; + } + } + + $result[] = $message_raw; + } + + if (!$result) { + return null; + } + + return implode('', $result); + } + + public function willReadMessageCallback( + PhabricatorSSHPassthruCommand $command, + $message) { + + $proto = $this->outProtocol; + $messages = $proto->writeData($message); + + $result = array(); + foreach ($messages as $message) { + $message_raw = $message['raw']; + $struct = $message['structure']; + + if (isset($struct[0]) && ($struct[0]['type'] == 'word')) { + + if ($struct[0]['value'] == 'success') { + switch ($this->outPhaseCount) { + case 0: + // This is the "greeting", which announces capabilities. + break; + case 1: + // This responds to the client greeting, and announces auth. + break; + case 2: + // This responds to auth, which should be trivial over SSH. + break; + case 3: + // This contains the URI of the repository. We need to edit it; + // if it does not match what the client requested it will reject + // the response. + $struct[1]['value'][1]['value'] = $this->makeExternalURI( + $struct[1]['value'][1]['value']); + $message_raw = $proto->serializeStruct($struct); + break; + default: + // We don't care about other protocol frames. + break; + } + + $this->outPhaseCount++; + } else if ($struct[0]['value'] == 'failure') { + // Find any error messages which include the internal URI, and + // replace the text with the external URI. + foreach ($struct[1]['value'] as $key => $error) { + $code = $error['value'][0]['value']; + $message = $error['value'][1]['value']; + + $message = str_replace( + $this->internalBaseURI, + $this->externalBaseURI, + $message); + + // Derp derp derp derp derp. The structure looks like this: + // ( failure ( ( code message ... ) ... ) ) + $struct[1]['value'][$key]['value'][1]['value'] = $message; + } + $message_raw = $proto->serializeStruct($struct); + } + + } + + $result[] = $message_raw; + } + + if (!$result) { + return null; + } + + return implode('', $result); + } + + private function makeInternalURI($uri_string) { + $uri = new PhutilURI($uri_string); + + $proto = $uri->getProtocol(); + if ($proto !== 'svn+ssh') { + throw new Exception( + pht( + 'Protocol for URI "%s" MUST be "svn+ssh".', + $uri_string)); + } + + $path = $uri->getPath(); + + // Subversion presumably deals with this, but make sure there's nothing + // skethcy going on with the URI. + if (preg_match('(/\\.\\./)', $path)) { + throw new Exception( + pht( + 'String "/../" is invalid in path specification "%s".', + $uri_string)); + } + + $repository = $this->loadRepository($path); + + $path = preg_replace( + '(^/diffusion/[A-Z]+)', + rtrim($repository->getLocalPath(), '/'), + $path); + + if (preg_match('(^/diffusion/[A-Z]+/$)', $path)) { + $path = rtrim($path, '/'); + } + + $uri->setPath($path); + + // If this is happening during the handshake, these are the base URIs for + // the request. + if ($this->externalBaseURI === null) { + $pre = (string)id(clone $uri)->setPath(''); + $this->externalBaseURI = $pre.'/diffusion/'.$repository->getCallsign(); + $this->internalBaseURI = $pre.rtrim($repository->getLocalPath(), '/'); + } + + return (string)$uri; + } + + private function makeExternalURI($uri) { + $internal = $this->internalBaseURI; + $external = $this->externalBaseURI; + + if (strncmp($uri, $internal, strlen($internal)) === 0) { + $uri = $external.substr($uri, strlen($internal)); + } + + return $uri; + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionSSHSubversionWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHSubversionWorkflow.php new file mode 100644 index 0000000000..a96cd55229 --- /dev/null +++ b/src/applications/diffusion/ssh/DiffusionSSHSubversionWorkflow.php @@ -0,0 +1,10 @@ +repository) { + throw new Exception("Call loadRepository() before getRepository()!"); + } + return $this->repository; + } public function getArgs() { return $this->args; } - abstract protected function isReadOnly(); - abstract protected function getRequestPath(); - abstract protected function executeRepositoryOperations( - PhabricatorRepository $repository); + public function getEnvironment() { + $env = array( + DiffusionCommitHookEngine::ENV_USER => $this->getUser()->getUsername(), + DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh', + ); + + $ssh_client = getenv('SSH_CLIENT'); + if ($ssh_client) { + // This has the format " ". Grab the IP. + $remote_address = head(explode(' ', $ssh_client)); + $env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address; + } + + return $env; + } + + abstract protected function executeRepositoryOperations(); protected function writeError($message) { $this->getErrorChannel()->write($message); @@ -22,17 +44,15 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { $this->args = $args; try { - $repository = $this->loadRepository(); - return $this->executeRepositoryOperations($repository); + return $this->executeRepositoryOperations(); } catch (Exception $ex) { $this->writeError(get_class($ex).': '.$ex->getMessage()); return 1; } } - private function loadRepository() { + protected function loadRepository($path) { $viewer = $this->getUser(); - $path = $this->getRequestPath(); $regex = '@^/?diffusion/(?P[A-Z]+)(?:/|$)@'; $matches = null; @@ -56,26 +76,11 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { pht('No repository "%s" exists!', $callsign)); } - $is_push = !$this->isReadOnly(); - switch ($repository->getServeOverSSH()) { case PhabricatorRepository::SERVE_READONLY: - if ($is_push) { - throw new Exception( - pht('This repository is read-only over SSH.')); - } - break; case PhabricatorRepository::SERVE_READWRITE: - if ($is_push) { - $can_push = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - DiffusionCapabilityPush::CAPABILITY); - if (!$can_push) { - throw new Exception( - pht('You do not have permission to push to this repository.')); - } - } + // If we have read or read/write access, proceed for now. We will + // check write access when the user actually issues a write command. break; case PhabricatorRepository::SERVE_OFF: default: @@ -83,7 +88,52 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { pht('This repository is not available over SSH.')); } + $this->repository = $repository; + return $repository; } + protected function requireWriteAccess($protocol_command = null) { + if ($this->hasWriteAccess === true) { + return; + } + + $repository = $this->getRepository(); + $viewer = $this->getUser(); + + switch ($repository->getServeOverSSH()) { + case PhabricatorRepository::SERVE_READONLY: + if ($protocol_command !== null) { + throw new Exception( + pht( + 'This repository is read-only over SSH (tried to execute '. + 'protocol command "%s").', + $protocol_command)); + } else { + throw new Exception( + pht('This repository is read-only over SSH.')); + } + break; + case PhabricatorRepository::SERVE_READWRITE: + $can_push = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + DiffusionCapabilityPush::CAPABILITY); + if (!$can_push) { + throw new Exception( + pht('You do not have permission to push to this repository.')); + } + break; + case PhabricatorRepository::SERVE_OFF: + default: + // This shouldn't be reachable because we don't get this far if the + // repository isn't enabled, but kick them out anyway. + throw new Exception( + pht('This repository is not available over SSH.')); + } + + $this->hasWriteAccess = true; + return $this->hasWriteAccess; + } + } diff --git a/src/applications/diffusion/ssh/__tests__/DiffusionSSHMercurialWireTestCase.php b/src/applications/diffusion/ssh/__tests__/DiffusionSSHMercurialWireTestCase.php new file mode 100644 index 0000000000..d1e8f8766c --- /dev/null +++ b/src/applications/diffusion/ssh/__tests__/DiffusionSSHMercurialWireTestCase.php @@ -0,0 +1,58 @@ +assertEqual(2, count($raw)); + $expect = json_decode($raw[1], true); + $this->assertEqual(true, is_array($expect), $file); + + $this->assertParserResult($expect, $raw[0], $file); + } + } + + private function assertParserResult(array $expect, $input, $file) { + list($x, $y) = PhutilSocketChannel::newChannelPair(); + $xp = new DiffusionSSHMercurialWireClientProtocolChannel($x); + + $y->write($input); + $y->flush(); + $y->closeWriteChannel(); + + $messages = array(); + for ($ii = 0; $ii < count($expect); $ii++) { + try { + $messages[] = $xp->waitForMessage(); + } catch (Exception $ex) { + // This is probably the parser not producing as many messages as + // we expect. Log the exception, but continue to the assertion below + // since that will often be easier to diagnose. + phlog($ex); + break; + } + } + + $this->assertEqual($expect, $messages, $file); + + // Now, make sure the channel doesn't have *more* messages than we expect. + // Specifically, it should throw when we try to read another message. + $caught = null; + try { + $xp->waitForMessage(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertEqual( + true, + ($caught instanceof Exception), + "No extra messages for '{$file}'."); + } + +} diff --git a/src/applications/diffusion/ssh/__tests__/hgwiredata/batch.txt b/src/applications/diffusion/ssh/__tests__/hgwiredata/batch.txt new file mode 100644 index 0000000000..86de58b959 --- /dev/null +++ b/src/applications/diffusion/ssh/__tests__/hgwiredata/batch.txt @@ -0,0 +1,16 @@ +batch +* 0 +cmds 19 +heads ;known nodes= +~~~~~~~~~~ +[ + { + "command" : "batch", + "arguments" : { + "*" : { + }, + "cmds" : "heads ;known nodes=" + }, + "raw" : "batch\n* 0\ncmds 19\nheads ;known nodes=" + } +] diff --git a/src/applications/diffusion/ssh/__tests__/hgwiredata/capabilities.txt b/src/applications/diffusion/ssh/__tests__/hgwiredata/capabilities.txt new file mode 100644 index 0000000000..9d3934b4fe --- /dev/null +++ b/src/applications/diffusion/ssh/__tests__/hgwiredata/capabilities.txt @@ -0,0 +1,10 @@ +capabilities + +~~~~~~~~~~ +[ + { + "command" : "capabilities", + "arguments" : [], + "raw" : "capabilities\n" + } +] diff --git a/src/applications/diffusion/ssh/__tests__/hgwiredata/capabilities2.txt b/src/applications/diffusion/ssh/__tests__/hgwiredata/capabilities2.txt new file mode 100644 index 0000000000..7ac0c71934 --- /dev/null +++ b/src/applications/diffusion/ssh/__tests__/hgwiredata/capabilities2.txt @@ -0,0 +1,16 @@ +capabilities +capabilities + +~~~~~~~~~~ +[ + { + "command" : "capabilities", + "arguments" : [], + "raw" : "capabilities\n" + }, + { + "command" : "capabilities", + "arguments" : [], + "raw" : "capabilities\n" + } +] diff --git a/src/applications/diffusion/ssh/__tests__/hgwiredata/getbundle.txt b/src/applications/diffusion/ssh/__tests__/hgwiredata/getbundle.txt new file mode 100644 index 0000000000..ff28bea22d --- /dev/null +++ b/src/applications/diffusion/ssh/__tests__/hgwiredata/getbundle.txt @@ -0,0 +1,18 @@ +getbundle +* 2 +common 40 +0000000000000000000000000000000000000000heads 122 +7cb27ad591d60500c020283b81c6467540218eda 1036b72db89a0451fa82fcd5462d903f591f0a3c 0b9d8290c4e067a0b91b43062ee9de392e8fae88 +~~~~~~~~~~ +[ + { + "command" : "getbundle", + "arguments" : { + "*" : { + "common" : "0000000000000000000000000000000000000000", + "heads" : "7cb27ad591d60500c020283b81c6467540218eda 1036b72db89a0451fa82fcd5462d903f591f0a3c 0b9d8290c4e067a0b91b43062ee9de392e8fae88" + } + }, + "raw" : "getbundle\n* 2\ncommon 40\n0000000000000000000000000000000000000000heads 122\n7cb27ad591d60500c020283b81c6467540218eda 1036b72db89a0451fa82fcd5462d903f591f0a3c 0b9d8290c4e067a0b91b43062ee9de392e8fae88" + } +] diff --git a/src/applications/diffusion/ssh/__tests__/hgwiredata/unbundle.txt b/src/applications/diffusion/ssh/__tests__/hgwiredata/unbundle.txt new file mode 100644 index 0000000000..8d4651e874 --- /dev/null +++ b/src/applications/diffusion/ssh/__tests__/hgwiredata/unbundle.txt @@ -0,0 +1,28 @@ +unbundle +heads 53 +686173686564 8022e00be6886fcf1be8f57f96c78aa924967f8320 +aaaaaaaaaaaaaaaaaaaa20 +bbbbbbbbbbbbbbbbbbbb0 + +~~~~~~~~~~ +[ + { + "command" : "unbundle", + "arguments" : { + "heads" : "686173686564 8022e00be6886fcf1be8f57f96c78aa924967f83" + }, + "raw" : "unbundle\nheads 53\n686173686564 8022e00be6886fcf1be8f57f96c78aa924967f83" + }, + { + "command" : "", + "raw" : "20\naaaaaaaaaaaaaaaaaaaa" + }, + { + "command" : "", + "raw" : "20\nbbbbbbbbbbbbbbbbbbbb" + }, + { + "command" : "", + "raw" : "0\n" + } +] diff --git a/src/applications/diffusion/view/DiffusionBranchTableView.php b/src/applications/diffusion/view/DiffusionBranchTableView.php index 001af93685..7cbdb0d089 100644 --- a/src/applications/diffusion/view/DiffusionBranchTableView.php +++ b/src/applications/diffusion/view/DiffusionBranchTableView.php @@ -6,12 +6,13 @@ final class DiffusionBranchTableView extends DiffusionView { private $commits = array(); public function setBranches(array $branches) { - assert_instances_of($branches, 'DiffusionBranchInformation'); + assert_instances_of($branches, 'DiffusionRepositoryRef'); $this->branches = $branches; return $this; } public function setCommits(array $commits) { + assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commits = mpull($commits, null, 'getCommitIdentifier'); return $this; } @@ -23,7 +24,7 @@ final class DiffusionBranchTableView extends DiffusionView { $rows = array(); $rowc = array(); foreach ($this->branches as $branch) { - $commit = idx($this->commits, $branch->getHeadCommitIdentifier()); + $commit = idx($this->commits, $branch->getCommitIdentifier()); if ($commit) { $details = $commit->getSummary(); $datetime = phabricator_datetime($commit->getEpoch(), $this->user); @@ -39,7 +40,7 @@ final class DiffusionBranchTableView extends DiffusionView { 'href' => $drequest->generateURI( array( 'action' => 'history', - 'branch' => $branch->getName(), + 'branch' => $branch->getShortName(), )) ), pht('History')), @@ -49,17 +50,17 @@ final class DiffusionBranchTableView extends DiffusionView { 'href' => $drequest->generateURI( array( 'action' => 'browse', - 'branch' => $branch->getName(), + 'branch' => $branch->getShortName(), )), ), - $branch->getName()), + $branch->getShortName()), self::linkCommit( $drequest->getRepository(), - $branch->getHeadCommitIdentifier()), + $branch->getCommitIdentifier()), $datetime, AphrontTableView::renderSingleDisplayLine($details), ); - if ($branch->getName() == $current_branch) { + if ($branch->getShortName() == $current_branch) { $rowc[] = 'highlighted'; } else { $rowc[] = null; diff --git a/src/applications/diffusion/view/DiffusionBrowseTableView.php b/src/applications/diffusion/view/DiffusionBrowseTableView.php index b0efba9c1b..cf64a18d7c 100644 --- a/src/applications/diffusion/view/DiffusionBrowseTableView.php +++ b/src/applications/diffusion/view/DiffusionBrowseTableView.php @@ -74,12 +74,12 @@ final class DiffusionBrowseTableView extends DiffusionView { $lint = self::loadLintMessagesCount($drequest); if ($lint !== null) { - $return['lint'] = hsprintf( - '%s', - $drequest->generateURI(array( + $return['lint'] = phutil_tag( + 'a', + array('href' => $drequest->generateURI(array( 'action' => 'lint', 'lint' => null, - )), + ))), number_format($lint)); } diff --git a/src/applications/diffusion/view/DiffusionCommentView.php b/src/applications/diffusion/view/DiffusionCommentView.php index a5dc0c59bb..defaf49a07 100644 --- a/src/applications/diffusion/view/DiffusionCommentView.php +++ b/src/applications/diffusion/view/DiffusionCommentView.php @@ -139,12 +139,12 @@ final class DiffusionCommentView extends AphrontView { if (!strlen($comment->getContent()) && empty($this->inlineComments)) { return null; } else { - return hsprintf( - '
%s%s
', + return phutil_tag_div('phabricator-remarkup', array( $engine->getOutput( $comment, PhabricatorAuditComment::MARKUP_FIELD_BODY), - $this->renderInlines()); + $this->renderInlines(), + )); } } diff --git a/src/applications/diviner/application/PhabricatorApplicationDiviner.php b/src/applications/diviner/application/PhabricatorApplicationDiviner.php index d4dbed044a..a0b140ae77 100644 --- a/src/applications/diviner/application/PhabricatorApplicationDiviner.php +++ b/src/applications/diviner/application/PhabricatorApplicationDiviner.php @@ -58,11 +58,12 @@ final class PhabricatorApplicationDiviner extends PhabricatorApplication { } if ($application && $application->getHelpURI()) { - $item = new PHUIListItemView(); - $item->setName(pht('%s Help', $application->getName())); - $item->addClass('core-menu-item'); - $item->setIcon('help'); - $item->setHref($application->getHelpURI()); + $item = id(new PHUIListItemView()) + ->setName(pht('%s Help', $application->getName())) + ->addClass('core-menu-item') + ->setIcon('info-sm') + ->setOrder(200) + ->setHref($application->getHelpURI()); $items[] = $item; } diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index d3895fc4ad..d4c5f54102 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -59,25 +59,22 @@ final class DivinerAtomController extends DivinerController { $atom = $symbol->getAtom(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($book->getShortTitle()) - ->setHref('/book/'.$book->getName().'/')); + $crumbs->addTextCrumb( + $book->getShortTitle(), + '/book/'.$book->getName().'/'); $atom_short_title = $atom->getDocblockMetaValue( 'short', $symbol->getTitle()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($atom_short_title)); + $crumbs->addTextCrumb($atom_short_title); $header = id(new PHUIHeaderView()) ->setHeader($this->renderFullSignature($symbol)) ->addTag( - id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLUE) ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))); $properties = id(new PHUIPropertyListView()); @@ -209,9 +206,9 @@ final class DivinerAtomController extends DivinerController { $inherited = $spec['inherited']; if ($inherited) { $method_header->addTag( - id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_GREY) + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_GREY) ->setName(pht('Inherited'))); } @@ -294,8 +291,8 @@ final class DivinerAtomController extends DivinerController { } private function renderAtomTag(DivinerLiveSymbol $symbol) { - return id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_OBJECT) + return id(new PHUITagView()) + ->setType(PHUITagView::TYPE_OBJECT) ->setName($symbol->getName()) ->setHref($symbol->getURI()); } diff --git a/src/applications/diviner/controller/DivinerBookController.php b/src/applications/diviner/controller/DivinerBookController.php index a3aeaedfc5..0ab122e801 100644 --- a/src/applications/diviner/controller/DivinerBookController.php +++ b/src/applications/diviner/controller/DivinerBookController.php @@ -27,10 +27,9 @@ final class DivinerBookController extends DivinerController { $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($book->getShortTitle()) - ->setHref('/book/'.$book->getName().'/')); + $crumbs->addTextCrumb( + $book->getShortTitle(), + '/book/'.$book->getName().'/'); $header = id(new PHUIHeaderView()) ->setHeader($book->getTitle()) diff --git a/src/applications/diviner/controller/DivinerLegacyController.php b/src/applications/diviner/controller/DivinerLegacyController.php index b38926b631..1efdb8c5d0 100644 --- a/src/applications/diviner/controller/DivinerLegacyController.php +++ b/src/applications/diviner/controller/DivinerLegacyController.php @@ -43,9 +43,7 @@ final class DivinerLegacyController extends DivinerController { } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Documentation'))); + $crumbs->addTextCrumb(pht('Documentation')); $header = id(new PHUIHeaderView()) ->setHeader(pht('Documentation')); diff --git a/src/applications/diviner/workflow/DivinerWorkflow.php b/src/applications/diviner/workflow/DivinerWorkflow.php index 2d79f7b0b7..83a5ac5e1d 100644 --- a/src/applications/diviner/workflow/DivinerWorkflow.php +++ b/src/applications/diviner/workflow/DivinerWorkflow.php @@ -1,6 +1,6 @@ bookConfigPath; } - public function isExecutable() { - return true; - } - protected function getConfig($key, $default = null) { return idx($this->config, $key, $default); } diff --git a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php index d46c1e535d..fbb627c527 100644 --- a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php +++ b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php @@ -45,11 +45,11 @@ final class DoorkeeperTagsController extends PhabricatorController { $id = $id_map[$key]; - $tag = id(new PhabricatorTagView()) + $tag = id(new PHUITagView()) ->setID($id) ->setName($ref->getFullName()) ->setHref($uri) - ->setType(PhabricatorTagView::TYPE_OBJECT) + ->setType(PHUITagView::TYPE_OBJECT) ->setExternal(true) ->render(); diff --git a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php index 7bc65b6230..f4cf151a97 100644 --- a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php +++ b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php @@ -53,6 +53,12 @@ final class DoorkeeperImportEngine extends Phobject { $xobj = $ref ->newExternalObject() ->setImporterPHID($viewer->getPHID()); + + // NOTE: Fill the new external object into the object map, so we'll + // reference the same external object if more than one ref is the + // same. This prevents issues later where we double-populate + // external objects when handed duplicate refs. + $xobjs[$ref->getObjectKey()] = $xobj; } $ref->attachExternalObject($xobj); } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index b3f4f8e972..48f984372c 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -21,6 +21,16 @@ final class PhabricatorAsanaConfigOptions 'ID here.'. "\n\n". "NOTE: This feature is new and experimental.")), + $this->newOption('asana.project-ids', 'wild', null) + ->setSummary(pht("Optional Asana projects to use as application tags.")) + ->setDescription( + pht( + 'When Phabricator creates tasks in Asana, it can add the tasks '. + 'to Asana projects based on which application the corresponding '. + 'object in Phabricator comes from. For example, you can add code '. + 'reviews in Asana to a "Differential" project.'. + "\n\n". + 'NOTE: This feature is new and experimental.')) ); } @@ -31,6 +41,8 @@ final class PhabricatorAsanaConfigOptions switch ($option->getKey()) { case 'asana.workspace-id': break; + case 'asana.project-ids': + return $this->renderContextualProjectDescription($option, $request); default: return parent::renderContextualDescription($option, $request); } @@ -89,5 +101,48 @@ final class PhabricatorAsanaConfigOptions $viewer); } + private function renderContextualProjectDescription( + PhabricatorConfigOption $option, + AphrontRequest $request) { + + $viewer = $request->getUser(); + + $publishers = id(new PhutilSymbolLoader()) + ->setAncestorClass('DoorkeeperFeedStoryPublisher') + ->loadObjects(); + + $out = array(); + $out[] = pht( + 'To specify projects to add tasks to, enter a JSON map with publisher '. + 'class names as keys and a list of project IDs as values. For example, '. + 'to put Differential tasks into Asana projects with IDs `123` and '. + '`456`, enter:'. + "\n\n". + " lang=txt\n". + " {\n". + " \"DifferentialDoorkeeperRevisionFeedStoryPublisher\" : [123, 456]\n". + " }\n"); + + $out[] = pht('Available publishers class names are:'); + foreach ($publishers as $publisher) { + $out[] = ' - `'.get_class($publisher).'`'; + } + + $out[] = pht( + 'You can find an Asana project ID by clicking the project in Asana and '. + 'then examining the URL:'. + "\n\n". + " lang=txt\n". + " https://app.asana.com/0/12345678901234567890/111111111111111111\n". + " ^^^^^^^^^^^^^^^^^^^^\n". + " This is the ID to use.\n"); + + $out = implode("\n", $out); + + return PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($out), + 'default', + $viewer); + } } diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php index 815ac56b69..a8988e910f 100644 --- a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php +++ b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php @@ -46,11 +46,11 @@ abstract class DoorkeeperRemarkupRule if ($this->getEngine()->isTextMode()) { $view = $spec['href']; } else { - $view = id(new PhabricatorTagView()) + $view = id(new PHUITagView()) ->setID($tag_id) ->setName($spec['href']) ->setHref($spec['href']) - ->setType(PhabricatorTagView::TYPE_OBJECT) + ->setType(PHUITagView::TYPE_OBJECT) ->setExternal(true); } diff --git a/src/applications/doorkeeper/view/DoorkeeperTagView.php b/src/applications/doorkeeper/view/DoorkeeperTagView.php new file mode 100644 index 0000000000..66f759f43c --- /dev/null +++ b/src/applications/doorkeeper/view/DoorkeeperTagView.php @@ -0,0 +1,42 @@ +xobj = $xobj; + return $this; + } + + public function render() { + $xobj = $this->xobj; + if (!$xobj) { + throw new Exception('Call setExternalObject() before render()!'); + } + + $tag_id = celerity_generate_unique_node_id(); + + $href = $xobj->getObjectURI(); + + $spec = array( + 'id' => $tag_id, + 'ref' => array( + $xobj->getApplicationType(), + $xobj->getApplicationDomain(), + $xobj->getObjectType(), + $xobj->getObjectID(), + ), + ); + + Javelin::initBehavior('doorkeeper-tag', array('tags' => array($spec))); + + return id(new PHUITagView()) + ->setID($tag_id) + ->setHref($href) + ->setName($href) + ->setType(PHUITagView::TYPE_OBJECT) + ->setExternal(true); + } + +} diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php index 2a7aeb0aa8..6e71dab3b8 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php +++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php @@ -110,6 +110,8 @@ final class DoorkeeperFeedWorkerAsana extends DoorkeeperFeedWorker { 'assignee' => $owner_asana_id, ); + $projects = $this->getAsanaProjectIDs(); + $extra_data = array(); if ($main_edge) { $extra_data = $main_edge['data']; @@ -164,6 +166,7 @@ final class DoorkeeperFeedWorkerAsana extends DoorkeeperFeedWorker { 'POST', array( 'workspace' => $workspace_id, + 'projects' => $projects, // NOTE: We initially create parent tasks in the "Later" state but // don't update it afterward, even if the corresponding object // becomes actionable. The expectation is that users will prioritize @@ -214,6 +217,9 @@ final class DoorkeeperFeedWorkerAsana extends DoorkeeperFeedWorker { if (empty($extra_data['gone'])) { $this->addFollowers($oauth_token, $task_id, $silent_followers, true); $this->addFollowers($oauth_token, $task_id, $noisy_followers); + + // We're also going to synchronize project data here. + $this->addProjects($oauth_token, $task_id, $projects); } $dst_phid = $parent_ref->getExternalObject()->getPHID(); @@ -649,4 +655,37 @@ final class DoorkeeperFeedWorkerAsana extends DoorkeeperFeedWorker { $data); } + private function getAsanaProjectIDs() { + $project_ids = array(); + + $publisher = $this->getPublisher(); + $config = PhabricatorEnv::getEnvConfig('asana.project-ids'); + if (is_array($config)) { + $ids = idx($config, get_class($publisher)); + if (is_array($ids)) { + foreach ($ids as $id) { + if (is_scalar($id)) { + $project_ids[] = $id; + } + } + } + } + + return $project_ids; + } + + private function addProjects( + $oauth_token, + $task_id, + array $project_ids) { + foreach ($project_ids as $project_id) { + $data = array('project' => $project_id); + $this->makeAsanaAPICall( + $oauth_token, + "tasks/{$task_id}/addProject", + 'POST', + $data); + } + } + } diff --git a/src/applications/drydock/application/PhabricatorApplicationDrydock.php b/src/applications/drydock/application/PhabricatorApplicationDrydock.php index 02c41e2e2b..fc2e39c4b4 100644 --- a/src/applications/drydock/application/PhabricatorApplicationDrydock.php +++ b/src/applications/drydock/application/PhabricatorApplicationDrydock.php @@ -7,7 +7,7 @@ final class PhabricatorApplicationDrydock extends PhabricatorApplication { } public function getShortDescription() { - return 'Allocate Software Resources'; + return pht('Allocate Software Resources'); } public function getIconName() { @@ -33,20 +33,42 @@ final class PhabricatorApplicationDrydock extends PhabricatorApplication { public function getRoutes() { return array( '/drydock/' => array( - '' => 'DrydockResourceListController', + '' => 'DrydockConsoleController', + 'blueprint/' => array( + '(?:query/(?P[^/]+)/)?' => 'DrydockBlueprintListController', + '(?P[1-9]\d*)/' => 'DrydockBlueprintViewController', + 'create/' => 'DrydockBlueprintCreateController', + 'edit/(?:(?P[1-9]\d*)/)?' => 'DrydockBlueprintEditController', + ), 'resource/' => array( - '' => 'DrydockResourceListController', + '(?:query/(?P[^/]+)/)?' => 'DrydockResourceListController', '(?P[1-9]\d*)/' => 'DrydockResourceViewController', '(?P[1-9]\d*)/close/' => 'DrydockResourceCloseController', ), 'lease/' => array( - '' => 'DrydockLeaseListController', + '(?:query/(?P[^/]+)/)?' => 'DrydockLeaseListController', '(?P[1-9]\d*)/' => 'DrydockLeaseViewController', '(?P[1-9]\d*)/release/' => 'DrydockLeaseReleaseController', ), - 'log/' => 'DrydockLogController', + 'log/' => array( + '(?:query/(?P[^/]+)/)?' => 'DrydockLogListController', + ), ), ); } + protected function getCustomCapabilities() { + return array( + DrydockCapabilityDefaultView::CAPABILITY => array( + ), + DrydockCapabilityDefaultEdit::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), + DrydockCapabilityCreateBlueprints::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), + ); + } + + } diff --git a/src/applications/drydock/blueprint/DrydockBlueprint.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php similarity index 90% rename from src/applications/drydock/blueprint/DrydockBlueprint.php rename to src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index c00413775c..8c4246aa8a 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprint.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -5,10 +5,11 @@ * @task resource Resource Allocation * @task log Logging */ -abstract class DrydockBlueprint { +abstract class DrydockBlueprintImplementation { private $activeResource; private $activeLease; + private $instance; abstract public function getType(); abstract public function getInterface( @@ -18,14 +19,18 @@ abstract class DrydockBlueprint { abstract public function isEnabled(); + abstract public function getBlueprintName(); + abstract public function getDescription(); + public function getBlueprintClass() { return get_class($this); } protected function loadLease($lease_id) { + // TODO: Get rid of this? $query = id(new DrydockLeaseQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($lease_id)) - ->needResources(true) ->execute(); $lease = idx($query, $lease_id); @@ -37,6 +42,20 @@ abstract class DrydockBlueprint { return $lease; } + protected function getInstance() { + if (!$this->instance) { + throw new Exception( + "Attach the blueprint instance to the implementation."); + } + + return $this->instance; + } + + public function attachInstance(DrydockBlueprint $instance) { + $this->instance = $instance; + return $this; + } + /* -( Lease Acquisition )-------------------------------------------------- */ @@ -104,6 +123,7 @@ abstract class DrydockBlueprint { $resource->beginReadLocking(); $resource->reload(); + // TODO: Policy stuff. $other_leases = id(new DrydockLease())->loadAllWhere( 'status IN (%Ld) AND resourceID = %d', array( @@ -343,13 +363,13 @@ abstract class DrydockBlueprint { } - public static function getAllBlueprints() { + public static function getAllBlueprintImplementations() { static $list = null; if ($list === null) { $blueprints = id(new PhutilSymbolLoader()) ->setType('class') - ->setAncestorClass('DrydockBlueprint') + ->setAncestorClass('DrydockBlueprintImplementation') ->setConcreteOnly(true) ->selectAndLoadSymbols(); $list = ipull($blueprints, 'name', 'name'); @@ -361,21 +381,26 @@ abstract class DrydockBlueprint { return $list; } - public static function getAllBlueprintsForResource($type) { + public static function getAllBlueprintImplementationsForResource($type) { static $groups = null; if ($groups === null) { - $groups = mgroup(self::getAllBlueprints(), 'getType'); + $groups = mgroup(self::getAllBlueprintImplementations(), 'getType'); } return idx($groups, $type, array()); } + public static function getNamedImplementation($class) { + return idx(self::getAllBlueprintImplementations(), $class); + } + protected function newResourceTemplate($name) { - $resource = new DrydockResource(); - $resource->setBlueprintClass($this->getBlueprintClass()); - $resource->setType($this->getType()); - $resource->setStatus(DrydockResourceStatus::STATUS_PENDING); - $resource->setName($name); - $resource->save(); + $resource = id(new DrydockResource()) + ->setBlueprintPHID($this->getInstance()->getPHID()) + ->setBlueprintClass($this->getBlueprintClass()) + ->setType($this->getType()) + ->setStatus(DrydockResourceStatus::STATUS_PENDING) + ->setName($name) + ->save(); $this->activeResource = $resource; diff --git a/src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php b/src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php similarity index 81% rename from src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php rename to src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php index f0d8cccc08..b1bfac2395 100644 --- a/src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php +++ b/src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php @@ -1,10 +1,19 @@ newResourceTemplate('Host (localhost)'); $resource->setStatus(DrydockResourceStatus::STATUS_OPEN); $resource->setAttribute('path', $path); + $resource->setAttribute('remote', "false"); + $resource->setAttribute('preallocated', "false"); $resource->save(); return $resource; @@ -42,7 +53,7 @@ final class DrydockLocalHostBlueprint extends DrydockBlueprint { protected function canAllocateLease( DrydockResource $resource, DrydockLease $lease) { - return true; + return false; } protected function shouldAllocateLease( diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php new file mode 100644 index 0000000000..86a2b15d9d --- /dev/null +++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php @@ -0,0 +1,123 @@ +getAttribute('platform') === $resource->getAttribute('platform'); + } + + protected function shouldAllocateLease( + DrydockResource $resource, + DrydockLease $lease, + array $other_leases) { + return true; + } + + protected function executeAcquireLease( + DrydockResource $resource, + DrydockLease $lease) { + + // Because preallocated resources are manually created, we should verify + // we have all the information we need. + PhutilTypeSpec::checkMap( + $resource->getAttributesForTypeSpec( + array('platform', 'host', 'port', 'credential', 'path')), + array( + 'platform' => 'string', + 'host' => 'string', + 'port' => 'string', // Value is a string from the command line + 'credential' => 'string', + 'path' => 'string', + )); + $v_platform = $resource->getAttribute('platform'); + $v_path = $resource->getAttribute('path'); + + // Similar to DrydockLocalHostBlueprint, we create a folder + // on the remote host that the lease can use. + + $lease_id = $lease->getID(); + + // Can't use DIRECTORY_SEPERATOR here because that is relevant to + // the platform we're currently running on, not the platform we are + // remoting to. + $separator = '/'; + if ($v_platform === 'windows') { + $separator = '\\'; + } + + // Clean up the directory path a little. + $base_path = rtrim($v_path, '/'); + $base_path = rtrim($base_path, '\\'); + $full_path = $base_path.$separator.$lease_id; + + $cmd = $lease->getInterface('command'); + + if ($v_platform !== 'windows') { + $cmd->execx('mkdir %s', $full_path); + } else { + // Windows is terrible. The mkdir command doesn't even support putting + // the path in quotes. IN QUOTES. ARGUHRGHUGHHGG!! Do some terribly + // inaccurate sanity checking since we can't safely escape the path. + if (preg_match('/^[A-Z]\\:\\\\[a-zA-Z0-9\\\\\\ ]/', $full_path) === 0) { + throw new Exception( + 'Unsafe path detected for Windows platform: "'.$full_path.'".'); + } + $cmd->execx('mkdir %C', $full_path); + } + + $lease->setAttribute('path', $full_path); + } + + public function getType() { + return 'host'; + } + + public function getInterface( + DrydockResource $resource, + DrydockLease $lease, + $type) { + + switch ($type) { + case 'command': + return id(new DrydockSSHCommandInterface()) + ->setConfiguration(array( + 'host' => $resource->getAttribute('host'), + 'port' => $resource->getAttribute('port'), + 'credential' => $resource->getAttribute('credential'), + 'platform' => $resource->getAttribute('platform'))); + case 'filesystem': + return id(new DrydockSFTPFilesystemInterface()) + ->setConfiguration(array( + 'host' => $resource->getAttribute('host'), + 'port' => $resource->getAttribute('port'), + 'credential' => $resource->getAttribute('credential'))); + } + + throw new Exception("No interface of type '{$type}'."); + } + +} diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprint.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php similarity index 85% rename from src/applications/drydock/blueprint/DrydockWorkingCopyBlueprint.php rename to src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index 8039f67037..ed92311e65 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprint.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -1,11 +1,20 @@ setResourceType('host') ->waitUntilActive(); @@ -58,8 +68,8 @@ final class DrydockWorkingCopyBlueprint extends DrydockBlueprint { $cmd = $host_lease->getInterface('command'); $cmd->execx( - 'git clone --origin origin %s %s', - $repository->getRemoteURI(), + 'git clone --origin origin %P %s', + $repository->getRemoteURIEnvelope(), $path); $this->log(pht('Complete.')); diff --git a/src/applications/drydock/capability/DrydockCapabilityCreateBlueprints.php b/src/applications/drydock/capability/DrydockCapabilityCreateBlueprints.php new file mode 100644 index 0000000000..3487385217 --- /dev/null +++ b/src/applications/drydock/capability/DrydockCapabilityCreateBlueprints.php @@ -0,0 +1,20 @@ + 'Pending', - self::STATUS_ACQUIRING => 'Acquiring', - self::STATUS_ACTIVE => 'Active', - self::STATUS_RELEASED => 'Released', - self::STATUS_BROKEN => 'Broken', - self::STATUS_EXPIRED => 'Expired', + $map = array( + self::STATUS_PENDING => pht('Pending'), + self::STATUS_ACQUIRING => pht('Acquiring'), + self::STATUS_ACTIVE => pht('Active'), + self::STATUS_RELEASED => pht('Released'), + self::STATUS_BROKEN => pht('Broken'), + self::STATUS_EXPIRED => pht('Expired'), ); - return idx($map, $status, 'Unknown'); + return idx($map, $status, pht('Unknown')); + } + + public static function getAllStatuses() { + return array( + self::STATUS_PENDING, + self::STATUS_ACQUIRING, + self::STATUS_ACTIVE, + self::STATUS_RELEASED, + self::STATUS_BROKEN, + self::STATUS_EXPIRED, + ); } } diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php index 0aa11c7b6f..7d11d93cba 100644 --- a/src/applications/drydock/constants/DrydockResourceStatus.php +++ b/src/applications/drydock/constants/DrydockResourceStatus.php @@ -9,15 +9,25 @@ final class DrydockResourceStatus extends DrydockConstants { const STATUS_DESTROYED = 4; public static function getNameForStatus($status) { - static $map = array( - self::STATUS_PENDING => 'Pending', - self::STATUS_OPEN => 'Open', - self::STATUS_CLOSED => 'Closed', - self::STATUS_BROKEN => 'Broken', - self::STATUS_DESTROYED => 'Destroyed', + $map = array( + self::STATUS_PENDING => pht('Pending'), + self::STATUS_OPEN => pht('Open'), + self::STATUS_CLOSED => pht('Closed'), + self::STATUS_BROKEN => pht('Broken'), + self::STATUS_DESTROYED => pht('Destroyed'), ); return idx($map, $status, 'Unknown'); } + public static function getAllStatuses() { + return array( + self::STATUS_PENDING, + self::STATUS_OPEN, + self::STATUS_CLOSED, + self::STATUS_BROKEN, + self::STATUS_DESTROYED, + ); + } + } diff --git a/src/applications/drydock/controller/DrydockBlueprintController.php b/src/applications/drydock/controller/DrydockBlueprintController.php new file mode 100644 index 0000000000..53bd24ebe4 --- /dev/null +++ b/src/applications/drydock/controller/DrydockBlueprintController.php @@ -0,0 +1,27 @@ +setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new DrydockBlueprintSearchEngine()) + ->setViewer($this->getRequest()->getUser()) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Blueprints'), + $this->getApplicationURI('blueprint/')); + return $crumbs; + } + +} diff --git a/src/applications/drydock/controller/DrydockBlueprintCreateController.php b/src/applications/drydock/controller/DrydockBlueprintCreateController.php new file mode 100644 index 0000000000..664ef2d2dc --- /dev/null +++ b/src/applications/drydock/controller/DrydockBlueprintCreateController.php @@ -0,0 +1,81 @@ +getRequest(); + $viewer = $request->getUser(); + + $this->requireApplicationCapability( + DrydockCapabilityCreateBlueprints::CAPABILITY); + + $implementations = + DrydockBlueprintImplementation::getAllBlueprintImplementations(); + + $errors = array(); + $e_blueprint = null; + + if ($request->isFormPost()) { + $class = $request->getStr('blueprint-type'); + if (!isset($implementations[$class])) { + $e_blueprint = pht('Required'); + $errors[] = pht('You must choose a blueprint type.'); + } + + if (!$errors) { + $edit_uri = $this->getApplicationURI('blueprint/edit/?class='.$class); + return id(new AphrontRedirectResponse())->setURI($edit_uri); + } + } + + $control = id(new AphrontFormRadioButtonControl()) + ->setName('blueprint-type') + ->setLabel(pht('Blueprint Type')) + ->setError($e_blueprint); + + foreach ($implementations as $implementation_name => $implementation) { + $disabled = !$implementation->isEnabled(); + + $control->addButton( + $implementation_name, + $implementation->getBlueprintName(), + array( + pht('Provides: %s', $implementation->getType()), + phutil_tag('br'), + phutil_tag('br'), + $implementation->getDescription(), + ), + $disabled ? 'disabled' : null, + $disabled); + } + + $title = pht('Create New Blueprint'); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('New Blueprint')); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($control) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($this->getApplicationURI('blueprint/')) + ->setValue(pht('Continue'))); + + $box = id(new PHUIObjectBoxView()) + ->setFormErrors($errors) + ->setHeaderText($title) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/drydock/controller/DrydockBlueprintEditController.php b/src/applications/drydock/controller/DrydockBlueprintEditController.php new file mode 100644 index 0000000000..f10359e519 --- /dev/null +++ b/src/applications/drydock/controller/DrydockBlueprintEditController.php @@ -0,0 +1,156 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$blueprint) { + return new Aphront404Response(); + } + + $impl = $blueprint->getImplementation(); + $cancel_uri = $this->getApplicationURI('blueprint/'.$this->id.'/'); + } else { + $this->requireApplicationCapability( + DrydockCapabilityCreateBlueprints::CAPABILITY); + + $class = $request->getStr('class'); + + $impl = DrydockBlueprintImplementation::getNamedImplementation($class); + if (!$impl || !$impl->isEnabled()) { + return new Aphront400Response(); + } + + $blueprint = DrydockBlueprint::initializeNewBlueprint($viewer); + $blueprint->setClassName($class); + $cancel_uri = $this->getApplicationURI('blueprint/'); + } + + $v_name = $blueprint->getBlueprintName(); + $e_name = true; + $errors = array(); + + if ($request->isFormPost()) { + $v_view_policy = $request->getStr('viewPolicy'); + $v_edit_policy = $request->getStr('editPolicy'); + $v_name = $request->getStr('name'); + if (!strlen($v_name)) { + $e_name = pht('Required'); + $errors[] = pht('You must name this blueprint.'); + } + + if (!$errors) { + $xactions = array(); + + $xactions[] = id(new DrydockBlueprintTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($v_view_policy); + + $xactions[] = id(new DrydockBlueprintTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($v_edit_policy); + + $xactions[] = id(new DrydockBlueprintTransaction()) + ->setTransactionType(DrydockBlueprintTransaction::TYPE_NAME) + ->setNewValue($v_name); + + $editor = id(new DrydockBlueprintEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($blueprint, $xactions); + + $id = $blueprint->getID(); + $save_uri = $this->getApplicationURI("blueprint/{$id}/"); + + return id(new AphrontRedirectResponse())->setURI($save_uri); + } + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($blueprint) + ->execute(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('class', $request->getStr('class')) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Blueprint Type')) + ->setValue($impl->getBlueprintName())) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($blueprint) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($blueprint) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies)); + + $crumbs = $this->buildApplicationCrumbs(); + + if ($blueprint->getID()) { + $title = pht('Edit Blueprint'); + $header = pht('Edit Blueprint %d', $blueprint->getID()); + $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); + $crumbs->addTextCrumb(pht('Edit')); + $submit = pht('Save Blueprint'); + } else { + $title = pht('New Blueprint'); + $header = pht('New Blueprint'); + $crumbs->addTextCrumb(pht('New Blueprint')); + $submit = pht('Create Blueprint'); + } + + $form->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($submit) + ->addCancelButton($cancel_uri)); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->setFormErrors($errors) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/drydock/controller/DrydockBlueprintListController.php b/src/applications/drydock/controller/DrydockBlueprintListController.php new file mode 100644 index 0000000000..c20d9a3715 --- /dev/null +++ b/src/applications/drydock/controller/DrydockBlueprintListController.php @@ -0,0 +1,67 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new DrydockBlueprintSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function renderResultsList( + array $blueprints, + PhabricatorSavedQuery $query) { + assert_instances_of($blueprints, 'DrydockBlueprint'); + + $viewer = $this->getRequest()->getUser(); + $view = new PHUIObjectItemListView(); + + foreach ($blueprints as $blueprint) { + $item = id(new PHUIObjectItemView()) + ->setHeader($blueprint->getBlueprintName()) + ->setHref($this->getApplicationURI('/blueprint/'.$blueprint->getID())) + ->setObjectName(pht('Blueprint %d', $blueprint->getID())); + + if (!$blueprint->getImplementation()->isEnabled()) { + $item->setDisabled(true); + } + + $item->addAttribute($blueprint->getImplementation()->getBlueprintName()); + + $view->addItem($item); + } + + return $view; + } + + public function buildApplicationCrumbs() { + $can_create = $this->hasApplicationCapability( + DrydockCapabilityCreateBlueprints::CAPABILITY); + + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('New Blueprint')) + ->setHref($this->getApplicationURI('/blueprint/create/')) + ->setDisabled(!$can_create) + ->setWorkflow(!$can_create) + ->setIcon('create')); + return $crumbs; + } + +} diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php new file mode 100644 index 0000000000..4661c6029b --- /dev/null +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -0,0 +1,125 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$blueprint) { + return new Aphront404Response(); + } + + $title = $blueprint->getBlueprintName(); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($blueprint); + + $actions = $this->buildActionListView($blueprint); + $properties = $this->buildPropertyListView($blueprint, $actions); + + $blueprint_uri = 'blueprint/'.$blueprint->getID().'/'; + $blueprint_uri = $this->getApplicationURI($blueprint_uri); + + $resources = id(new DrydockResourceQuery()) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->setViewer($viewer) + ->execute(); + + $resource_list = $this->buildResourceListView($resources); + $resource_list->setNoDataString(pht('This blueprint has no resources.')); + + $pager = new AphrontPagerView(); + $pager->setURI(new PhutilURI($blueprint_uri), 'offset'); + $pager->setOffset($request->getInt('offset')); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setActionList($actions); + $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + $xactions = id(new DrydockBlueprintTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($blueprint->getPHID())) + ->execute(); + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer); + + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($blueprint->getPHID()) + ->setTransactions($xactions) + ->setMarkupEngine($engine); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + $resource_list, + $timeline, + ), + array( + 'device' => true, + 'title' => $title, + )); + + } + + private function buildActionListView(DrydockBlueprint $blueprint) { + $viewer = $this->getRequest()->getUser(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setObject($blueprint); + + $uri = '/blueprint/edit/'.$blueprint->getID().'/'; + $uri = $this->getApplicationURI($uri); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $blueprint, + PhabricatorPolicyCapability::CAN_EDIT); + + $view->addAction( + id(new PhabricatorActionView()) + ->setHref($uri) + ->setName(pht('Edit Blueprint')) + ->setIcon('edit') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + return $view; + } + + private function buildPropertyListView( + DrydockBlueprint $blueprint, + PhabricatorActionListView $actions) { + + $view = new PHUIPropertyListView(); + $view->setActionList($actions); + + $view->addProperty( + pht('Type'), + $blueprint->getImplementation()->getBlueprintName()); + + return $view; + } + +} diff --git a/src/applications/drydock/controller/DrydockConsoleController.php b/src/applications/drydock/controller/DrydockConsoleController.php new file mode 100644 index 0000000000..dca47dd588 --- /dev/null +++ b/src/applications/drydock/controller/DrydockConsoleController.php @@ -0,0 +1,80 @@ +setBaseURI(new PhutilURI($this->getApplicationURI())); + + // These are only used on mobile. + + $nav->addFilter('blueprint', pht('Blueprints')); + $nav->addFilter('resource', pht('Resources')); + $nav->addFilter('lease', pht('Leases')); + $nav->addFilter('log', pht('Logs')); + + $nav->selectFilter(null); + + return $nav; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Blueprints')) + ->setHref($this->getApplicationURI('blueprint/')) + ->addAttribute( + pht( + 'Configure blueprints so Drydock can build resources, like '. + 'hosts and working copies.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Resources')) + ->setHref($this->getApplicationURI('resource/')) + ->addAttribute( + pht( + 'View and manage resources Drydock has built, like hosts.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Leases')) + ->setHref($this->getApplicationURI('lease/')) + ->addAttribute( + pht( + 'Manage leases on resources.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Logs')) + ->setHref($this->getApplicationURI('log/')) + ->addAttribute( + pht( + 'View logs.'))); + + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Console')); + + return $this->buildApplicationPage( + array( + $crumbs, + $menu, + ), + array( + 'title' => pht('Drydock Console'), + 'device' => true, + )); + } + +} diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php index 0e3929761b..567961c5bb 100644 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -2,20 +2,10 @@ abstract class DrydockController extends PhabricatorController { - final protected function buildSideNav($selected) { - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI('/drydock/')); - $nav->addFilter('resource', 'Resources'); - $nav->addFilter('lease', 'Leases'); - $nav->addFilter('log', 'Logs'); - - $nav->selectFilter($selected, 'resource'); - - return $nav; - } + abstract function buildSideNavView(); public function buildApplicationMenu() { - return $this->buildSideNav(null)->getMenu(); + return $this->buildSideNavView()->getMenu(); } protected function buildLogTableView(array $logs) { @@ -79,11 +69,12 @@ abstract class DrydockController extends PhabricatorController { protected function buildLeaseListView(array $leases) { assert_instances_of($leases, 'DrydockLease'); - $user = $this->getRequest()->getUser(); + $viewer = $this->getRequest()->getUser(); $view = new PHUIObjectItemListView(); foreach ($leases as $lease) { $item = id(new PHUIObjectItemView()) + ->setUser($viewer) ->setHeader($lease->getLeaseName()) ->setHref($this->getApplicationURI('/lease/'.$lease->getID().'/')); @@ -106,9 +97,7 @@ abstract class DrydockController extends PhabricatorController { $status = DrydockLeaseStatus::getNameForStatus($lease->getStatus()); $item->addAttribute($status); - - $date_created = phabricator_date($lease->getDateCreated(), $user); - $item->addAttribute(pht('Created on %s', $date_created)); + $item->setEpoch($lease->getDateCreated()); if ($lease->isActive()) { $item->setBarColor('green'); diff --git a/src/applications/drydock/controller/DrydockLeaseController.php b/src/applications/drydock/controller/DrydockLeaseController.php new file mode 100644 index 0000000000..7319eec4cd --- /dev/null +++ b/src/applications/drydock/controller/DrydockLeaseController.php @@ -0,0 +1,27 @@ +setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new DrydockLeaseSearchEngine()) + ->setViewer($this->getRequest()->getUser()) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Leases'), + $this->getApplicationURI('lease/')); + return $crumbs; + } + +} diff --git a/src/applications/drydock/controller/DrydockLeaseListController.php b/src/applications/drydock/controller/DrydockLeaseListController.php index 8ae5e4add5..2b3111ebe1 100644 --- a/src/applications/drydock/controller/DrydockLeaseListController.php +++ b/src/applications/drydock/controller/DrydockLeaseListController.php @@ -1,49 +1,34 @@ queryKey = idx($data, 'queryKey'); + } public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new DrydockLeaseSearchEngine()) + ->setNavigation($this->buildSideNavView()); - $nav = $this->buildSideNav('lease'); + return $this->delegateToController($controller); + } - $pager = new AphrontPagerView(); - $pager->setURI(new PhutilURI('/drydock/lease/'), 'offset'); - $pager->setOffset($request->getInt('offset')); - - $leases = id(new DrydockLeaseQuery()) - ->needResources(true) - ->executeWithOffsetPager($pager); - - $title = pht('Leases'); - - $header = id(new PHUIHeaderView()) - ->setHeader($title); - - $lease_list = $this->buildLeaseListView($leases); - - $nav->appendChild( - array( - $header, - $lease_list, - $pager, - )); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); - $nav->setCrumbs($crumbs); - - return $this->buildApplicationPage( - $nav, - array( - 'device' => true, - 'title' => $title, - )); + public function renderResultsList( + array $leases, + PhabricatorSavedQuery $query) { + assert_instances_of($leases, 'DrydockLease'); + return $this->buildLeaseListView($leases); } } diff --git a/src/applications/drydock/controller/DrydockLeaseReleaseController.php b/src/applications/drydock/controller/DrydockLeaseReleaseController.php index dd238e2467..580e0660e4 100644 --- a/src/applications/drydock/controller/DrydockLeaseReleaseController.php +++ b/src/applications/drydock/controller/DrydockLeaseReleaseController.php @@ -1,6 +1,6 @@ getRequest(); $user = $request->getUser(); - $lease = id(new DrydockLease())->load($this->id); + $lease = id(new DrydockLeaseQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->executeOne(); if (!$lease) { return new Aphront404Response(); } @@ -45,7 +48,7 @@ final class DrydockLeaseReleaseController extends DrydockController { return id(new AphrontDialogResponse())->setDialog($dialog); } - $resource = $lease->loadResource(); + $resource = $lease->getResource(); $blueprint = $resource->getBlueprint(); $blueprint->releaseLease($resource, $lease); diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index 78f5447c3a..cbf83d6120 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -1,6 +1,6 @@ getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); - $lease = id(new DrydockLease())->load($this->id); + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); if (!$lease) { return new Aphront404Response(); } @@ -32,6 +35,7 @@ final class DrydockLeaseViewController extends DrydockController { $pager->setOffset($request->getInt('offset')); $logs = id(new DrydockLogQuery()) + ->setViewer($viewer) ->withLeaseIDs(array($lease->getID())) ->executeWithOffsetPager($pager); @@ -40,10 +44,7 @@ final class DrydockLeaseViewController extends DrydockController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($lease_uri)); + $crumbs->addTextCrumb($title, $lease_uri); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) diff --git a/src/applications/drydock/controller/DrydockLogController.php b/src/applications/drydock/controller/DrydockLogController.php index 1a378df3cc..9147385ef3 100644 --- a/src/applications/drydock/controller/DrydockLogController.php +++ b/src/applications/drydock/controller/DrydockLogController.php @@ -1,61 +1,27 @@ getRequest(); - $user = $request->getUser(); + public function buildSideNavView() { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - $nav = $this->buildSideNav('log'); + id(new DrydockLogSearchEngine()) + ->setViewer($this->getRequest()->getUser()) + ->addNavigationItems($nav->getMenu()); - $query = new DrydockLogQuery(); + $nav->selectFilter(null); - $resource_ids = $request->getStrList('resource'); - if ($resource_ids) { - $query->withResourceIDs($resource_ids); - } - - $lease_ids = $request->getStrList('lease'); - if ($lease_ids) { - $query->withLeaseIDs($lease_ids); - } - - $pager = new AphrontPagerView(); - $pager->setPageSize(500); - $pager->setOffset($request->getInt('offset')); - $pager->setURI($request->getRequestURI(), 'offset'); - - $logs = $query->executeWithOffsetPager($pager); - - $title = pht('Logs'); - - $header = id(new PHUIHeaderView()) - ->setHeader($title); - - $table = $this->buildLogTableView($logs); - $table->appendChild($pager); - - $nav->appendChild( - array( - $header, - $table, - $pager, - )); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI('/logs/'))); - $nav->setCrumbs($crumbs); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - 'device' => true, - )); + return $nav; + } + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Logs'), + $this->getApplicationURI('log/')); + return $crumbs; } } diff --git a/src/applications/drydock/controller/DrydockLogListController.php b/src/applications/drydock/controller/DrydockLogListController.php new file mode 100644 index 0000000000..6ccd4f6fb1 --- /dev/null +++ b/src/applications/drydock/controller/DrydockLogListController.php @@ -0,0 +1,34 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new DrydockLogSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function renderResultsList( + array $logs, + PhabricatorSavedQuery $query) { + assert_instances_of($logs, 'DrydockLog'); + + return $this->buildLogTableView($logs); + } + +} diff --git a/src/applications/drydock/controller/DrydockResourceCloseController.php b/src/applications/drydock/controller/DrydockResourceCloseController.php index 8b28ce0319..7963612ee4 100644 --- a/src/applications/drydock/controller/DrydockResourceCloseController.php +++ b/src/applications/drydock/controller/DrydockResourceCloseController.php @@ -1,6 +1,6 @@ getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); - $resource = id(new DrydockResource())->load($this->id); + $resource = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); if (!$resource) { return new Aphront404Response(); } @@ -22,7 +25,7 @@ final class DrydockResourceCloseController extends DrydockController { if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Resource Not Open')) ->appendChild(phutil_tag('p', array(), pht( 'You can only close "open" resources.'))) @@ -31,22 +34,22 @@ final class DrydockResourceCloseController extends DrydockController { return id(new AphrontDialogResponse())->setDialog($dialog); } - if (!$request->isDialogFormPost()) { - $dialog = id(new AphrontDialogView()) - ->setUser($user) - ->setTitle(pht('Really close resource?')) - ->appendChild(phutil_tag('p', array(), pht( - 'Closing a resource releases all leases and destroys the '. - 'resource. It can not be undone. Continue?'))) - ->addSubmitButton(pht('Close Resource')) - ->addCancelButton($resource_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); + if ($request->isFormPost()) { + $resource->closeResource(); + return id(new AphrontReloadResponse())->setURI($resource_uri); } - $resource->closeResource(); + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Really close resource?')) + ->appendChild( + pht( + 'Closing a resource releases all leases and destroys the '. + 'resource. It can not be undone. Continue?')) + ->addSubmitButton(pht('Close Resource')) + ->addCancelButton($resource_uri); - return id(new AphrontReloadResponse())->setURI($resource_uri); + return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/drydock/controller/DrydockResourceController.php b/src/applications/drydock/controller/DrydockResourceController.php new file mode 100644 index 0000000000..74e869e789 --- /dev/null +++ b/src/applications/drydock/controller/DrydockResourceController.php @@ -0,0 +1,27 @@ +setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new DrydockResourceSearchEngine()) + ->setViewer($this->getRequest()->getUser()) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Resources'), + $this->getApplicationURI('resource/')); + return $crumbs; + } + +} diff --git a/src/applications/drydock/controller/DrydockResourceListController.php b/src/applications/drydock/controller/DrydockResourceListController.php index c94d64c085..fe80a314cb 100644 --- a/src/applications/drydock/controller/DrydockResourceListController.php +++ b/src/applications/drydock/controller/DrydockResourceListController.php @@ -1,45 +1,34 @@ queryKey = idx($data, 'queryKey'); + } public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new DrydockResourceSearchEngine()) + ->setNavigation($this->buildSideNavView()); - $title = pht('Resources'); + return $this->delegateToController($controller); + } - $resource_header = id(new PHUIHeaderView()) - ->setHeader($title); - - $pager = new AphrontPagerView(); - $pager->setURI(new PhutilURI('/drydock/resource/'), 'offset'); - $resources = id(new DrydockResourceQuery()) - ->executeWithOffsetPager($pager); - - $resource_list = $this->buildResourceListView($resources); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); - - $nav = $this->buildSideNav('resource'); - $nav->setCrumbs($crumbs); - $nav->appendChild( - array( - $resource_header, - $resource_list, - $pager, - )); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - 'device' => true, - )); + public function renderResultsList( + array $resources, + PhabricatorSavedQuery $query) { + assert_instances_of($resources, 'DrydockResource'); + return $this->buildResourceListView($resources); } } diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index 60232117b4..e8e7340a88 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -1,6 +1,6 @@ getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); - $resource = id(new DrydockResource())->load($this->id); + $resource = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); if (!$resource) { return new Aphront404Response(); } @@ -29,13 +32,10 @@ final class DrydockResourceViewController extends DrydockController { $resource_uri = $this->getApplicationURI($resource_uri); $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) ->withResourceIDs(array($resource->getID())) - ->needResources(true) ->execute(); - $lease_header = id(new PHUIHeaderView()) - ->setHeader(pht('Leases')); - $lease_list = $this->buildLeaseListView($leases); $lease_list->setNoDataString(pht('This resource has no leases.')); @@ -44,6 +44,7 @@ final class DrydockResourceViewController extends DrydockController { $pager->setOffset($request->getInt('offset')); $logs = id(new DrydockLogQuery()) + ->setViewer($viewer) ->withResourceIDs(array($resource->getID())) ->executeWithOffsetPager($pager); @@ -52,9 +53,7 @@ final class DrydockResourceViewController extends DrydockController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Resource %d', $resource->getID()))); + $crumbs->addTextCrumb(pht('Resource %d', $resource->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) @@ -64,7 +63,6 @@ final class DrydockResourceViewController extends DrydockController { array( $crumbs, $object_box, - $lease_header, $lease_list, $log_table, ), @@ -114,6 +112,11 @@ final class DrydockResourceViewController extends DrydockController { pht('Resource Type'), $resource->getType()); + // TODO: Load handle. + $view->addProperty( + pht('Blueprint'), + $resource->getBlueprintPHID()); + $attributes = $resource->getAttributes(); if ($attributes) { $view->addSectionHeader(pht('Attributes')); diff --git a/src/applications/drydock/editor/DrydockBlueprintEditor.php b/src/applications/drydock/editor/DrydockBlueprintEditor.php new file mode 100644 index 0000000000..6f4e7f32cd --- /dev/null +++ b/src/applications/drydock/editor/DrydockBlueprintEditor.php @@ -0,0 +1,65 @@ +getTransactionType()) { + case DrydockBlueprintTransaction::TYPE_NAME: + return $object->getBlueprintName(); + } + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case DrydockBlueprintTransaction::TYPE_NAME: + return $xaction->getNewValue(); + } + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case DrydockBlueprintTransaction::TYPE_NAME: + $object->setBlueprintName($xaction->getNewValue()); + break; + } + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + return; + } + + protected function extractFilePHIDsFromCustomTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + return array(); + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return false; + } + +} diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php index ff62eeb7a5..3ac20f0f41 100644 --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -2,21 +2,54 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { + private $passphraseSSHKey; + + private function openCredentialsIfNotOpen() { + if ($this->passphraseSSHKey !== null) { + return; + } + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($this->getConfig('credential'))) + ->needSecrets(true) + ->executeOne(); + + if ($credential->getProvidesType() !== + PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) { + throw new Exception("Only private key credentials are supported."); + } + + $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID( + $credential->getPHID(), + PhabricatorUser::getOmnipotentUser()); + } + public function getExecFuture($command) { + $this->openCredentialsIfNotOpen(); + $argv = func_get_args(); - $argv = $this->applyWorkingDirectoryToArgv($argv); + + // This assumes there's a UNIX shell living at the other + // end of the connection, which isn't the case for Windows machines. + if ($this->getConfig('platform') !== 'windows') { + $argv = $this->applyWorkingDirectoryToArgv($argv); + } $full_command = call_user_func_array('csprintf', $argv); - // NOTE: The "-t -t" is for psuedo-tty allocation so we can "sudo" on some - // systems, but maybe more trouble than it's worth? + if ($this->getConfig('platform') === 'windows') { + // On Windows platforms we need to execute cmd.exe explicitly since + // most commands are not really executables. + $full_command = 'C:\\Windows\\system32\\cmd.exe /C '.$full_command; + } return new ExecFuture( - 'ssh -t -t -o StrictHostKeyChecking=no -i %s %s@%s -- %s', - $this->getConfig('ssh-keyfile'), - $this->getConfig('user'), + 'ssh -o StrictHostKeyChecking=no -p %s -i %P %P@%s -- %s', + $this->getConfig('port'), + $this->passphraseSSHKey->getKeyfileEnvelope(), + $this->passphraseSSHKey->getUsernameEnvelope(), $this->getConfig('host'), $full_command); } - } diff --git a/src/applications/drydock/interface/filesystem/DrydockFilesystemInterface.php b/src/applications/drydock/interface/filesystem/DrydockFilesystemInterface.php new file mode 100644 index 0000000000..13b047626e --- /dev/null +++ b/src/applications/drydock/interface/filesystem/DrydockFilesystemInterface.php @@ -0,0 +1,24 @@ +passphraseSSHKey !== null) { + return; + } + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($this->getConfig('credential'))) + ->needSecrets(true) + ->executeOne(); + + if ($credential->getProvidesType() !== + PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) { + throw new Exception("Only private key credentials are supported."); + } + + $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID( + $credential->getPHID(), + PhabricatorUser::getOmnipotentUser()); + } + + private function getExecFuture($path) { + $this->openCredentialsIfNotOpen(); + + return new ExecFuture( + 'sftp -o "StrictHostKeyChecking no" -P %s -i %P %P@%s', + $this->getConfig('port'), + $this->passphraseSSHKey->getKeyfileEnvelope(), + $this->passphraseSSHKey->getUsernameEnvelope(), + $this->getConfig('host')); + } + + public function readFile($path) { + $target = new TempFile(); + $future = $this->getExecFuture($path); + $future->write(csprintf("get %s %s", $path, $target)); + $future->resolvex(); + return Filesystem::readFile($target); + } + + public function saveFile($path, $name) { + $data = $this->readFile($path); + $file = PhabricatorFile::newFromFileData($data); + $file->setName($name); + $file->save(); + return $file; + } + + public function writeFile($path, $data) { + $source = new TempFile(); + Filesystem::writeFile($source, $data); + $future = $this->getExecFuture($path); + $future->write(csprintf("put %s %s", $source, $path)); + $future->resolvex(); + } + +} diff --git a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php b/src/applications/drydock/management/DrydockManagementCloseWorkflow.php index 3d30f7f802..684a5333cf 100644 --- a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementCloseWorkflow.php @@ -25,8 +25,15 @@ final class DrydockManagementCloseWorkflow "Specify one or more resource IDs to close."); } + $viewer = $this->getViewer(); + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + foreach ($ids as $id) { - $resource = id(new DrydockResource())->load($id); + $resource = idx($resources, $id); if (!$resource) { $console->writeErr("Resource %d does not exist!\n", $id); } else if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { diff --git a/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php new file mode 100644 index 0000000000..82259d5d2b --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php @@ -0,0 +1,77 @@ +setName('create-resource') + ->setSynopsis('Create a resource manually.') + ->setArguments( + array( + array( + 'name' => 'name', + 'param' => 'resource_name', + 'help' => 'Resource name.', + ), + array( + 'name' => 'blueprint', + 'param' => 'blueprint_id', + 'help' => 'Blueprint ID.', + ), + array( + 'name' => 'attributes', + 'param' => 'name=value,...', + 'help' => 'Resource attributes.', + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $resource_name = $args->getArg('name'); + if (!$resource_name) { + throw new PhutilArgumentUsageException( + "Specify a resource name with `--name`."); + } + + $blueprint_id = $args->getArg('blueprint'); + if (!$blueprint_id) { + throw new PhutilArgumentUsageException( + "Specify a blueprint ID with `--blueprint`."); + } + + $attributes = $args->getArg('attributes'); + if ($attributes) { + $options = new PhutilSimpleOptions(); + $options->setCaseSensitive(true); + $attributes = $options->parse($attributes); + } + + $viewer = $this->getViewer(); + + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($blueprint_id)) + ->executeOne(); + if (!$blueprint) { + throw new PhutilArgumentUsageException( + "Specified blueprint does not exist."); + } + + $resource = id(new DrydockResource()) + ->setBlueprintPHID($blueprint->getPHID()) + ->setType($blueprint->getImplementation()->getType()) + ->setName($resource_name) + ->setStatus(DrydockResourceStatus::STATUS_OPEN); + if ($attributes) { + $resource->setAttributes($attributes); + } + $resource->save(); + + $console->writeOut("Created Resource %s\n", $resource->getID()); + return 0; + } + +} diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php index 9df072d88e..4b60ce2fab 100644 --- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php @@ -38,39 +38,16 @@ final class DrydockManagementLeaseWorkflow $attributes = $options->parse($attributes); } - $lease = new DrydockLease(); - $lease->setResourceType($resource_type); + PhabricatorWorker::setRunAllTasksInProcess(true); + + $lease = id(new DrydockLease()) + ->setResourceType($resource_type); if ($attributes) { $lease->setAttributes($attributes); } - $lease->queueForActivation(); - - $root = dirname(phutil_get_library_root('phabricator')); - $wait = new ExecFuture( - 'php -f %s wait-for-lease --id %s', - $root.'/scripts/drydock/drydock_control.php', - $lease->getID()); - - $cursor = 0; - foreach (Futures(array($wait))->setUpdateInterval(1) as $key => $future) { - if ($future) { - $future->resolvex(); - break; - } - - $logs = id(new DrydockLogQuery()) - ->withLeaseIDs(array($lease->getID())) - ->withAfterID($cursor) - ->setOrder(DrydockLogQuery::ORDER_ID) - ->execute(); - - if ($logs) { - foreach ($logs as $log) { - $console->writeErr("%s\n", $log->getMessage()); - } - $cursor = max(mpull($logs, 'getID')); - } - } + $lease + ->queueForActivation() + ->waitUntilActive(); $console->writeOut("Acquired Lease %s\n", $lease->getID()); return 0; diff --git a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php index 54b98bf62d..5913bda447 100644 --- a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php @@ -25,14 +25,21 @@ final class DrydockManagementReleaseWorkflow "Specify one or more lease IDs to release."); } + $viewer = $this->getViewer(); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + foreach ($ids as $id) { - $lease = id(new DrydockLease())->load($id); + $lease = idx($leases, $id); if (!$lease) { $console->writeErr("Lease %d does not exist!\n", $id); } else if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { $console->writeErr("Lease %d is not 'active'!\n", $id); } else { - $resource = $lease->loadResource(); + $resource = $lease->getResource(); $blueprint = $resource->getBlueprint(); $blueprint->releaseLease($resource, $lease); diff --git a/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php deleted file mode 100644 index 2ff52c3516..0000000000 --- a/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php +++ /dev/null @@ -1,40 +0,0 @@ -setName('wait-for-lease') - ->setSynopsis('Wait for a lease to become available.') - ->setArguments( - array( - array( - 'name' => 'id', - 'param' => 'lease_id', - 'help' => 'Lease ID to wait for.', - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $lease_id = $args->getArg('id'); - if (!$lease_id) { - throw new PhutilArgumentUsageException( - "Specify a lease ID with `--id`."); - } - - $console = PhutilConsole::getConsole(); - - $lease = id(new DrydockLease())->load($lease_id); - if (!$lease) { - $console->writeErr("No such lease.\n"); - return 1; - } else { - $lease->waitUntilActive(); - $console->writeErr("Lease active.\n"); - return 0; - } - } - -} diff --git a/src/applications/drydock/management/DrydockManagementWorkflow.php b/src/applications/drydock/management/DrydockManagementWorkflow.php index ad2cb70fb3..d4ffd0bfd3 100644 --- a/src/applications/drydock/management/DrydockManagementWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementWorkflow.php @@ -1,10 +1,6 @@ withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $blueprint = $objects[$phid]; + $id = $blueprint->getID(); + + $handle->setURI("/drydock/blueprint/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/phid/DrydockPHIDTypeLease.php b/src/applications/drydock/phid/DrydockPHIDTypeLease.php new file mode 100644 index 0000000000..f3f34be0c7 --- /dev/null +++ b/src/applications/drydock/phid/DrydockPHIDTypeLease.php @@ -0,0 +1,40 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $lease = $objects[$phid]; + $id = $lease->getID(); + + $handle->setURI("/drydock/lease/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/phid/DrydockPHIDTypeResource.php b/src/applications/drydock/phid/DrydockPHIDTypeResource.php new file mode 100644 index 0000000000..f6bc00f689 --- /dev/null +++ b/src/applications/drydock/phid/DrydockPHIDTypeResource.php @@ -0,0 +1,40 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $resource = $objects[$phid]; + $id = $resource->getID(); + + $handle->setURI("/drydock/resource/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/query/DrydockBlueprintQuery.php b/src/applications/drydock/query/DrydockBlueprintQuery.php new file mode 100644 index 0000000000..72d9d9ada8 --- /dev/null +++ b/src/applications/drydock/query/DrydockBlueprintQuery.php @@ -0,0 +1,65 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function loadPage() { + $table = new DrydockBlueprint(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT blueprint.* FROM %T blueprint %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + $blueprints = $table->loadAllFromArray($data); + + $implementations = + DrydockBlueprintImplementation::getAllBlueprintImplementations(); + + foreach ($blueprints as $blueprint) { + if (array_key_exists($blueprint->getClassName(), $implementations)) { + $blueprint->attachImplementation( + $implementations[$blueprint->getClassName()]); + } + } + + return $blueprints; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + return $this->formatWhereClause($where); + } + +} diff --git a/src/applications/drydock/query/DrydockBlueprintSearchEngine.php b/src/applications/drydock/query/DrydockBlueprintSearchEngine.php new file mode 100644 index 0000000000..c4eedc8863 --- /dev/null +++ b/src/applications/drydock/query/DrydockBlueprintSearchEngine.php @@ -0,0 +1,48 @@ + pht('All Blueprints'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/drydock/query/DrydockBlueprintTransactionQuery.php b/src/applications/drydock/query/DrydockBlueprintTransactionQuery.php new file mode 100644 index 0000000000..f2d70a692e --- /dev/null +++ b/src/applications/drydock/query/DrydockBlueprintTransactionQuery.php @@ -0,0 +1,10 @@ +resourceIDs = $ids; - return $this; - } + private $statuses; public function withIDs(array $ids) { $this->ids = $ids; return $this; } - public function needResources($need_resources) { - $this->needResources = $need_resources; + public function withPHIDs(array $phids) { + $this->phids = $phids; return $this; } - public function execute() { + public function withResourceIDs(array $ids) { + $this->resourceIDs = $ids; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function loadPage() { $table = new DrydockLease(); $conn_r = $table->establishConnection('r'); @@ -33,21 +39,31 @@ final class DrydockLeaseQuery extends PhabricatorOffsetPagedQuery { $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); - $leases = $table->loadAllFromArray($data); + return $table->loadAllFromArray($data); + } - if ($leases && $this->needResources) { - $resources = id(new DrydockResource())->loadAllWhere( - 'id IN (%Ld)', - mpull($leases, 'getResourceID')); + public function willFilterPage(array $leases) { + $resource_ids = array_filter(mpull($leases, 'getResourceID')); + if ($resource_ids) { + $resources = id(new DrydockResourceQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withIDs($resource_ids) + ->execute(); + } else { + $resources = array(); + } - foreach ($leases as $lease) { - if ($lease->getResourceID()) { - $resource = idx($resources, $lease->getResourceID()); - if ($resource) { - $lease->attachResource($resource); - } + foreach ($leases as $key => $lease) { + $resource = null; + if ($lease->getResourceID()) { + $resource = idx($resources, $lease->getResourceID()); + if (!$resource) { + unset($leases[$key]); + continue; } } + $lease->attachResource($resource); } return $leases; @@ -70,11 +86,23 @@ final class DrydockLeaseQuery extends PhabricatorOffsetPagedQuery { $this->ids); } + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->statuses) { + $where[] = qsprintf( + $conn_r, + 'status IN (%Ld)', + $this->statuses); + } + + $where[] = $this->buildPagingClause($conn_r); + return $this->formatWhereClause($where); } - private function buildOrderClause(AphrontDatabaseConnection $conn_r) { - return qsprintf($conn_r, 'ORDER BY id DESC'); - } - } diff --git a/src/applications/drydock/query/DrydockLeaseSearchEngine.php b/src/applications/drydock/query/DrydockLeaseSearchEngine.php new file mode 100644 index 0000000000..9f4d8bf549 --- /dev/null +++ b/src/applications/drydock/query/DrydockLeaseSearchEngine.php @@ -0,0 +1,81 @@ +setParameter( + 'statuses', + $this->readListFromRequest($request, 'statuses')); + + return $saved; + } + + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $query = id(new DrydockLeaseQuery()); + + $statuses = $saved->getParameter('statuses', array()); + if ($statuses) { + $query->withStatuses($statuses); + } + + return $query; + } + + public function buildSearchForm( + AphrontFormView $form, + PhabricatorSavedQuery $saved) { + + $statuses = $saved->getParameter('statuses', array()); + + $status_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Status')); + foreach (DrydockLeaseStatus::getAllStatuses() as $status) { + $status_control->addCheckbox( + 'statuses[]', + $status, + DrydockLeaseStatus::getNameForStatus($status), + in_array($status, $statuses)); + } + + $form + ->appendChild($status_control); + + } + + protected function getURI($path) { + return '/drydock/lease/'.$path; + } + + public function getBuiltinQueryNames() { + $names = array( + 'active' => pht('Active Leases'), + 'all' => pht('All Leases'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'active': + return $query->setParameter( + 'statuses', + array( + DrydockLeaseStatus::STATUS_PENDING, + DrydockLeaseStatus::STATUS_ACQUIRING, + DrydockLeaseStatus::STATUS_ACTIVE, + )); + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/drydock/query/DrydockLogQuery.php b/src/applications/drydock/query/DrydockLogQuery.php index cc9f8abee9..0488fb204a 100644 --- a/src/applications/drydock/query/DrydockLogQuery.php +++ b/src/applications/drydock/query/DrydockLogQuery.php @@ -1,14 +1,9 @@ resourceIDs = $ids; @@ -20,17 +15,7 @@ final class DrydockLogQuery extends PhabricatorOffsetPagedQuery { return $this; } - public function setOrder($order) { - $this->order = $order; - return $this; - } - - public function withAfterID($id) { - $this->afterID = $id; - return $this; - } - - public function execute() { + public function loadPage() { $table = new DrydockLog(); $conn_r = $table->establishConnection('r'); @@ -45,6 +30,64 @@ final class DrydockLogQuery extends PhabricatorOffsetPagedQuery { return $table->loadAllFromArray($data); } + public function willFilterPage(array $logs) { + $resource_ids = array_filter(mpull($logs, 'getResourceID')); + if ($resource_ids) { + $resources = id(new DrydockResourceQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withIDs($resource_ids) + ->execute(); + } else { + $resources = array(); + } + + foreach ($logs as $key => $log) { + $resource = null; + if ($log->getResourceID()) { + $resource = idx($resources, $log->getResourceID()); + if (!$resource) { + unset($logs[$key]); + continue; + } + } + $log->attachResource($resource); + } + + $lease_ids = array_filter(mpull($logs, 'getLeaseID')); + if ($lease_ids) { + $leases = id(new DrydockLeaseQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withIDs($lease_ids) + ->execute(); + } else { + $leases = array(); + } + + foreach ($logs as $key => $log) { + $lease = null; + if ($log->getLeaseID()) { + $lease = idx($leases, $log->getLeaseID()); + if (!$lease) { + unset($logs[$key]); + continue; + } + } + $log->attachLease($lease); + } + + // These logs are meaningless and their policies aren't computable. They + // shouldn't exist, but throw them away if they do. + foreach ($logs as $key => $log) { + if (!$log->getResource() && !$log->getLease()) { + unset($logs[$key]); + } + } + + return $logs; + } + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); @@ -62,25 +105,9 @@ final class DrydockLogQuery extends PhabricatorOffsetPagedQuery { $this->leaseIDs); } - if ($this->afterID) { - $where[] = qsprintf( - $conn_r, - 'id > %d', - $this->afterID); - } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } - private function buildOrderClause(AphrontDatabaseConnection $conn_r) { - switch ($this->order) { - case self::ORDER_EPOCH: - return 'ORDER BY log.epoch DESC, log.id DESC'; - case self::ORDER_ID: - return 'ORDER BY id ASC'; - default: - throw new Exception("Unknown order '{$this->order}'!"); - } - } - } diff --git a/src/applications/drydock/query/DrydockLogSearchEngine.php b/src/applications/drydock/query/DrydockLogSearchEngine.php new file mode 100644 index 0000000000..c090ae52e9 --- /dev/null +++ b/src/applications/drydock/query/DrydockLogSearchEngine.php @@ -0,0 +1,48 @@ + pht('All Logs'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/drydock/query/DrydockQuery.php b/src/applications/drydock/query/DrydockQuery.php new file mode 100644 index 0000000000..68fe919858 --- /dev/null +++ b/src/applications/drydock/query/DrydockQuery.php @@ -0,0 +1,10 @@ +ids = $ids; return $this; } - public function execute() { + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withTypes(array $types) { + $this->types = $types; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function withBlueprintPHIDs(array $blueprint_phids) { + $this->blueprintPHIDs = $blueprint_phids; + return $this; + } + + public function loadPage() { $table = new DrydockResource(); $conn_r = $table->establishConnection('r'); @@ -36,11 +60,37 @@ final class DrydockResourceQuery extends PhabricatorOffsetPagedQuery { $this->ids); } + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->types) { + $where[] = qsprintf( + $conn_r, + 'type IN (%Ls)', + $this->types); + } + + if ($this->statuses) { + $where[] = qsprintf( + $conn_r, + 'status IN (%Ls)', + $this->statuses); + } + + if ($this->blueprintPHIDs) { + $where[] = qsprintf( + $conn_r, + 'blueprintPHID IN (%Ls)', + $this->blueprintPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + return $this->formatWhereClause($where); } - private function buildOrderClause(AphrontDatabaseConnection $conn_r) { - return qsprintf($conn_r, 'ORDER BY id DESC'); - } - } diff --git a/src/applications/drydock/query/DrydockResourceSearchEngine.php b/src/applications/drydock/query/DrydockResourceSearchEngine.php new file mode 100644 index 0000000000..80a54212c9 --- /dev/null +++ b/src/applications/drydock/query/DrydockResourceSearchEngine.php @@ -0,0 +1,80 @@ +setParameter( + 'statuses', + $this->readListFromRequest($request, 'statuses')); + + return $saved; + } + + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $query = id(new DrydockResourceQuery()); + + $statuses = $saved->getParameter('statuses', array()); + if ($statuses) { + $query->withStatuses($statuses); + } + + return $query; + } + + public function buildSearchForm( + AphrontFormView $form, + PhabricatorSavedQuery $saved) { + + $statuses = $saved->getParameter('statuses', array()); + + $status_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Status')); + foreach (DrydockResourceStatus::getAllStatuses() as $status) { + $status_control->addCheckbox( + 'statuses[]', + $status, + DrydockResourceStatus::getNameForStatus($status), + in_array($status, $statuses)); + } + + $form + ->appendChild($status_control); + + } + + protected function getURI($path) { + return '/drydock/resource/'.$path; + } + + public function getBuiltinQueryNames() { + $names = array( + 'active' => pht('Active Resources'), + 'all' => pht('All Resources'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'active': + return $query->setParameter( + 'statuses', + array( + DrydockResourceStatus::STATUS_PENDING, + DrydockResourceStatus::STATUS_OPEN, + )); + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php new file mode 100644 index 0000000000..499097445f --- /dev/null +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -0,0 +1,88 @@ +setViewer($actor) + ->withClasses(array('PhabricatorApplicationDrydock')) + ->executeOne(); + + $view_policy = $app->getPolicy( + DrydockCapabilityDefaultView::CAPABILITY); + $edit_policy = $app->getPolicy( + DrydockCapabilityDefaultEdit::CAPABILITY); + + return id(new DrydockBlueprint()) + ->setViewPolicy($view_policy) + ->setEditPolicy($edit_policy) + ->setBlueprintName(''); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'details' => self::SERIALIZATION_JSON, + ) + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + DrydockPHIDTypeBlueprint::TYPECONST); + } + + public function getImplementation() { + $class = $this->className; + $implementations = + DrydockBlueprintImplementation::getAllBlueprintImplementations(); + if (!isset($implementations[$class])) { + throw new Exception( + "Invalid class name for blueprint (got '".$class."')"); + } + return id(new $class())->attachInstance($this); + } + + public function attachImplementation(DrydockBlueprintImplementation $impl) { + $this->implementation = $impl; + return $this; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } +} diff --git a/src/applications/drydock/storage/DrydockBlueprintTransaction.php b/src/applications/drydock/storage/DrydockBlueprintTransaction.php new file mode 100644 index 0000000000..6bb46e28e7 --- /dev/null +++ b/src/applications/drydock/storage/DrydockBlueprintTransaction.php @@ -0,0 +1,39 @@ +getOldValue(); + $new = $this->getNewValue(); + $author_handle = $this->renderHandleLink($this->getAuthorPHID()); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + if (!strlen($old)) { + return pht( + '%s created this blueprint.', + $author_handle); + } else { + return pht( + '%s renamed this blueprint from "%s" to "%s".', + $author_handle, + $old, + $new); + } + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index 9e08dbd3b1..26312ef51c 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,6 +1,7 @@ assertAttached($this->resource); } - public function attachResource(DrydockResource $resource) { + public function attachResource(DrydockResource $resource = null) { $this->resource = $resource; return $this; } @@ -91,23 +91,17 @@ final class DrydockLease extends DrydockDAO { $this->setStatus(DrydockLeaseStatus::STATUS_PENDING); $this->save(); - // NOTE: Prevent a race where some eager worker quickly grabs the task - // before we can save the Task ID. + $task = PhabricatorWorker::scheduleTask( + 'DrydockAllocatorWorker', + $this->getID()); - $this->openTransaction(); - $this->beginReadLocking(); + // NOTE: Scheduling the task might execute it in-process, if we're running + // from a CLI script. Reload the lease to make sure we have the most + // up-to-date information. Normally, this has no effect. + $this->reload(); - $this->reload(); - - $task = PhabricatorWorker::scheduleTask( - 'DrydockAllocatorWorker', - $this->getID()); - - $this->setTaskID($task->getID()); - $this->save(); - - $this->endReadLocking(); - $this->saveTransaction(); + $this->setTaskID($task->getID()); + $this->save(); return $this; } @@ -163,6 +157,8 @@ final class DrydockLease extends DrydockDAO { case DrydockLeaseStatus::STATUS_PENDING: case DrydockLeaseStatus::STATUS_ACQUIRING: break; + default: + throw new Exception("Unknown status??"); } } @@ -187,4 +183,32 @@ final class DrydockLease extends DrydockDAO { return $this; } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + if ($this->getResource()) { + return $this->getResource()->getPolicy($capability); + } + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->getResource()) { + return $this->getResource()->hasAutomaticCapability($capability, $viewer); + } + return false; + } + + public function describeAutomaticCapability($capability) { + return pht('Leases inherit policies from the resources they lease.'); + } + } diff --git a/src/applications/drydock/storage/DrydockLog.php b/src/applications/drydock/storage/DrydockLog.php index a26e593300..a34114e321 100644 --- a/src/applications/drydock/storage/DrydockLog.php +++ b/src/applications/drydock/storage/DrydockLog.php @@ -1,16 +1,66 @@ false, ) + parent::getConfiguration(); } + public function attachResource(DrydockResource $resource = null) { + $this->resource = $resource; + return $this; + } + + public function getResource() { + return $this->assertAttached($this->resource); + } + + public function attachLease(DrydockLease $lease = null) { + $this->lease = $lease; + return $this; + } + + public function getLease() { + return $this->assertAttached($this->lease); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + if ($this->getResource()) { + return $this->getResource()->getPolicy($capability); + } + return $this->getLease()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->getResource()) { + return $this->getResource()->hasAutomaticCapability($capability, $viewer); + } + return $this->getLease()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Logs inherit the policy of their resources.'); + } + } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index c4fa978532..ba2e6a8012 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,10 +1,11 @@ attributes, $key, $default); } + public function getAttributesForTypeSpec(array $attribute_names) { + return array_select_keys($this->attributes, $attribute_names); + } + public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; @@ -48,21 +52,27 @@ final class DrydockResource extends DrydockDAO { } public function getBlueprint() { + // TODO: Policy stuff. if (empty($this->blueprint)) { - $this->blueprint = newv($this->blueprintClass, array()); + $blueprint = id(new DrydockBlueprint()) + ->loadOneWhere('phid = %s', $this->blueprintPHID); + $this->blueprint = $blueprint->getImplementation(); } return $this->blueprint; } public function closeResource() { $this->openTransaction(); - $leases = id(new DrydockLease())->loadAllWhere( - 'resourceID = %d AND status IN (%Ld)', - $this->getID(), - array( - DrydockLeaseStatus::STATUS_PENDING, - DrydockLeaseStatus::STATUS_ACTIVE, - )); + $statuses = array( + DrydockLeaseStatus::STATUS_PENDING, + DrydockLeaseStatus::STATUS_ACTIVE, + ); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withResourceIDs(array($this->getID())) + ->withStatuses($statuses) + ->execute(); foreach ($leases as $lease) { switch ($lease->getStatus()) { @@ -75,7 +85,7 @@ final class DrydockResource extends DrydockDAO { $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); break; } - DrydockBlueprint::writeLog($this, $lease, $message); + DrydockBlueprintImplementation::writeLog($this, $lease, $message); $lease->save(); } @@ -84,4 +94,28 @@ final class DrydockResource extends DrydockDAO { $this->saveTransaction(); } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } } diff --git a/src/applications/drydock/util/DrydockBlueprintScopeGuard.php b/src/applications/drydock/util/DrydockBlueprintScopeGuard.php index 539fb6c459..ab8268ba12 100644 --- a/src/applications/drydock/util/DrydockBlueprintScopeGuard.php +++ b/src/applications/drydock/util/DrydockBlueprintScopeGuard.php @@ -2,7 +2,7 @@ final class DrydockBlueprintScopeGuard { - public function __construct(DrydockBlueprint $blueprint) { + public function __construct(DrydockBlueprintImplementation $blueprint) { $this->blueprint = $blueprint; } diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php index ecdd14a912..6c2064c587 100644 --- a/src/applications/drydock/worker/DrydockAllocatorWorker.php +++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php @@ -13,10 +13,13 @@ final class DrydockAllocatorWorker extends PhabricatorWorker { private function loadLease() { if (empty($this->lease)) { - $lease = id(new DrydockLease())->load($this->getTaskData()); + $lease = id(new DrydockLeaseQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($this->getTaskData())) + ->executeOne(); if (!$lease) { throw new PhabricatorWorkerPermanentFailureException( - "No such lease!"); + pht("No such lease %d!", $this->getTaskData())); } $this->lease = $lease; } @@ -24,7 +27,7 @@ final class DrydockAllocatorWorker extends PhabricatorWorker { } private function logToDrydock($message) { - DrydockBlueprint::writeLog( + DrydockBlueprintImplementation::writeLog( null, $this->loadLease(), $message); @@ -52,9 +55,24 @@ final class DrydockAllocatorWorker extends PhabricatorWorker { } } + private function loadAllBlueprints() { + $viewer = PhabricatorUser::getOmnipotentUser(); + $instances = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->execute(); + $blueprints = array(); + foreach ($instances as $instance) { + $blueprints[$instance->getPHID()] = $instance; + } + return $blueprints; + } + private function allocateLease(DrydockLease $lease) { $type = $lease->getResourceType(); + $blueprints = $this->loadAllBlueprints(); + + // TODO: Policy stuff. $pool = id(new DrydockResource())->loadAllWhere( 'type = %s AND status = %s', $lease->getResourceType(), @@ -65,14 +83,15 @@ final class DrydockAllocatorWorker extends PhabricatorWorker { $candidates = array(); foreach ($pool as $key => $candidate) { - try { - $blueprint = $candidate->getBlueprint(); - } catch (Exception $ex) { + if (!isset($blueprints[$candidate->getBlueprintPHID()])) { unset($pool[$key]); continue; } - if ($blueprint->filterResource($candidate, $lease)) { + $blueprint = $blueprints[$candidate->getBlueprintPHID()]; + $implementation = $blueprint->getImplementation(); + + if ($implementation->filterResource($candidate, $lease)) { $candidates[] = $candidate; } } @@ -83,7 +102,8 @@ final class DrydockAllocatorWorker extends PhabricatorWorker { if ($candidates) { shuffle($candidates); foreach ($candidates as $candidate_resource) { - $blueprint = $candidate_resource->getBlueprint(); + $blueprint = $blueprints[$candidate_resource->getBlueprintPHID()] + ->getImplementation(); if ($blueprint->allocateLease($candidate_resource, $lease)) { $resource = $candidate_resource; break; @@ -92,7 +112,8 @@ final class DrydockAllocatorWorker extends PhabricatorWorker { } if (!$resource) { - $blueprints = DrydockBlueprint::getAllBlueprintsForResource($type); + $blueprints = DrydockBlueprintImplementation + ::getAllBlueprintImplementationsForResource($type); $this->logToDrydock( pht('Found %d Blueprints', count($blueprints))); diff --git a/src/applications/fact/management/PhabricatorFactManagementWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementWorkflow.php index 0d15d208ae..76628b3a90 100644 --- a/src/applications/fact/management/PhabricatorFactManagementWorkflow.php +++ b/src/applications/fact/management/PhabricatorFactManagementWorkflow.php @@ -1,10 +1,6 @@ appendChild(hsprintf( - '')); + $null_view->appendChild( + phutil_tag_div('phabricator-feed-story-date-separator')); } $last_date = $date; $header = new PhabricatorActionHeaderView(); diff --git a/src/applications/feed/controller/PhabricatorFeedDetailController.php b/src/applications/feed/controller/PhabricatorFeedDetailController.php index 1608b1633f..98ee7e2f66 100644 --- a/src/applications/feed/controller/PhabricatorFeedDetailController.php +++ b/src/applications/feed/controller/PhabricatorFeedDetailController.php @@ -27,14 +27,10 @@ final class PhabricatorFeedDetailController extends PhabricatorFeedController { $title = pht('Story'); - $feed_view = hsprintf( - '
%s
', - $feed_view); + $feed_view = phutil_tag_div('phabricator-feed-frame', $feed_view); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( diff --git a/src/applications/feed/controller/PhabricatorFeedListController.php b/src/applications/feed/controller/PhabricatorFeedListController.php index f292735bb6..42dd0fb5f9 100644 --- a/src/applications/feed/controller/PhabricatorFeedListController.php +++ b/src/applications/feed/controller/PhabricatorFeedListController.php @@ -32,9 +32,7 @@ final class PhabricatorFeedListController extends PhabricatorFeedController $builder->setUser($this->getRequest()->getUser()); $view = $builder->buildView(); - return hsprintf( - '
%s
', - $view); + return phutil_tag_div('phabricator-feed-frame', $view); } } diff --git a/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php b/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php index 84e8f3f155..b43e79450f 100644 --- a/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php +++ b/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php @@ -25,7 +25,8 @@ final class PhabricatorFeedPublicStreamController ->setFramed(true) ->setUser($viewer); - $view = hsprintf('
%s
', + $view = phutil_tag_div( + 'phabricator-public-feed-frame', $builder->buildView()); return $this->buildStandardPageResponse( diff --git a/src/applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php b/src/applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php index 7f474f0826..4f6279ba7a 100644 --- a/src/applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php +++ b/src/applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php @@ -21,7 +21,7 @@ final class PhabricatorFeedManagementRepublishWorkflow public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $key = $args->getArg('key'); if (count($key) < 1) { diff --git a/src/applications/feed/management/PhabricatorFeedManagementWorkflow.php b/src/applications/feed/management/PhabricatorFeedManagementWorkflow.php index 109ca98084..0895e1ed21 100644 --- a/src/applications/feed/management/PhabricatorFeedManagementWorkflow.php +++ b/src/applications/feed/management/PhabricatorFeedManagementWorkflow.php @@ -1,10 +1,6 @@ getBaseURI().'upload/'; - } - public function canUninstall() { return false; } @@ -61,6 +57,7 @@ final class PhabricatorApplicationFiles extends PhabricatorApplication { 'xform/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorFileTransformController', 'uploaddialog/' => 'PhabricatorFileUploadDialogController', + 'download/(?P[^/]+)/' => 'PhabricatorFileDialogController', ), ); } diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index e8bebcf8c9..8829d1a5bd 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -70,12 +70,12 @@ final class PhabricatorFileDataController extends PhabricatorFileController { if ($is_viewable && !$force_download) { $response->setMimeType($file->getViewableMimeType()); } else { - if (!$request->isHTTPPost()) { - // NOTE: Require POST to download files. We'd rather go full-bore and - // do a real CSRF check, but can't currently authenticate users on the - // file domain. This should blunt any attacks based on iframes, script - // tags, applet tags, etc., at least. Send the user to the "info" page - // if they're using some other method. + if (!$request->isHTTPPost() && !$alt_domain) { + // NOTE: Require POST to download files from the primary domain. We'd + // rather go full-bore and do a real CSRF check, but can't currently + // authenticate users on the file domain. This should blunt any + // attacks based on iframes, script tags, applet tags, etc., at least. + // Send the user to the "info" page if they're using some other method. return id(new AphrontRedirectResponse()) ->setURI(PhabricatorEnv::getProductionURI($file->getBestURI())); } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 18672b892b..a9e1c610bd 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -37,12 +37,14 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $this->loadHandles($handle_phids); $header = id(new PHUIHeaderView()) + ->setUser($user) + ->setPolicyObject($file) ->setHeader($file->getName()); $ttl = $file->getTTL(); if ($ttl !== null) { - $ttl_tag = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_OBJECT) + $ttl_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_OBJECT) ->setName(pht("Temporary")); $header->addTag($ttl_tag); } @@ -51,10 +53,9 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $timeline = $this->buildTransactionView($file, $xactions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('F'.$file->getID()) - ->setHref($this->getApplicationURI("/info/{$phid}/"))); + $crumbs->addTextCrumb( + 'F'.$file->getID(), + $this->getApplicationURI("/info/{$phid}/")); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header); @@ -98,11 +99,9 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $add_comment_header = id(new PHUIHeaderView()) - ->setHeader( - $is_serious - ? pht('Add Comment') - : pht('Question File Integrity')); + $add_comment_header = $is_serious + ? pht('Add Comment') + : pht('Question File Integrity'); $submit_button_name = $is_serious ? pht('Add Comment') @@ -114,17 +113,13 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { ->setUser($user) ->setObjectPHID($file->getPHID()) ->setDraft($draft) + ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$file->getID().'/')) ->setSubmitButtonName($submit_button_name); - $comment_box = id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($add_comment_header) - ->appendChild($add_comment_form); - return array( $timeline, - $comment_box); + $add_comment_form); } private function buildActionView(PhabricatorFile $file) { diff --git a/src/applications/files/controller/PhabricatorFileUploadController.php b/src/applications/files/controller/PhabricatorFileUploadController.php index b92bd9ac09..4f3d19d9e1 100644 --- a/src/applications/files/controller/PhabricatorFileUploadController.php +++ b/src/applications/files/controller/PhabricatorFileUploadController.php @@ -60,25 +60,16 @@ final class PhabricatorFileUploadController extends PhabricatorFileController { ->appendChild($instructions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Upload')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb(pht('Upload'), $request->getRequestURI()); $title = pht('Upload File'); - if ($errors) { - $errors = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $global_upload = id(new PhabricatorGlobalUploadTargetView()) ->setShowIfSupportedID($support_id); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php b/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php new file mode 100644 index 0000000000..624ec01097 --- /dev/null +++ b/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php @@ -0,0 +1,18 @@ +loadAllWhere( + 'ttl < %d LIMIT 100', + time()); + + foreach ($files as $file) { + $file->delete(); + } + + return (count($files) == 100); + } + +} diff --git a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php index 4ce3e21a0f..a8cd9d6023 100644 --- a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php @@ -1,11 +1,7 @@ getArg('names'); @@ -20,7 +16,7 @@ abstract class PhabricatorFilesManagementWorkflow if ($names) { $query = id(new PhabricatorObjectQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withNames($names) ->withTypes(array(PhabricatorFilePHIDTypeFile::TYPECONST)); diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index 854f49411e..f3243fe1fa 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -13,6 +13,7 @@ final class PhabricatorFileQuery private $transforms; private $dateCreatedAfter; private $dateCreatedBefore; + private $contentHashes; public function withIDs(array $ids) { $this->ids = $ids; @@ -39,6 +40,11 @@ final class PhabricatorFileQuery return $this; } + public function withContentHashes(array $content_hashes) { + $this->contentHashes = $content_hashes; + 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 @@ -228,6 +234,13 @@ final class PhabricatorFileQuery $this->dateCreatedBefore); } + if ($this->contentHashes) { + $where[] = qsprintf( + $conn_r, + 'f.contentHash IN (%Ls)', + $this->contentHashes); + } + return $this->formatWhereClause($where); } diff --git a/src/applications/files/remarkup/PhabricatorRemarkupRuleEmbedFile.php b/src/applications/files/remarkup/PhabricatorRemarkupRuleEmbedFile.php index 9eb21eef41..fbf30d7cb9 100644 --- a/src/applications/files/remarkup/PhabricatorRemarkupRuleEmbedFile.php +++ b/src/applications/files/remarkup/PhabricatorRemarkupRuleEmbedFile.php @@ -50,9 +50,11 @@ final class PhabricatorRemarkupRuleEmbedFile private function getFileOptions($option_string) { $options = array( - 'size' => 'thumb', + 'size' => null, 'layout' => 'left', 'float' => false, + 'width' => null, + 'height' => null, ); if ($option_string) { @@ -73,23 +75,40 @@ final class PhabricatorRemarkupRuleEmbedFile $attrs = array(); $image_class = null; - switch ((string)$options['size']) { - case 'full': + + $use_size = true; + if (!$options['size']) { + $width = $this->parseDimension($options['width']); + $height = $this->parseDimension($options['height']); + if ($width || $height) { + $use_size = false; $attrs += array( 'src' => $file->getBestURI(), - 'width' => $file->getImageWidth(), - 'height' => $file->getImageHeight(), + 'width' => $width, + 'height' => $height, ); - break; - case 'thumb': - default: - $attrs['src'] = $file->getPreview220URI(); - $dimensions = - PhabricatorImageTransformer::getPreviewDimensions($file, 220); - $attrs['width'] = $dimensions['sdx']; - $attrs['height'] = $dimensions['sdy']; - $image_class = 'phabricator-remarkup-embed-image'; - break; + } + } + + if ($use_size) { + switch ((string)$options['size']) { + case 'full': + $attrs += array( + 'src' => $file->getBestURI(), + 'width' => $file->getImageWidth(), + 'height' => $file->getImageHeight(), + ); + break; + case 'thumb': + default: + $attrs['src'] = $file->getPreview220URI(); + $dimensions = + PhabricatorImageTransformer::getPreviewDimensions($file, 220); + $attrs['width'] = $dimensions['sdx']; + $attrs['height'] = $dimensions['sdy']; + $image_class = 'phabricator-remarkup-embed-image'; + break; + } } $img = phutil_tag('img', $attrs); @@ -186,4 +205,14 @@ final class PhabricatorRemarkupRuleEmbedFile ->setFileViewable($options['viewable']); } + private function parseDimension($string) { + $string = trim($string); + + if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) { + return $string; + } + + return null; + } + } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 9d7cfea709..cbe33d4530 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -175,13 +175,17 @@ final class PhabricatorFile extends PhabricatorFileDAO $file_ttl = idx($params, 'ttl'); $authorPHID = idx($params, 'authorPHID'); - $new_file = new PhabricatorFile(); + $new_file = new PhabricatorFile(); $new_file->setName($file_name); $new_file->setByteSize($copy_of_byteSize); $new_file->setAuthorPHID($authorPHID); $new_file->setTtl($file_ttl); + if (idx($params, 'viewPolicy')) { + $new_file->setViewPolicy($params['viewPolicy']); + } + $new_file->setContentHash($hash); $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); @@ -262,6 +266,10 @@ final class PhabricatorFile extends PhabricatorFileDAO $file->setTtl($file_ttl); $file->setContentHash(self::hashFileContent($data)); + if (idx($params, 'viewPolicy')) { + $file->setViewPolicy($params['viewPolicy']); + } + $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); @@ -877,8 +885,12 @@ final class PhabricatorFile extends PhabricatorFileDAO } public function getPolicy($capability) { - // TODO: Implement proper per-object policies. - return PhabricatorPolicies::POLICY_PUBLIC; + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return PhabricatorPolicies::POLICY_NOONE; + } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { diff --git a/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php b/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php index bf0d7c21ac..b9d0918f6f 100644 --- a/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php +++ b/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php @@ -26,6 +26,12 @@ final class PhabricatorApplicationHarbormaster extends PhabricatorApplication { return self::GROUP_UTILITIES; } + public function getEventListeners() { + return array( + new HarbormasterUIEventListener(), + ); + } + public function isBeta() { return true; } @@ -42,19 +48,27 @@ final class PhabricatorApplicationHarbormaster extends PhabricatorApplication { '/harbormaster/' => array( '(?:query/(?P[^/]+)/)?' => 'HarbormasterBuildableListController', - 'buildable/' => array( - 'edit/(?:(?P\d+)/)?' => 'HarbormasterBuildableEditController', - 'apply/(?:(?P\d+)/)?' => 'HarbormasterBuildableApplyController', - ), 'step/' => array( 'add/(?:(?P\d+)/)?' => 'HarbormasterStepAddController', 'edit/(?:(?P\d+)/)?' => 'HarbormasterStepEditController', 'delete/(?:(?P\d+)/)?' => 'HarbormasterStepDeleteController', ), + 'buildable/' => array( + '(?P\d+)/(?Pstop|resume|restart)/' + => 'HarbormasterBuildableActionController', + ), + 'build/' => array( + '(?:(?P\d+)/)?' => 'HarbormasterBuildViewController', + '(?Pstop|resume|restart)/(?P\d+)/(?:(?P[^/]+)/)?' + => 'HarbormasterBuildActionController', + ), 'plan/' => array( '(?:query/(?P[^/]+)/)?' => 'HarbormasterPlanListController', 'edit/(?:(?P\d+)/)?' => 'HarbormasterPlanEditController', + 'order/(?:(?P\d+)/)?' => 'HarbormasterPlanOrderController', + 'disable/(?P\d+)/' => 'HarbormasterPlanDisableController', + 'run/(?P\d+)/' => 'HarbormasterPlanRunController', '(?P\d+)/' => 'HarbormasterPlanViewController', ), ), diff --git a/src/applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php b/src/applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php new file mode 100644 index 0000000000..ee1729d6ad --- /dev/null +++ b/src/applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php @@ -0,0 +1,18 @@ +id = $data['id']; + $this->action = $data['action']; + $this->via = idx($data, 'via'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $command = $this->action; + + $build = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$build) { + return new Aphront404Response(); + } + + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + $can_issue = $build->canRestartBuild(); + break; + case HarbormasterBuildCommand::COMMAND_STOP: + $can_issue = $build->canStopBuild(); + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + $can_issue = $build->canResumeBuild(); + break; + default: + return new Aphront400Response(); + } + + switch ($this->via) { + case 'buildable': + $return_uri = '/'.$build->getBuildable()->getMonogram(); + break; + default: + $return_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); + break; + } + + if ($request->isDialogFormPost() && $can_issue) { + + // Issue the new build command. + id(new HarbormasterBuildCommand()) + ->setAuthorPHID($viewer->getPHID()) + ->setTargetPHID($build->getPHID()) + ->setCommand($command) + ->save(); + + // Schedule a build update. We may already have stuff in queue (in which + // case this will just no-op), but we might also be dealing with a + // stopped build, which won't restart unless we deal with this. + PhabricatorWorker::scheduleTask( + 'HarbormasterBuildWorker', + array( + 'buildID' => $build->getID() + )); + + return id(new AphrontRedirectResponse())->setURI($return_uri); + } + + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + if ($can_issue) { + $title = pht('Really restart build?'); + $body = pht( + 'Progress on this build will be discarded and the build will '. + 'restart. Side effects of the build will occur again. Really '. + 'restart build?'); + $submit = pht('Restart Build'); + } else { + $title = pht('Unable to Restart Build'); + if ($build->isRestarting()) { + $body = pht( + 'This build is already restarting. You can not reissue a '. + 'restart command to a restarting build.'); + } else { + $body = pht( + 'You can not restart this build.'); + } + } + break; + case HarbormasterBuildCommand::COMMAND_STOP: + if ($can_issue) { + $title = pht('Really stop build?'); + $body = pht( + 'If you stop this build, work will halt once the current steps '. + 'complete. You can resume the build later.'); + $submit = pht('Stop Build'); + } else { + $title = pht('Unable to Stop Build'); + if ($build->isComplete()) { + $body = pht( + 'This build is already complete. You can not stop a completed '. + 'build.'); + } else if ($build->isStopped()) { + $body = pht( + 'This build is already stopped. You can not stop a build which '. + 'has already been stopped.'); + } else if ($build->isStopping()) { + $body = pht( + 'This build is already stopping. You can not reissue a stop '. + 'command to a stopping build.'); + } else { + $body = pht( + 'This build can not be stopped.'); + } + } + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + if ($can_issue) { + $title = pht('Really resume build?'); + $body = pht( + 'Work will continue on the build. Really resume?'); + $submit = pht('Resume Build'); + } else { + $title = pht('Unable to Resume Build'); + if ($build->isResuming()) { + $body = pht( + 'This build is already resuming. You can not reissue a resume '. + 'command to a resuming build.'); + } else if (!$build->isStopped()) { + $body = pht( + 'This build is not stopped. You can only resume a stopped '. + 'build.'); + } + } + break; + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild($body) + ->addCancelButton($return_uri); + + if ($can_issue) { + $dialog->addSubmitButton($submit); + } + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php new file mode 100644 index 0000000000..ccbda9acc4 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -0,0 +1,319 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $id = $this->id; + + $build = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$build) { + return new Aphront404Response(); + } + + $title = pht("Build %d", $id); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($build); + + if ($build->isRestarting()) { + $header->setStatus('warning', 'red', pht('Restarting')); + } else if ($build->isStopping()) { + $header->setStatus('warning', 'red', pht('Stopping')); + } else if ($build->isResuming()) { + $header->setStatus('warning', 'red', pht('Resuming')); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header); + + $actions = $this->buildActionList($build); + $this->buildPropertyLists($box, $build, $actions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + $build->getBuildable()->getMonogram(), + '/'.$build->getBuildable()->getMonogram()); + $crumbs->addTextCrumb($title); + + $build_targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($viewer) + ->withBuildPHIDs(array($build->getPHID())) + ->execute(); + + $targets = array(); + foreach ($build_targets as $build_target) { + $header = id(new PHUIHeaderView()) + ->setHeader(pht( + 'Build Target %d (%s)', + $build_target->getID(), + $build_target->getImplementation()->getName())) + ->setUser($viewer); + $properties = new PHUIPropertyListView(); + + $details = $build_target->getDetails(); + if ($details) { + $properties->addSectionHeader(pht('Configuration Details')); + foreach ($details as $key => $value) { + $properties->addProperty($key, $value); + } + } + + $variables = $build_target->getVariables(); + if ($variables) { + $properties->addSectionHeader(pht('Variables')); + foreach ($variables as $key => $value) { + $properties->addProperty($key, $value); + } + } + + $targets[] = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + $targets[] = $this->buildArtifacts($build_target); + $targets[] = $this->buildLog($build, $build_target); + } + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $targets + ), + array( + 'title' => $title, + 'device' => true, + )); + } + + private function buildArtifacts(HarbormasterBuildTarget $build_target) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $artifacts = id(new HarbormasterBuildArtifactQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs(array($build_target->getPHID())) + ->execute(); + + if (count($artifacts) === 0) { + return null; + } + + $list = new PHUIObjectItemListView(); + + foreach ($artifacts as $artifact) { + $list->addItem($artifact->getObjectItemView($viewer)); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Build Artifacts')) + ->setUser($viewer); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header); + + return array($box, $list); + } + + private function buildLog( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + $limit = $request->getInt('l', 25); + + $logs = id(new HarbormasterBuildLogQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs(array($build_target->getPHID())) + ->execute(); + + $log_boxes = array(); + foreach ($logs as $log) { + $start = 1; + $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); + if ($limit !== 0) { + $start = count($lines) - $limit; + if ($start >= 1) { + $lines = array_slice($lines, -$limit, $limit); + } else { + $start = 1; + } + } + $log_view = new ShellLogView(); + $log_view->setLines($lines); + $log_view->setStart($start); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht( + 'Build Log %d (%s - %s)', + $log->getID(), + $log->getLogSource(), + $log->getLogType())) + ->setSubheader($this->createLogHeader($build, $log)) + ->setUser($viewer); + + $log_boxes[] = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setForm($log_view); + } + + return $log_boxes; + } + + private function createLogHeader($build, $log) { + $request = $this->getRequest(); + $limit = $request->getInt('l', 25); + + $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); + $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); + $lines_100 = + $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); + $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); + + $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); + $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); + $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); + $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); + + if ($limit === 25) { + $link_25 = phutil_tag('strong', array(), $link_25); + } else if ($limit === 50) { + $link_50 = phutil_tag('strong', array(), $link_50); + } else if ($limit === 100) { + $link_100 = phutil_tag('strong', array(), $link_100); + } else if ($limit === 0) { + $link_0 = phutil_tag('strong', array(), $link_0); + } + + return phutil_tag( + 'span', + array(), + array( + $link_25, + ' - ', + $link_50, + ' - ', + $link_100, + ' - ', + $link_0, + ' Lines')); + } + + private function buildActionList(HarbormasterBuild $build) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $id = $build->getID(); + + $list = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($build) + ->setObjectURI("/build/{$id}"); + + $can_restart = $build->canRestartBuild(); + $can_stop = $build->canStopBuild(); + $can_resume = $build->canResumeBuild(); + + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Restart Build')) + ->setIcon('backward') + ->setHref($this->getApplicationURI('/build/restart/'.$id.'/')) + ->setDisabled(!$can_restart) + ->setWorkflow(true)); + + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Stop Build')) + ->setIcon('stop') + ->setHref($this->getApplicationURI('/build/stop/'.$id.'/')) + ->setDisabled(!$can_stop) + ->setWorkflow(true)); + + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Resume Build')) + ->setIcon('play') + ->setHref($this->getApplicationURI('/build/resume/'.$id.'/')) + ->setDisabled(!$can_resume) + ->setWorkflow(true)); + + return $list; + } + + private function buildPropertyLists( + PHUIObjectBoxView $box, + HarbormasterBuild $build, + PhabricatorActionListView $actions) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($build) + ->setActionList($actions); + $box->addPropertyList($properties); + + $properties->addProperty( + pht('Status'), + $this->getStatus($build)); + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(array( + $build->getBuildablePHID(), + $build->getBuildPlanPHID())) + ->execute(); + + $properties->addProperty( + pht('Buildable'), + $handles[$build->getBuildablePHID()]->renderLink()); + + $properties->addProperty( + pht('Build Plan'), + $handles[$build->getBuildPlanPHID()]->renderLink()); + + } + + private function getStatus(HarbormasterBuild $build) { + if ($build->isStopping()) { + return pht('Stopping'); + } + switch ($build->getBuildStatus()) { + case HarbormasterBuild::STATUS_INACTIVE: + return pht('Inactive'); + case HarbormasterBuild::STATUS_PENDING: + return pht('Pending'); + case HarbormasterBuild::STATUS_WAITING: + return pht('Waiting'); + case HarbormasterBuild::STATUS_BUILDING: + return pht('Building'); + case HarbormasterBuild::STATUS_PASSED: + return pht('Passed'); + case HarbormasterBuild::STATUS_FAILED: + return pht('Failed'); + case HarbormasterBuild::STATUS_ERROR: + return pht('Unexpected Error'); + case HarbormasterBuild::STATUS_STOPPED: + return pht('Stopped'); + default: + return pht('Unknown'); + } + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php new file mode 100644 index 0000000000..1ecfefba67 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -0,0 +1,127 @@ +id = $data['id']; + $this->action = $data['action']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $command = $this->action; + + $buildable = id(new HarbormasterBuildableQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->needBuilds(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$buildable) { + return new Aphront404Response(); + } + + $issuable = array(); + + foreach ($buildable->getBuilds() as $build) { + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + if ($build->canRestartBuild()) { + $issuable[] = $build; + } + break; + case HarbormasterBuildCommand::COMMAND_STOP: + if ($build->canStopBuild()) { + $issuable[] = $build; + } + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + if ($build->canResumeBuild()) { + $issuable[] = $build; + } + break; + default: + return new Aphront400Response(); + } + } + + $return_uri = $buildable->getMonogram(); + if ($request->isDialogFormPost() && $issuable) { + foreach ($issuable as $build) { + id(new HarbormasterBuildCommand()) + ->setAuthorPHID($viewer->getPHID()) + ->setTargetPHID($build->getPHID()) + ->setCommand($command) + ->save(); + + PhabricatorWorker::scheduleTask( + 'HarbormasterBuildWorker', + array( + 'buildID' => $build->getID() + )); + } + + return id(new AphrontRedirectResponse())->setURI($return_uri); + } + + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + if ($issuable) { + $title = pht('Really restart all builds?'); + $body = pht( + 'Progress on all builds will be discarded, and all builds will '. + 'restart. Side effects of the builds will occur again. Really '. + 'restart all builds?'); + $submit = pht('Restart All Builds'); + } else { + $title = pht('Unable to Restart Build'); + $body = pht('No builds can be restarted.'); + } + break; + case HarbormasterBuildCommand::COMMAND_STOP: + if ($issuable) { + $title = pht('Really stop all builds?'); + $body = pht( + 'If you stop all build, work will halt once the current steps '. + 'complete. You can resume the builds later.'); + $submit = pht('Stop All Builds'); + } else { + $title = pht('Unable to Stop Build'); + $body = pht('No builds can be stopped.'); + } + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + if ($issuable) { + $title = pht('Really resume all builds?'); + $body = pht('Work will continue on all builds. Really resume?'); + $submit = pht('Resume All Builds'); + } else { + $title = pht('Unable to Resume Build'); + $body = pht('No builds can be resumed.'); + } + break; + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild($body) + ->addCancelButton($return_uri); + + if ($issuable) { + $dialog->addSubmitButton($submit); + } + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php b/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php deleted file mode 100644 index 215e117953..0000000000 --- a/src/applications/harbormaster/controller/HarbormasterBuildableApplyController.php +++ /dev/null @@ -1,81 +0,0 @@ -id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $id = $this->id; - - $buildable = id(new HarbormasterBuildableQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if ($buildable === null) { - throw new Exception("Buildable not found!"); - } - - $buildable_uri = '/B'.$buildable->getID(); - - if ($request->isDialogFormPost()) { - $plan = id(new HarbormasterBuildPlanQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getInt('build-plan'))) - ->executeOne(); - - $build = HarbormasterBuild::initializeNewBuild($viewer); - $build->setBuildablePHID($buildable->getPHID()); - $build->setBuildPlanPHID($plan->getPHID()); - $build->setBuildStatus(HarbormasterBuild::STATUS_PENDING); - $build->save(); - - PhabricatorWorker::scheduleTask( - 'HarbormasterBuildWorker', - array( - 'buildID' => $build->getID() - )); - - return id(new AphrontRedirectResponse())->setURI($buildable_uri); - } - - $plans = id(new HarbormasterBuildPlanQuery()) - ->setViewer($viewer) - ->execute(); - - $options = array(); - foreach ($plans as $plan) { - $options[$plan->getID()] = $plan->getName(); - } - - // FIXME: I'd really like to use the dialog that "Edit Differential - // Revisions" uses, but that code is quite hard-coded for the particular - // uses, so for now we just give a single dropdown. - - $dialog = new AphrontDialogView(); - $dialog->setTitle(pht('Apply which plan?')) - ->setUser($viewer) - ->addSubmitButton(pht('Apply')) - ->addCancelButton($buildable_uri); - $dialog->appendChild( - phutil_tag( - 'p', - array(), - pht( - 'Select what build plan you want to apply to this buildable:'))); - $dialog->appendChild( - id(new AphrontFormSelectControl()) - ->setUser($viewer) - ->setName('build-plan') - ->setOptions($options)); - return id(new AphrontDialogResponse())->setDialog($dialog); - } - -} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php b/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php deleted file mode 100644 index a95e831a97..0000000000 --- a/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php +++ /dev/null @@ -1,144 +0,0 @@ -id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $this->requireApplicationCapability( - HarbormasterCapabilityManagePlans::CAPABILITY); - - if ($this->id) { - $buildable = id(new HarbormasterBuildableQuery()) - ->setViewer($viewer) - ->withIDs(array($this->id)) - ->executeOne(); - if (!$buildable) { - return new Aphront404Response(); - } - } else { - $buildable = HarbormasterBuildable::initializeNewBuildable($viewer); - } - - $e_name = true; - $v_name = null; - - $errors = array(); - if ($request->isFormPost()) { - $v_name = $request->getStr('buildablePHID'); - - if ($v_name) { - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withNames(array($v_name)) - ->executeOne(); - - if ($object instanceof DifferentialRevision) { - throw new Exception( - "TODO: We need to assign PHIDs to diffs before this will work."); - } else if ($object instanceof PhabricatorRepositoryCommit) { - $buildable - ->setBuildablePHID($object->getPHID()) - ->setContainerPHID($object->getRepository()->getPHID()); - } else { - $e_name = pht('Invalid'); - $errors[] = pht('Enter the name of a revision or commit.'); - } - } else { - $e_name = pht('Required'); - $errors[] = pht('You must choose a revision or commit to build.'); - } - - if (!$errors) { - $buildable->save(); - - $buildable_uri = '/B'.$buildable->getID(); - return id(new AphrontRedirectResponse())->setURI($buildable_uri); - } - } - - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } - - $is_new = (!$buildable->getID()); - if ($is_new) { - $title = pht('New Buildable'); - $cancel_uri = $this->getApplicationURI(); - $save_button = pht('Create Buildable'); - } else { - $id = $buildable->getID(); - - $title = pht('Edit Buildable'); - $cancel_uri = "/B{$id}"; - $save_button = pht('Save Buildable'); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer); - - if ($is_new) { - $form - ->appendRemarkupInstructions( - pht( - 'Enter the name of a commit or revision, like `rX123456789` '. - 'or `D123`.')) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Buildable Name') - ->setName('buildablePHID') - ->setError($e_name) - ->setValue($v_name)); - } else { - $form->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel(pht('Buildable')) - ->setValue($buildable->getBuildableHandle()->renderLink())); - } - - $form->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue($save_button) - ->addCancelButton($cancel_uri)); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setFormError($errors) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - if ($is_new) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Buildable'))); - } else { - $id = $buildable->getID(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("B{$id}") - ->setHref("/B{$id}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); - } - - return $this->buildApplicationPage( - array( - $crumbs, - $box, - ), - array( - 'title' => $title, - 'device' => true, - )); - } - -} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableListController.php b/src/applications/harbormaster/controller/HarbormasterBuildableListController.php index 5a66de2129..d8e72971f7 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableListController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableListController.php @@ -37,14 +37,21 @@ final class HarbormasterBuildableListController $item = id(new PHUIObjectItemView()) ->setHeader(pht('Buildable %d', $buildable->getID())); + $item->addAttribute($buildable->getContainerHandle()->getName()); $item->addAttribute($buildable->getBuildableHandle()->getFullName()); if ($id) { $item->setHref("/B{$id}"); } + if ($buildable->getIsManualBuildable()) { + $item->addIcon('wrench-grey', pht('Manual')); + } + $list->addItem($item); + + // TODO: This is proof-of-concept for getting meaningful status // information into this list, and should get an improvement pass // once we're a little farther along. @@ -81,12 +88,7 @@ final class HarbormasterBuildableListController ->setViewer($user) ->addNavigationItems($nav->getMenu()); - if ($for_app) { - $nav->addFilter('new/', pht('New Build Plan')); - } - - $nav->addLabel('Utilities'); - $nav->addFilter('buildable/edit/', pht('New Manual Build')); + $nav->addLabel(pht('Build Plans')); $nav->addFilter('plan/', pht('Manage Build Plans')); $nav->selectFilter(null); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index a5c9c0b34d..b5bdec1030 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -19,24 +19,22 @@ final class HarbormasterBuildableViewController ->setViewer($viewer) ->withIDs(array($id)) ->needBuildableHandles(true) - ->needContainerObjects(true) + ->needContainerHandles(true) + ->needBuilds(true) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } - $builds = id(new HarbormasterBuildQuery()) - ->setViewer($viewer) - ->withBuildablePHIDs(array($buildable->getPHID())) - ->needBuildPlans(true) - ->execute(); - $build_list = id(new PHUIObjectItemListView()) ->setUser($viewer); - foreach ($builds as $build) { + foreach ($buildable->getBuilds() as $build) { + $view_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Build %d', $build->getID())) - ->setHeader($build->getName()); + ->setHeader($build->getName()) + ->setHref($view_uri); + switch ($build->getBuildStatus()) { case HarbormasterBuild::STATUS_INACTIVE: $item->setBarColor('grey'); @@ -47,8 +45,8 @@ final class HarbormasterBuildableViewController $item->addAttribute(pht('Pending')); break; case HarbormasterBuild::STATUS_WAITING: - $item->setBarColor('blue'); - $item->addAttribute(pht('Waiting on Resource')); + $item->setBarColor('violet'); + $item->addAttribute(pht('Waiting')); break; case HarbormasterBuild::STATUS_BUILDING: $item->setBarColor('yellow'); @@ -66,7 +64,51 @@ final class HarbormasterBuildableViewController $item->setBarColor('red'); $item->addAttribute(pht('Unexpected Error')); break; + case HarbormasterBuild::STATUS_STOPPED: + $item->setBarColor('black'); + $item->addAttribute(pht('Stopped')); + break; } + + if ($build->isRestarting()) { + $item->addIcon('backward', pht('Restarting')); + } else if ($build->isStopping()) { + $item->addIcon('stop', pht('Stopping')); + } else if ($build->isResuming()) { + $item->addIcon('play', pht('Resuming')); + } + + $build_id = $build->getID(); + + $restart_uri = "build/restart/{$build_id}/buildable/"; + $resume_uri = "build/resume/{$build_id}/buildable/"; + $stop_uri = "build/stop/{$build_id}/buildable/"; + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('backward') + ->setName(pht('Restart')) + ->setHref($this->getApplicationURI($restart_uri)) + ->setWorkflow(true) + ->setDisabled(!$build->canRestartBuild())); + + if ($build->canResumeBuild()) { + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('play') + ->setName(pht('Resume')) + ->setHref($this->getApplicationURI($resume_uri)) + ->setWorkflow(true)); + } else { + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('stop') + ->setName(pht('Stop')) + ->setHref($this->getApplicationURI($stop_uri)) + ->setWorkflow(true) + ->setDisabled(!$build->canStopBuild())); + } + $build_list->addItem($item); } @@ -84,9 +126,7 @@ final class HarbormasterBuildableViewController $this->buildPropertyLists($box, $buildable, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("B{$id}")); + $crumbs->addTextCrumb("B{$id}"); return $this->buildApplicationPage( array( @@ -108,16 +148,56 @@ final class HarbormasterBuildableViewController $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($buildable) - ->setObjectURI("/B{$id}"); + ->setObjectURI($buildable->getMonogram()); - $apply_uri = $this->getApplicationURI('/buildable/apply/'.$id.'/'); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $buildable, + PhabricatorPolicyCapability::CAN_EDIT); + + $can_restart = false; + $can_resume = false; + $can_stop = false; + + foreach ($buildable->getBuilds() as $build) { + if ($build->canRestartBuild()) { + $can_restart = true; + } + if ($build->canResumeBuild()) { + $can_resume = true; + } + if ($build->canStopBuild()) { + $can_stop = true; + } + } + + $restart_uri = "buildable/{$id}/restart/"; + $stop_uri = "buildable/{$id}/stop/"; + $resume_uri = "buildable/{$id}/resume/"; $list->addAction( id(new PhabricatorActionView()) - ->setName(pht('Apply Build Plan')) - ->setIcon('edit') - ->setHref($apply_uri) - ->setWorkflow(true)); + ->setIcon('backward') + ->setName(pht('Restart All Builds')) + ->setHref($this->getApplicationURI($restart_uri)) + ->setWorkflow(true) + ->setDisabled(!$can_restart || !$can_edit)); + + $list->addAction( + id(new PhabricatorActionView()) + ->setIcon('stop') + ->setName(pht('Stop All Builds')) + ->setHref($this->getApplicationURI($stop_uri)) + ->setWorkflow(true) + ->setDisabled(!$can_stop || !$can_edit)); + + $list->addAction( + id(new PhabricatorActionView()) + ->setIcon('play') + ->setName(pht('Resume All Builds')) + ->setHref($this->getApplicationURI($resume_uri)) + ->setWorkflow(true) + ->setDisabled(!$can_resume || !$can_edit)); return $list; } @@ -139,6 +219,18 @@ final class HarbormasterBuildableViewController pht('Buildable'), $buildable->getBuildableHandle()->renderLink()); + if ($buildable->getContainerHandle() !== null) { + $properties->addProperty( + pht('Container'), + $buildable->getContainerHandle()->renderLink()); + } + + $properties->addProperty( + pht('Origin'), + $buildable->getIsManualBuildable() + ? pht('Manual Buildable') + : pht('Automatic Buildable')); + } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanController.php b/src/applications/harbormaster/controller/HarbormasterPlanController.php index 6ba3d8c0bd..dd3a7802d7 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanController.php @@ -1,14 +1,13 @@ addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Build Plans')) - ->setHref($this->getApplicationURI('plan/'))); + $crumbs->addTextCrumb( + pht('Build Plans'), + $this->getApplicationURI('plan/')); return $crumbs; } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php new file mode 100644 index 0000000000..31a35944c0 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php @@ -0,0 +1,76 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $this->requireApplicationCapability( + HarbormasterCapabilityManagePlans::CAPABILITY); + + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$plan) { + return new Aphront404Response(); + } + + $plan_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); + + if ($request->isFormPost()) { + + $type_status = HarbormasterBuildPlanTransaction::TYPE_STATUS; + + $v_status = $plan->isDisabled() + ? HarbormasterBuildPlan::STATUS_ACTIVE + : HarbormasterBuildPlan::STATUS_DISABLED; + + $xactions = array(); + + $xactions[] = id(new HarbormasterBuildPlanTransaction()) + ->setTransactionType($type_status) + ->setNewValue($v_status); + + $editor = id(new HarbormasterBuildPlanEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($plan, $xactions); + + return id(new AphrontRedirectResponse())->setURI($plan_uri); + } + + if ($plan->isDisabled()) { + $title = pht('Enable Build Plan'); + $body = pht('Enable this build plan?'); + $button = pht('Enable Plan'); + } else { + $title = pht('Disable Build Plan'); + $body = pht( + 'Disable this build plan? It will no longer be executed '. + 'automatically.'); + $button = pht('Disable Plan'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild($body) + ->addSubmitButton($button) + ->addCancelButton($plan_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterPlanEditController.php b/src/applications/harbormaster/controller/HarbormasterPlanEditController.php index 03fd378147..95495e3420 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanEditController.php @@ -68,7 +68,7 @@ final class HarbormasterPlanEditController $id = $plan->getID(); $title = pht('Edit Build Plan'); - $cancel_uri = "/B{$id}"; + $cancel_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); $save_button = pht('Save Build Plan'); } @@ -93,18 +93,13 @@ final class HarbormasterPlanEditController $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Build Plan'))); + $crumbs->addTextCrumb(pht('New Build Plan')); } else { $id = $plan->getID(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Plan %d", $id)) - ->setHref($this->getApplicationURI("plan/{$id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb( + pht("Plan %d", $id), + $this->getApplicationURI("plan/{$id}/")); + $crumbs->addTextCrumb(pht('Edit')); } return $this->buildApplicationPage( diff --git a/src/applications/harbormaster/controller/HarbormasterPlanListController.php b/src/applications/harbormaster/controller/HarbormasterPlanListController.php index 749de67707..8559ce087e 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanListController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanListController.php @@ -39,6 +39,10 @@ final class HarbormasterPlanListController ->setObjectName(pht('Plan %d', $plan->getID())) ->setHeader($plan->getName()); + if ($plan->isDisabled()) { + $item->setDisabled(true); + } + $item->setHref($this->getApplicationURI("plan/{$id}/")); $list->addItem($item); @@ -53,6 +57,10 @@ final class HarbormasterPlanListController $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + if ($for_app) { + $nav->addFilter('new/', pht('New Build Plan')); + } + id(new HarbormasterBuildPlanSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); diff --git a/src/applications/harbormaster/controller/HarbormasterPlanOrderController.php b/src/applications/harbormaster/controller/HarbormasterPlanOrderController.php new file mode 100644 index 0000000000..ef2af9d3c0 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterPlanOrderController.php @@ -0,0 +1,84 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $request->validateCSRF(); + + $this->requireApplicationCapability( + HarbormasterCapabilityManagePlans::CAPABILITY); + + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$plan) { + return new Aphront404Response(); + } + + // Load all steps. + $order = $request->getStrList('order'); + $steps = id(new HarbormasterBuildStepQuery()) + ->setViewer($user) + ->withIDs($order) + ->execute(); + $steps = array_select_keys($steps, $order); + $reordered_steps = array(); + + // Apply sequences. + $sequence = 1; + foreach ($steps as $step) { + $step->setSequence($sequence++); + $step->save(); + + $reordered_steps[] = $step; + } + + // We must ensure that steps with artifacts become invalid if they are + // placed before the steps that produce them. + foreach ($reordered_steps as $step) { + $implementation = $step->getStepImplementation(); + $settings = $implementation->getSettings(); + foreach ($implementation->getSettingDefinitions() as $name => $opt) { + switch ($opt['type']) { + case BuildStepImplementation::SETTING_TYPE_ARTIFACT: + $value = $settings[$name]; + $filter = $opt['artifact_type']; + $available_artifacts = + BuildStepImplementation::getAvailableArtifacts( + $plan, + $reordered_steps, + $step, + $filter); + $artifact_found = false; + foreach ($available_artifacts as $key => $type) { + if ($key === $value) { + $artifact_found = true; + } + } + if (!$artifact_found) { + $step->setDetail($name, null); + } + break; + } + $step->save(); + } + } + + // Force the page to re-render. + return id(new AphrontRedirectResponse()); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php new file mode 100644 index 0000000000..b9aec75d69 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php @@ -0,0 +1,102 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $this->requireApplicationCapability( + HarbormasterCapabilityManagePlans::CAPABILITY); + + $plan_id = $this->id; + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->withIDs(array($plan_id)) + ->executeOne(); + if (!$plan) { + return new Aphront404Response(); + } + + $e_name = true; + $v_name = null; + + $errors = array(); + if ($request->isFormPost()) { + $buildable = HarbormasterBuildable::initializeNewBuildable($viewer) + ->setIsManualBuildable(true); + + $v_name = $request->getStr('buildablePHID'); + + if ($v_name) { + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames(array($v_name)) + ->executeOne(); + + if ($object instanceof HarbormasterBuildableInterface) { + $buildable + ->setBuildablePHID($object->getHarbormasterBuildablePHID()) + ->setContainerPHID($object->getHarbormasterContainerPHID()); + } else { + $e_name = pht('Invalid'); + $errors[] = pht('Enter the name of a revision or commit.'); + } + } else { + $e_name = pht('Required'); + $errors[] = pht('You must choose a revision or commit to build.'); + } + + if (!$errors) { + $buildable->save(); + $buildable->applyPlan($plan); + + $buildable_uri = '/B'.$buildable->getID(); + return id(new AphrontRedirectResponse())->setURI($buildable_uri); + } + } + + if ($errors) { + $errors = id(new AphrontErrorView())->setErrors($errors); + } + + $title = pht('Run Build Plan Manually'); + $cancel_uri = $this->getApplicationURI("plan/{$plan_id}/"); + $save_button = pht('Run Plan Manually'); + + $form = id(new PHUIFormLayoutView()) + ->setUser($viewer) + ->appendRemarkupInstructions( + pht( + "Enter the name of a commit or revision to run this plan on (for ". + "example, `rX123456` or `D123`).\n\n". + "For more detailed output, you can also run manual builds from ". + "the command line:\n\n". + " phabricator/ $ ./bin/harbormaster build --plan %s", + $plan->getID())) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Buildable Name') + ->setName('buildablePHID') + ->setError($e_name) + ->setValue($v_name)); + + $dialog = id(new AphrontDialogView()) + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setUser($viewer) + ->setTitle($title) + ->appendChild($form) + ->addCancelButton($cancel_uri) + ->addSubmitButton($save_button); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 9cc6944202..b11a6bf288 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -51,9 +51,7 @@ final class HarbormasterPlanViewController $this->buildPropertyLists($box, $plan, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Plan %d", $id))); + $crumbs->addTextCrumb(pht("Plan %d", $id)); $step_list = $this->buildStepList($plan); @@ -74,6 +72,8 @@ final class HarbormasterPlanViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $list_id = celerity_generate_unique_node_id(); + $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) @@ -84,9 +84,40 @@ final class HarbormasterPlanViewController $i = 1; $step_list = id(new PHUIObjectItemListView()) - ->setUser($viewer); + ->setUser($viewer) + ->setID($list_id); + Javelin::initBehavior( + 'harbormaster-reorder-steps', + array( + 'listID' => $list_id, + 'orderURI' => '/harbormaster/plan/order/'.$plan->getID().'/', + )); foreach ($steps as $step) { - $implementation = $step->getStepImplementation(); + $implementation = null; + try { + $implementation = $step->getStepImplementation(); + } catch (Exception $ex) { + // We can't initialize the implementation. This might be because + // it's been renamed or no longer exists. + $item = id(new PHUIObjectItemView()) + ->setObjectName("Step ".$i++) + ->setHeader(pht('Unknown Implementation')) + ->setBarColor('red') + ->addAttribute(pht( + 'This step has an invalid implementation (%s).', + $step->getClassName())) + ->addAction( + id(new PHUIListItemView()) + ->setIcon('delete') + ->addSigil('harbormaster-build-step-delete') + ->setWorkflow(true) + ->setRenderNameAsTooltip(true) + ->setName(pht("Delete")) + ->setHref( + $this->getApplicationURI("step/delete/".$step->getID()."/"))); + $step_list->addItem($item); + continue; + } $item = id(new PHUIObjectItemView()) ->setObjectName("Step ".$i++) ->setHeader($implementation->getName()); @@ -112,6 +143,12 @@ final class HarbormasterPlanViewController ->setName(pht("Delete")) ->setHref( $this->getApplicationURI("step/delete/".$step->getID()."/"))); + $item->setGrippable(true); + $item->addSigil('build-step'); + $item->setMetadata( + array( + 'stepID' => $step->getID(), + )); } $step_list->addItem($item); @@ -141,14 +178,40 @@ final class HarbormasterPlanViewController ->setDisabled(!$can_edit) ->setIcon('edit')); + if ($plan->isDisabled()) { + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Enable Plan')) + ->setHref($this->getApplicationURI("plan/disable/{$id}/")) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setIcon('enable')); + } else { + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Disable Plan')) + ->setHref($this->getApplicationURI("plan/disable/{$id}/")) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setIcon('disable')); + } + $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Build Step')) ->setHref($this->getApplicationURI("step/add/{$id}/")) - ->setWorkflow($can_edit) + ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('new')); + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Run Plan Manually')) + ->setHref($this->getApplicationURI("plan/run/{$id}/")) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setIcon('start-sandcastle')); + return $list; } diff --git a/src/applications/harbormaster/controller/HarbormasterStepAddController.php b/src/applications/harbormaster/controller/HarbormasterStepAddController.php index 095f6149ff..e09cfa4dab 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepAddController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepAddController.php @@ -33,13 +33,16 @@ final class HarbormasterStepAddController if ($request->isDialogFormPost()) { $class = $request->getStr('step-type'); if (!in_array($class, $implementations)) { - return $this->createDialog($implementations); + return $this->createDialog($implementations, $cancel_uri); } + $steps = $plan->loadOrderedBuildSteps(); + $step = new HarbormasterBuildStep(); $step->setBuildPlanPHID($plan->getPHID()); $step->setClassName($class); $step->setDetails(array()); + $step->setSequence(count($steps) + 1); $step->save(); $edit_uri = $this->getApplicationURI("step/edit/".$step->getID()."/"); diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php index 81d3f0b2a8..b141eabafc 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -63,6 +63,11 @@ final class HarbormasterStepEditController $form = id(new AphrontFormView()) ->setUser($viewer); + $instructions = $implementation->getSettingRemarkupInstructions(); + if ($instructions !== null) { + $form->appendRemarkupInstructions($instructions); + } + // We need to render out all of the fields for the settings that // the implementation has. foreach ($implementation->getSettingDefinitions() as $name => $opt) { @@ -86,6 +91,23 @@ final class HarbormasterStepEditController ->setName($name) ->setValue($value); break; + case BuildStepImplementation::SETTING_TYPE_ARTIFACT: + $filter = $opt['artifact_type']; + $available_artifacts = + BuildStepImplementation::loadAvailableArtifacts( + $plan, + $step, + $filter); + $options = array(); + foreach ($available_artifacts as $key => $type) { + $options[$key] = $key; + } + $control = id(new AphrontFormSelectControl()) + ->setLabel($this->getReadableName($name, $opt)) + ->setName($name) + ->setValue($value) + ->setOptions($options); + break; default: throw new Exception("Unable to render field with unknown type."); } @@ -110,13 +132,10 @@ final class HarbormasterStepEditController $crumbs = $this->buildApplicationCrumbs(); $id = $plan->getID(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Plan %d", $id)) - ->setHref($this->getApplicationURI("plan/{$id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Step'))); + $crumbs->addTextCrumb( + pht("Plan %d", $id), + $this->getApplicationURI("plan/{$id}/")); + $crumbs->addTextCrumb(pht('Edit Step')); return $this->buildApplicationPage( array( @@ -140,6 +159,7 @@ final class HarbormasterStepEditController public function getValueFromRequest(AphrontRequest $request, $name, $type) { switch ($type) { case BuildStepImplementation::SETTING_TYPE_STRING: + case BuildStepImplementation::SETTING_TYPE_ARTIFACT: return $request->getStr($name); break; case BuildStepImplementation::SETTING_TYPE_INTEGER: diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php index f1a045e35f..68aadfb8c2 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php @@ -6,6 +6,7 @@ final class HarbormasterBuildPlanEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = HarbormasterBuildPlanTransaction::TYPE_NAME; + $types[] = HarbormasterBuildPlanTransaction::TYPE_STATUS; $types[] = PhabricatorTransactions::TYPE_COMMENT; return $types; } @@ -19,6 +20,8 @@ final class HarbormasterBuildPlanEditor return null; } return $object->getName(); + case HarbormasterBuildPlanTransaction::TYPE_STATUS: + return $object->getPlanStatus(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -30,6 +33,8 @@ final class HarbormasterBuildPlanEditor switch ($xaction->getTransactionType()) { case HarbormasterBuildPlanTransaction::TYPE_NAME: return $xaction->getNewValue(); + case HarbormasterBuildPlanTransaction::TYPE_STATUS: + return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } @@ -41,6 +46,9 @@ final class HarbormasterBuildPlanEditor case HarbormasterBuildPlanTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; + case HarbormasterBuildPlanTransaction::TYPE_STATUS: + $object->setPlanStatus($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); } @@ -50,6 +58,7 @@ final class HarbormasterBuildPlanEditor PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildPlanTransaction::TYPE_NAME: + case HarbormasterBuildPlanTransaction::TYPE_STATUS: return; } return parent::applyCustomExternalTransaction($object, $xaction); @@ -64,14 +73,11 @@ final class HarbormasterBuildPlanEditor switch ($type) { case HarbormasterBuildPlanTransaction::TYPE_NAME: - $missing_name = true; - if (strlen($object->getName()) && empty($xactions)) { - $missing_name = false; - } else if (strlen(last($xactions)->getNewValue())) { - $missing_name = false; - } + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); - if ($missing_name) { + if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php new file mode 100644 index 0000000000..af2057b990 --- /dev/null +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -0,0 +1,248 @@ +newBuildTargets[] = $target; + return $this; + } + + public function getNewBuildTargets() { + return $this->newBuildTargets; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBuild(HarbormasterBuild $build) { + $this->build = $build; + return $this; + } + + public function getBuild() { + return $this->build; + } + + public function continueBuild() { + $build = $this->getBuild(); + + $lock_key = 'harbormaster.build:'.$build->getID(); + $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); + + $build->reload(); + + try { + $this->updateBuild($build); + } catch (Exception $ex) { + // If any exception is raised, the build is marked as a failure and the + // exception is re-thrown (this ensures we don't leave builds in an + // inconsistent state). + $build->setBuildStatus(HarbormasterBuild::STATUS_ERROR); + $build->save(); + + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + + // NOTE: We queue new targets after releasing the lock so that in-process + // execution via `bin/harbormaster` does not reenter the locked region. + foreach ($this->getNewBuildTargets() as $target) { + $task = PhabricatorWorker::scheduleTask( + 'HarbormasterTargetWorker', + array( + 'targetID' => $target->getID(), + )); + } + } + + private function updateBuild(HarbormasterBuild $build) { + if (($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) || + ($build->isRestarting())) { + $this->destroyBuildTargets($build); + $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); + $build->save(); + } + + if ($build->isResuming()) { + $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); + $build->save(); + } + + if ($build->isStopping() && !$build->isComplete()) { + $build->setBuildStatus(HarbormasterBuild::STATUS_STOPPED); + $build->save(); + } + + $build->deleteUnprocessedCommands(); + + if ($build->getBuildStatus() == HarbormasterBuild::STATUS_BUILDING) { + $this->updateBuildSteps($build); + } + } + + private function destroyBuildTargets(HarbormasterBuild $build) { + $targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($this->getViewer()) + ->withBuildPHIDs(array($build->getPHID())) + ->execute(); + + if (!$targets) { + return; + } + + $target_phids = mpull($targets, 'getPHID'); + + $artifacts = id(new HarbormasterBuildArtifactQuery()) + ->setViewer($this->getViewer()) + ->withBuildTargetPHIDs($target_phids) + ->execute(); + + foreach ($artifacts as $artifact) { + $artifact->delete(); + } + + foreach ($targets as $target) { + $target->delete(); + } + } + + private function updateBuildSteps(HarbormasterBuild $build) { + $targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($this->getViewer()) + ->withBuildPHIDs(array($build->getPHID())) + ->execute(); + $targets = mgroup($targets, 'getBuildStepPHID'); + + $steps = id(new HarbormasterBuildStepQuery()) + ->setViewer($this->getViewer()) + ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) + ->execute(); + + // Identify steps which are complete. + + $complete = array(); + $failed = array(); + $waiting = array(); + foreach ($steps as $step) { + $step_targets = idx($targets, $step->getPHID(), array()); + + if ($step_targets) { + $is_complete = true; + foreach ($step_targets as $target) { + if (!$target->isComplete()) { + $is_complete = false; + break; + } + } + + $is_failed = false; + foreach ($step_targets as $target) { + if ($target->isFailed()) { + $is_failed = true; + break; + } + } + + $is_waiting = false; + } else { + $is_complete = false; + $is_failed = false; + $is_waiting = true; + } + + if ($is_complete) { + $complete[$step->getPHID()] = true; + } + + if ($is_failed) { + $failed[$step->getPHID()] = true; + } + + if ($is_waiting) { + $waiting[$step->getPHID()] = true; + } + } + + // If any step failed, fail the whole build, then bail. + if (count($failed)) { + $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); + $build->save(); + return; + } + + // If every step is complete, we're done with this build. Mark it passed + // and bail. + if (count($complete) == count($steps)) { + $build->setBuildStatus(HarbormasterBuild::STATUS_PASSED); + $build->save(); + return; + } + + // Identify all the steps which are ready to run (because all their + // depdendencies are complete). + + $previous_step = null; + $runnable = array(); + foreach ($steps as $step) { + // TODO: For now, we're hard coding sequential dependencies into build + // steps. In the future, we can be smart about this instead. + + if ($previous_step) { + $dependencies = array($previous_step); + } else { + $dependencies = array(); + } + + if (isset($waiting[$step->getPHID()])) { + $can_run = true; + foreach ($dependencies as $dependency) { + if (empty($complete[$dependency->getPHID()])) { + $can_run = false; + break; + } + } + + if ($can_run) { + $runnable[] = $step; + } + } + + $previous_step = $step; + } + + if (!$runnable) { + // TODO: This means the build is deadlocked, probably? It should not + // normally be possible, but we should communicate it more clearly. + $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); + $build->save(); + return; + } + + foreach ($runnable as $runnable_step) { + $target = HarbormasterBuildTarget::initializeNewBuildTarget( + $build, + $runnable_step, + $build->retrieveVariablesFromBuild()); + $target->save(); + + $this->queueNewBuildTarget($target); + } + } + +} diff --git a/src/applications/harbormaster/event/HarbormasterUIEventListener.php b/src/applications/harbormaster/event/HarbormasterUIEventListener.php new file mode 100644 index 0000000000..2a6fee9af3 --- /dev/null +++ b/src/applications/harbormaster/event/HarbormasterUIEventListener.php @@ -0,0 +1,113 @@ +listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES); + } + + public function handleEvent(PhutilEvent $event) { + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES: + $this->handlePropertyEvent($event); + break; + } + } + + private function handlePropertyEvent($ui_event) { + $user = $ui_event->getUser(); + $object = $ui_event->getValue('object'); + + if (!$object || !$object->getPHID()) { + // No object, or the object has no PHID yet.. + return; + } + + if ($object instanceof HarbormasterBuildable) { + // Although HarbormasterBuildable implements the correct interface, it + // does not make sense to show a build's build status. In the best case + // it is meaningless, and in the worst case it's confusing. + return; + } + + if (!($object instanceof HarbormasterBuildableInterface)) { + return; + } + + $buildable_phid = $object->getHarbormasterBuildablePHID(); + if (!$buildable_phid) { + return; + } + + if (!$this->canUseApplication($ui_event->getUser())) { + return; + } + + $buildables = id(new HarbormasterBuildableQuery()) + ->setViewer($user) + ->withManualBuildables(false) + ->withBuildablePHIDs(array($buildable_phid)) + ->execute(); + if (!$buildables) { + return; + } + + $builds = id(new HarbormasterBuildQuery()) + ->setViewer($user) + ->withBuildablePHIDs(mpull($buildables, 'getPHID')) + ->execute(); + if (!$builds) { + return; + } + + $build_handles = id(new PhabricatorHandleQuery()) + ->setViewer($user) + ->withPHIDs(mpull($builds, 'getPHID')) + ->execute(); + + $status_view = new PHUIStatusListView(); + + foreach ($builds as $build) { + $item = new PHUIStatusItemView(); + $item->setTarget($build_handles[$build->getPHID()]->renderLink()); + + switch ($build->getBuildStatus()) { + case HarbormasterBuild::STATUS_INACTIVE: + $item->setIcon('open-dark', pht('Inactive')); + break; + case HarbormasterBuild::STATUS_PENDING: + $item->setIcon('open-blue', pht('Pending')); + break; + case HarbormasterBuild::STATUS_WAITING: + $item->setIcon('up-blue', pht('Waiting on Resource')); + break; + case HarbormasterBuild::STATUS_BUILDING: + $item->setIcon('right-blue', pht('Building')); + break; + case HarbormasterBuild::STATUS_PASSED: + $item->setIcon('accept-green', pht('Passed')); + break; + case HarbormasterBuild::STATUS_FAILED: + $item->setIcon('reject-red', pht('Failed')); + break; + case HarbormasterBuild::STATUS_ERROR: + $item->setIcon('minus-red', pht('Unexpected Error')); + break; + case HarbormasterBuild::STATUS_STOPPED: + $item->setIcon('minus-dark', pht('Stopped')); + break; + default: + $item->setIcon('question', pht('Unknown')); + break; + } + + + $status_view->addItem($item); + } + + $view = $ui_event->getValue('view'); + $view->addProperty(pht('Build Status'), $status_view); + } + +} diff --git a/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php b/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php new file mode 100644 index 0000000000..7fd936e6a7 --- /dev/null +++ b/src/applications/harbormaster/interface/HarbormasterBuildableInterface.php @@ -0,0 +1,8 @@ +setName('build') + ->setExamples('**build** [__options__] __buildable__ --plan __id__') + ->setSynopsis(pht('Run plan __id__ on __buildable__.')) + ->setArguments( + array( + array( + 'name' => 'plan', + 'param' => 'id', + 'help' => pht('ID of build plan to run.'), + ), + array( + 'name' => 'buildable', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $names = $args->getArg('buildable'); + if (count($names) != 1) { + throw new PhutilArgumentUsageException( + pht('Specify exactly one buildable, by object name.')); + } + + $name = head($names); + + $buildable = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames($names) + ->executeOne(); + if (!$buildable) { + throw new PhutilArgumentUsageException( + pht('No such buildable "%s"!', $name)); + } + + if (!($buildable instanceof HarbormasterBuildableInterface)) { + throw new PhutilArgumentUsageException( + pht('Object "%s" is not a buildable!', $name)); + } + + $plan_id = $args->getArg('plan'); + if (!$plan_id) { + throw new PhutilArgumentUsageException( + pht('Use --plan to specify a build plan to run.')); + } + + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->withIDs(array($plan_id)) + ->executeOne(); + if (!$plan) { + throw new PhutilArgumentUsageException( + pht('Build plan "%s" does not exist.', $plan_id)); + } + + $console = PhutilConsole::getConsole(); + + $buildable = HarbormasterBuildable::initializeNewBuildable($viewer) + ->setIsManualBuildable(true) + ->setBuildablePHID($buildable->getHarbormasterBuildablePHID()) + ->setContainerPHID($buildable->getHarbormasterContainerPHID()) + ->save(); + + $console->writeOut( + "%s\n", + pht( + 'Applying plan %s to new buildable %s...', + $plan->getID(), + 'B'.$buildable->getID())); + + $console->writeOut( + "\n %s\n\n", + PhabricatorEnv::getProductionURI('/B'.$buildable->getID())); + + PhabricatorWorker::setRunAllTasksInProcess(true); + $buildable->applyPlan($plan); + + $console->writeOut("%s\n", pht('Done.')); + + return 0; + } + +} diff --git a/src/applications/harbormaster/management/HarbormasterManagementWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementWorkflow.php new file mode 100644 index 0000000000..c92e97fa57 --- /dev/null +++ b/src/applications/harbormaster/management/HarbormasterManagementWorkflow.php @@ -0,0 +1,6 @@ + $handle) { - $build_plan = $objects[$phid]; + $build = $objects[$phid]; + $handles[$phid]->setName(pht( + 'Build %d: %s', + $build->getID(), + $build->getName())); + $handles[$phid]->setURI( + '/harbormaster/build/'.$build->getID()); } } diff --git a/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php new file mode 100644 index 0000000000..385cbcaac6 --- /dev/null +++ b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php @@ -0,0 +1,37 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $build_log = $objects[$phid]; + } + } + +} diff --git a/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php index c378902f5d..0a18c73d85 100644 --- a/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php +++ b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php @@ -31,6 +31,8 @@ final class HarbormasterPHIDTypeBuildPlan extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $build_plan = $objects[$phid]; + $handles[$phid]->setName($build_plan->getName()); + $handles[$phid]->setURI('/harbormaster/plan/'.$build_plan->getID()); } } diff --git a/src/applications/harbormaster/query/HarbormasterBuildableArtifactQuery.php b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php similarity index 64% rename from src/applications/harbormaster/query/HarbormasterBuildableArtifactQuery.php rename to src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php index 901335a0ca..44a5e7dcda 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildableArtifactQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php @@ -1,20 +1,21 @@ ids = $ids; return $this; } - public function withBuildablePHIDs(array $buildable_phids) { - $this->buildablePHIDs = $buildable_phids; + public function withBuildTargetPHIDs(array $build_target_phids) { + $this->buildTargetPHIDs = $build_target_phids; return $this; } @@ -23,7 +24,8 @@ final class HarbormasterBuildableArtifactQuery return $this; } - public function withArtifactKeys(array $artifact_keys) { + public function withArtifactKeys($build_phid, array $artifact_keys) { + $this->keyBuildPHID = $build_phid; $this->artifactKeys = $artifact_keys; return $this; } @@ -44,25 +46,25 @@ final class HarbormasterBuildableArtifactQuery } protected function willFilterPage(array $page) { - $buildables = array(); + $build_targets = array(); - $buildable_phids = array_filter(mpull($page, 'getBuildablePHID')); - if ($buildable_phids) { - $buildables = id(new PhabricatorObjectQuery()) + $build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID')); + if ($build_target_phids) { + $build_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) - ->withPHIDs($buildable_phids) + ->withPHIDs($build_target_phids) ->setParentQuery($this) ->execute(); - $buildables = mpull($buildables, null, 'getPHID'); + $build_targets = mpull($build_targets, null, 'getPHID'); } - foreach ($page as $key => $artifact) { - $buildable_phid = $artifact->getBuildablePHID(); - if (empty($buildables[$buildable_phid])) { + foreach ($page as $key => $build_log) { + $build_target_phid = $build_log->getBuildTargetPHID(); + if (empty($build_targets[$build_target_phid])) { unset($page[$key]); continue; } - $artifact->attachBuildable($buildables[$buildable_phid]); + $build_log->attachBuildTarget($build_targets[$build_target_phid]); } return $page; @@ -78,11 +80,11 @@ final class HarbormasterBuildableArtifactQuery $this->ids); } - if ($this->buildablePHIDs) { + if ($this->buildTargetPHIDs) { $where[] = qsprintf( $conn_r, - 'buildablePHID IN (%Ls)', - $this->buildablePHIDs); + 'buildTargetPHID IN (%Ls)', + $this->buildTargetPHIDs); } if ($this->artifactTypes) { @@ -95,7 +97,8 @@ final class HarbormasterBuildableArtifactQuery if ($this->artifactKeys) { $indexes = array(); foreach ($this->artifactKeys as $key) { - $indexes[] = PhabricatorHash::digestForIndex($key); + $indexes[] = PhabricatorHash::digestForIndex( + $this->keyBuildPHID.$key); } $where[] = qsprintf( diff --git a/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php new file mode 100644 index 0000000000..f25597f48e --- /dev/null +++ b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php @@ -0,0 +1,98 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withBuildTargetPHIDs(array $build_target_phids) { + $this->buildTargetPHIDs = $build_target_phids; + return $this; + } + + protected function loadPage() { + $table = new HarbormasterBuildLog(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function willFilterPage(array $page) { + $build_targets = array(); + + $build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID')); + if ($build_target_phids) { + $build_targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($build_target_phids) + ->setParentQuery($this) + ->execute(); + $build_targets = mpull($build_targets, null, 'getPHID'); + } + + foreach ($page as $key => $build_log) { + $build_target_phid = $build_log->getBuildTargetPHID(); + if (empty($build_targets[$build_target_phid])) { + unset($page[$key]); + continue; + } + $build_log->attachBuildTarget($build_targets[$build_target_phid]); + } + + return $page; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->buildTargetPHIDs) { + $where[] = qsprintf( + $conn_r, + 'buildTargetPHID IN (%Ls)', + $this->buildTargetPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationHarbormaster'; + } + +} diff --git a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php index a888b876b1..e3863a438a 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php @@ -5,6 +5,7 @@ final class HarbormasterBuildPlanQuery private $ids; private $phids; + private $statuses; public function withIDs(array $ids) { $this->ids = $ids; @@ -16,6 +17,11 @@ final class HarbormasterBuildPlanQuery return $this; } + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + protected function loadPage() { $table = new HarbormasterBuildPlan(); $conn_r = $table->establishConnection('r'); @@ -49,6 +55,13 @@ final class HarbormasterBuildPlanQuery $this->phids); } + if ($this->statuses) { + $where[] = qsprintf( + $conn_r, + 'planStatus IN (%Ls)', + $this->statuses); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); diff --git a/src/applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php index 41cd854d77..76402cc5c5 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php +++ b/src/applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php @@ -6,12 +6,21 @@ final class HarbormasterBuildPlanSearchEngine public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); + $saved->setParameter( + 'status', + $this->readListFromRequest($request, 'status')); + return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new HarbormasterBuildPlanQuery()); + $status = $saved->getParameter('status', array()); + if ($status) { + $query->withStatuses($status); + } + return $query; } @@ -19,6 +28,23 @@ final class HarbormasterBuildPlanSearchEngine AphrontFormView $form, PhabricatorSavedQuery $saved_query) { + $status = $saved_query->getParameter('status', array()); + + $form + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setLabel('Status') + ->addCheckbox( + 'status[]', + HarbormasterBuildPlan::STATUS_ACTIVE, + pht('Active'), + in_array(HarbormasterBuildPlan::STATUS_ACTIVE, $status)) + ->addCheckbox( + 'status[]', + HarbormasterBuildPlan::STATUS_DISABLED, + pht('Disabled'), + in_array(HarbormasterBuildPlan::STATUS_DISABLED, $status))); + } protected function getURI($path) { @@ -27,6 +53,7 @@ final class HarbormasterBuildPlanSearchEngine public function getBuiltinQueryNames() { $names = array( + 'active' => pht('Active Plans'), 'all' => pht('All Plans'), ); @@ -39,6 +66,12 @@ final class HarbormasterBuildPlanSearchEngine $query->setQueryKey($query_key); switch ($query_key) { + case 'active': + return $query->setParameter( + 'status', + array( + HarbormasterBuildPlan::STATUS_ACTIVE, + )); case 'all': return $query; } diff --git a/src/applications/harbormaster/query/HarbormasterBuildQuery.php b/src/applications/harbormaster/query/HarbormasterBuildQuery.php index cc4fe92bcf..930cd07b83 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildQuery.php @@ -9,8 +9,6 @@ final class HarbormasterBuildQuery private $buildablePHIDs; private $buildPlanPHIDs; - private $needBuildPlans; - public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -36,11 +34,6 @@ final class HarbormasterBuildQuery return $this; } - public function needBuildPlans($need_plans) { - $this->needBuildPlans = $need_plans; - return $this; - } - protected function loadPage() { $table = new HarbormasterBuild(); $conn_r = $table->establishConnection('r'); @@ -82,23 +75,31 @@ final class HarbormasterBuildQuery } protected function didFilterPage(array $page) { - if ($this->needBuildPlans) { - $plans = array(); + $plans = array(); - $plan_phids = array_filter(mpull($page, 'getBuildPlanPHID')); - if ($plan_phids) { - $plans = id(new PhabricatorObjectQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($plan_phids) - ->setParentQuery($this) - ->execute(); - $plans = mpull($plans, null, 'getPHID'); - } + $plan_phids = array_filter(mpull($page, 'getBuildPlanPHID')); + if ($plan_phids) { + $plans = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($plan_phids) + ->setParentQuery($this) + ->execute(); + $plans = mpull($plans, null, 'getPHID'); + } - foreach ($page as $key => $build) { - $plan_phid = $build->getBuildPlanPHID(); - $build->attachBuildPlan(idx($plans, $plan_phid)); - } + foreach ($page as $key => $build) { + $plan_phid = $build->getBuildPlanPHID(); + $build->attachBuildPlan(idx($plans, $plan_phid)); + } + + $build_phids = mpull($page, 'getPHID'); + $commands = id(new HarbormasterBuildCommand())->loadAllWhere( + 'targetPHID IN (%Ls) ORDER BY id ASC', + $build_phids); + $commands = mgroup($commands, 'getTargetPHID'); + foreach ($page as $build) { + $unprocessed_commands = idx($commands, $build->getPHID(), array()); + $build->attachUnprocessedCommands($unprocessed_commands); } return $page; diff --git a/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php b/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php index 3bcc057f16..db452da20b 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php @@ -23,7 +23,7 @@ final class HarbormasterBuildStepQuery } public function getPagingColumn() { - return 'id'; + return 'sequence'; } public function getReversePaging() { diff --git a/src/applications/harbormaster/query/HarbormasterBuildTargetQuery.php b/src/applications/harbormaster/query/HarbormasterBuildTargetQuery.php index 2d7e1fa7d9..aff82090ba 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildTargetQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildTargetQuery.php @@ -5,6 +5,7 @@ final class HarbormasterBuildTargetQuery private $ids; private $phids; + private $buildPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -16,6 +17,11 @@ final class HarbormasterBuildTargetQuery return $this; } + public function withBuildPHIDs(array $build_phids) { + $this->buildPHIDs = $build_phids; + return $this; + } + protected function loadPage() { $table = new HarbormasterBuildTarget(); $conn_r = $table->establishConnection('r'); @@ -48,11 +54,43 @@ final class HarbormasterBuildTargetQuery $this->phids); } + if ($this->buildPHIDs) { + $where[] = qsprintf( + $conn_r, + 'buildPHID in (%Ls)', + $this->buildPHIDs); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } + protected function willFilterPage(array $page) { + $builds = array(); + + $build_phids = array_filter(mpull($page, 'getBuildPHID')); + if ($build_phids) { + $builds = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($build_phids) + ->setParentQuery($this) + ->execute(); + $builds = mpull($builds, null, 'getPHID'); + } + + foreach ($page as $key => $build_target) { + $build_phid = $build_target->getBuildPHID(); + if (empty($builds[$build_phid])) { + unset($page[$key]); + continue; + } + $build_target->attachBuild($builds[$build_phid]); + } + + return $page; + } + public function getQueryApplicationClass() { return 'PhabricatorApplicationHarbormaster'; } diff --git a/src/applications/harbormaster/query/HarbormasterBuildableQuery.php b/src/applications/harbormaster/query/HarbormasterBuildableQuery.php index decf9d8a40..1fb1d4224d 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildableQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildableQuery.php @@ -7,8 +7,10 @@ final class HarbormasterBuildableQuery private $phids; private $buildablePHIDs; private $containerPHIDs; + private $manualBuildables; private $needContainerObjects; + private $needContainerHandles; private $needBuildableHandles; private $needBuilds; @@ -32,11 +34,21 @@ final class HarbormasterBuildableQuery return $this; } + public function withManualBuildables($manual) { + $this->manualBuildables = $manual; + return $this; + } + public function needContainerObjects($need) { $this->needContainerObjects = $need; return $this; } + public function needContainerHandles($need) { + $this->needContainerHandles = $need; + return $this; + } + public function needBuildableHandles($need) { $this->needBuildableHandles = $need; return $this; @@ -88,22 +100,42 @@ final class HarbormasterBuildableQuery } protected function didFilterPage(array $page) { - if ($this->needContainerObjects) { - $containers = array(); - + if ($this->needContainerObjects || $this->needContainerHandles) { $container_phids = array_filter(mpull($page, 'getContainerPHID')); - if ($container_phids) { - $containers = id(new PhabricatorObjectQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($container_phids) - ->setParentQuery($this) - ->execute(); - $containers = mpull($containers, null, 'getPHID'); + + if ($this->needContainerObjects) { + $containers = array(); + + if ($container_phids) { + $containers = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($container_phids) + ->setParentQuery($this) + ->execute(); + $containers = mpull($containers, null, 'getPHID'); + } + + foreach ($page as $key => $buildable) { + $container_phid = $buildable->getContainerPHID(); + $buildable->attachContainerObject(idx($containers, $container_phid)); + } } - foreach ($page as $key => $buildable) { - $container_phid = $buildable->getContainerPHID(); - $buildable->attachContainerObject(idx($containers, $container_phid)); + if ($this->needContainerHandles) { + $handles = array(); + + if ($container_phids) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($container_phids) + ->setParentQuery($this) + ->execute(); + } + + foreach ($page as $key => $buildable) { + $container_phid = $buildable->getContainerPHID(); + $buildable->attachContainerHandle(idx($handles, $container_phid)); + } } } @@ -171,6 +203,13 @@ final class HarbormasterBuildableQuery $this->containerPHIDs); } + if ($this->manualBuildables !== null) { + $where[] = qsprintf( + $conn_r, + 'isManualBuildable = %d', + (int)$this->manualBuildables); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); diff --git a/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php index c37aa7da3b..3f33276af5 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php +++ b/src/applications/harbormaster/query/HarbormasterBuildableSearchEngine.php @@ -6,14 +6,71 @@ final class HarbormasterBuildableSearchEngine public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); + $revisions = $this->readPHIDsFromRequest( + $request, + 'revisions', + array( + DifferentialPHIDTypeRevision::TYPECONST, + )); + + $repositories = $this->readPHIDsFromRequest( + $request, + 'repositories', + array( + PhabricatorRepositoryPHIDTypeRepository::TYPECONST, + )); + + $container_phids = array_merge($revisions, $repositories); + $saved->setParameter('containerPHIDs', $container_phids); + + $commits = $this->readPHIDsFromRequest( + $request, + 'commits', + array( + PhabricatorRepositoryPHIDTypeCommit::TYPECONST, + )); + + $diffs = $this->readListFromRequest($request, 'diffs'); + if ($diffs) { + $diffs = id(new DifferentialDiffQuery()) + ->setViewer($this->requireViewer()) + ->withIDs($diffs) + ->execute(); + $diffs = mpull($diffs, 'getPHID', 'getPHID'); + } + + $buildable_phids = array_merge($commits, $diffs); + $saved->setParameter('buildablePHIDs', $buildable_phids); + + $saved->setParameter( + 'manual', + $this->readBoolFromRequest($request, 'manual')); + return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new HarbormasterBuildableQuery()) + ->needContainerHandles(true) ->needBuildableHandles(true) ->needBuilds(true); + $container_phids = $saved->getParameter('containerPHIDs', array()); + if ($container_phids) { + $query->withContainerPHIDs($container_phids); + } + + $buildable_phids = $saved->getParameter('buildablePHIDs', array()); + + if ($buildable_phids) { + $query->withBuildablePHIDs($buildable_phids); + } + + $manual = $saved->getParameter('manual'); + if ($manual !== null) { + $query->withManualBuildables($manual); + } + return $query; } @@ -21,6 +78,74 @@ final class HarbormasterBuildableSearchEngine AphrontFormView $form, PhabricatorSavedQuery $saved_query) { + $container_phids = $saved_query->getParameter('containerPHIDs', array()); + $buildable_phids = $saved_query->getParameter('buildablePHIDs', array()); + + $all_phids = array_merge($container_phids, $buildable_phids); + + $revision_names = array(); + $diff_names = array(); + $repository_names = array(); + $commit_names = array(); + + if ($all_phids) { + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->requireViewer()) + ->withPHIDs($all_phids) + ->execute(); + + foreach ($all_phids as $phid) { + $object = idx($objects, $phid); + if (!$object) { + continue; + } + + if ($object instanceof DifferentialRevision) { + $revision_names[] = 'D'.$object->getID(); + } else if ($object instanceof DifferentialDiff) { + $diff_names[] = $object->getID(); + } else if ($object instanceof PhabricatorRepository) { + $repository_names[] = 'r'.$object->getCallsign(); + } else if ($object instanceof PhabricatorRepositoryCommit) { + $repository = $object->getRepository(); + $commit_names[] = $repository->formatCommitName( + $object->getCommitIdentifier()); + } + } + } + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Differential Revisions')) + ->setName('revisions') + ->setValue(implode(', ', $revision_names))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Differential Diffs')) + ->setName('diffs') + ->setValue(implode(', ', $diff_names))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Repositories')) + ->setName('repositories') + ->setValue(implode(', ', $repository_names))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Commits')) + ->setName('commits') + ->setValue(implode(', ', $commit_names))) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Origin')) + ->setName('manual') + ->setValue($this->getBoolFromQuery($saved_query, 'manual')) + ->setOptions( + array( + '' => pht('(All Origins)'), + 'true' => pht('Manual Buildables'), + 'false' => pht('Automatic Buildables'), + ))); } protected function getURI($path) { diff --git a/src/applications/harbormaster/step/BuildStepImplementation.php b/src/applications/harbormaster/step/BuildStepImplementation.php index 6a524f4e39..087aba2f7e 100644 --- a/src/applications/harbormaster/step/BuildStepImplementation.php +++ b/src/applications/harbormaster/step/BuildStepImplementation.php @@ -7,6 +7,7 @@ abstract class BuildStepImplementation { const SETTING_TYPE_STRING = 'string'; const SETTING_TYPE_INTEGER = 'integer'; const SETTING_TYPE_BOOLEAN = 'boolean'; + const SETTING_TYPE_ARTIFACT = 'artifact'; public static function getImplementations() { $symbols = id(new PhutilSymbolLoader()) @@ -36,9 +37,11 @@ abstract class BuildStepImplementation { } /** - * Run the build step against the specified build. + * Run the build target against the specified build. */ - abstract public function execute(HarbormasterBuild $build); + abstract public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. @@ -50,18 +53,19 @@ abstract class BuildStepImplementation { /** * Validate the current settings of this build step. */ - public function validate() { + public function validateSettings() { return true; } /** - * Loads the settings for this build step implementation from the build step. + * Loads the settings for this build step implementation from a build + * step or target. */ - public final function loadSettings(HarbormasterBuildStep $build_step) { + public final function loadSettings($build_object) { $this->settings = array(); $this->validateSettingDefinitions(); foreach ($this->getSettingDefinitions() as $name => $opt) { - $this->settings[$name] = $build_step->getDetail($name); + $this->settings[$name] = $build_object->getDetail($name); } return $this->settings; } @@ -85,4 +89,75 @@ abstract class BuildStepImplementation { public function getSettingDefinitions() { return array(); } + + /** + * Return relevant setting instructions as Remarkup. + */ + public function getSettingRemarkupInstructions() { + return null; + } + + /** + * Return the name of artifacts produced by this command. + * + * Something like: + * + * return array( + * 'some_name_input_by_user' => 'host'); + * + * Future steps will calculate all available artifact mappings + * before them and filter on the type. + * + * @return array The mappings of artifact names to their types. + */ + public function getArtifactMappings() { + return array(); + } + + /** + * Returns a list of all artifacts made available by previous build steps. + */ + public static function loadAvailableArtifacts( + HarbormasterBuildPlan $build_plan, + HarbormasterBuildStep $current_build_step, + $artifact_type) { + + $build_steps = $build_plan->loadOrderedBuildSteps(); + + return self::getAvailableArtifacts( + $build_plan, + $build_steps, + $current_build_step, + $artifact_type); + } + + /** + * Returns a list of all artifacts made available by previous build steps. + */ + public static function getAvailableArtifacts( + HarbormasterBuildPlan $build_plan, + array $build_steps, + HarbormasterBuildStep $current_build_step, + $artifact_type) { + + $previous_implementations = array(); + foreach ($build_steps as $build_step) { + if ($build_step->getPHID() === $current_build_step->getPHID()) { + break; + } + $previous_implementations[] = $build_step->getStepImplementation(); + } + + $artifact_arrays = mpull($previous_implementations, 'getArtifactMappings'); + $artifacts = array(); + foreach ($artifact_arrays as $array) { + foreach ($array as $name => $type) { + if ($type !== $artifact_type && $artifact_type !== null) { + continue; + } + $artifacts[$name] = $type; + } + } + return $artifacts; + } } diff --git a/src/applications/harbormaster/step/CommandBuildStepImplementation.php b/src/applications/harbormaster/step/CommandBuildStepImplementation.php new file mode 100644 index 0000000000..16312edcfd --- /dev/null +++ b/src/applications/harbormaster/step/CommandBuildStepImplementation.php @@ -0,0 +1,108 @@ +getSettings(); + + return pht( + 'Run \'%s\' on \'%s\'.', + $settings['command'], + $settings['hostartifact']); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $command = $this->mergeVariables( + 'vcsprintf', + $settings['command'], + $variables); + + $artifact = $build->loadArtifact($settings['hostartifact']); + + $lease = $artifact->loadDrydockLease(); + + $interface = $lease->getInterface('command'); + + $future = $interface->getExecFuture('%C', $command); + + $log_stdout = $build->createLog($build_target, "remote", "stdout"); + $log_stderr = $build->createLog($build_target, "remote", "stderr"); + + $start_stdout = $log_stdout->start(); + $start_stderr = $log_stderr->start(); + + // Read the next amount of available output every second. + while (!$future->isReady()) { + list($stdout, $stderr) = $future->read(); + $log_stdout->append($stdout); + $log_stderr->append($stderr); + $future->discardBuffers(); + + // Wait one second before querying for more data. + sleep(1); + } + + // Get the return value so we can log that as well. + list($err) = $future->resolve(); + + // Retrieve the last few bits of information. + list($stdout, $stderr) = $future->read(); + $log_stdout->append($stdout); + $log_stderr->append($stderr); + $future->discardBuffers(); + + $log_stdout->finalize($start_stdout); + $log_stderr->finalize($start_stderr); + + if ($err) { + throw new Exception(pht('Command failed with error %d.', $err)); + } + } + + public function validateSettings() { + $settings = $this->getSettings(); + + if ($settings['command'] === null || !is_string($settings['command'])) { + return false; + } + if ($settings['hostartifact'] === null || + !is_string($settings['hostartifact'])) { + return false; + } + + // TODO: Check if the host artifact is provided by previous build steps. + + return true; + } + + public function getSettingDefinitions() { + return array( + 'command' => array( + 'name' => 'Command', + 'description' => 'The command to execute on the remote machine.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'hostartifact' => array( + 'name' => 'Host Artifact', + 'description' => + 'The host artifact that determines what machine the command '. + 'will run on.', + 'type' => BuildStepImplementation::SETTING_TYPE_ARTIFACT, + 'artifact_type' => HarbormasterBuildArtifact::TYPE_HOST)); + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php new file mode 100644 index 0000000000..ba6f3e6c7e --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php @@ -0,0 +1,70 @@ +getSettings(); + + $uri = new PhutilURI($settings['uri']); + $domain = $uri->getDomain(); + return pht('Make an HTTP request to %s', $domain); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $uri = $this->mergeVariables( + 'vurisprintf', + $settings['uri'], + $variables); + + $log_body = $build->createLog($build_target, $uri, 'http-body'); + $start = $log_body->start(); + + list($status, $body, $headers) = id(new HTTPSFuture($uri)) + ->setMethod('POST') + ->setTimeout(60) + ->resolve(); + + $log_body->append($body); + $log_body->finalize($start); + + if ($status->getStatusCode() != 200) { + $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); + } + } + + public function validateSettings() { + $settings = $this->getSettings(); + + if ($settings['uri'] === null || !is_string($settings['uri'])) { + return false; + } + + return true; + } + + public function getSettingDefinitions() { + return array( + 'uri' => array( + 'name' => 'URI', + 'description' => pht('The URI to request.'), + 'type' => BuildStepImplementation::SETTING_TYPE_STRING, + ), + ); + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php b/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php new file mode 100644 index 0000000000..3a2adb8d41 --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php @@ -0,0 +1,25 @@ +getSettings(); + + return pht( + 'Obtain a lease on a Drydock host whose platform is \'%s\' and store '. + 'the resulting lease in a host artifact called \'%s\'.', + $settings['platform'], + $settings['name']); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + + // Create the lease. + $lease = id(new DrydockLease()) + ->setResourceType('host') + ->setAttributes( + array( + 'platform' => $settings['platform'], + )) + ->queueForActivation(); + + // Wait until the lease is fulfilled. + // TODO: This will throw an exception if the lease can't be fulfilled; + // we should treat that as build failure not build error. + $lease->waitUntilActive(); + + // Create the associated artifact. + $artifact = $build->createArtifact( + $build_target, + $settings['name'], + HarbormasterBuildArtifact::TYPE_HOST); + $artifact->setArtifactData(array( + 'drydock-lease' => $lease->getID())); + $artifact->save(); + } + + public function getArtifactMappings() { + $settings = $this->getSettings(); + + return array( + $settings['name'] => HarbormasterBuildArtifact::TYPE_HOST); + } + + public function validateSettings() { + $settings = $this->getSettings(); + + if ($settings['name'] === null || !is_string($settings['name'])) { + return false; + } + if ($settings['platform'] === null || !is_string($settings['platform'])) { + return false; + } + + return true; + } + + public function getSettingDefinitions() { + return array( + 'name' => array( + 'name' => 'Artifact Name', + 'description' => + 'The name of the artifact to reference in future build steps.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'platform' => array( + 'name' => 'Platform', + 'description' => 'The platform of the host.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING)); + } + +} diff --git a/src/applications/harbormaster/step/PublishFragmentBuildStepImplementation.php b/src/applications/harbormaster/step/PublishFragmentBuildStepImplementation.php new file mode 100644 index 0000000000..8689538789 --- /dev/null +++ b/src/applications/harbormaster/step/PublishFragmentBuildStepImplementation.php @@ -0,0 +1,91 @@ +getSettings(); + + return pht( + 'Publish file artifact \'%s\' to the fragment path \'%s\'.', + $settings['artifact'], + $settings['path']); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $path = $this->mergeVariables( + 'vsprintf', + $settings['path'], + $variables); + + $artifact = $build->loadArtifact($settings['artifact']); + + $file = $artifact->loadPhabricatorFile(); + + $fragment = id(new PhragmentFragmentQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPaths(array($path)) + ->executeOne(); + + if ($fragment === null) { + PhragmentFragment::createFromFile( + PhabricatorUser::getOmnipotentUser(), + $file, + $path, + PhabricatorPolicies::getMostOpenPolicy(), + PhabricatorPolicies::POLICY_USER); + } else { + if ($file->getMimeType() === "application/zip") { + $fragment->updateFromZIP(PhabricatorUser::getOmnipotentUser(), $file); + } else { + $fragment->updateFromFile(PhabricatorUser::getOmnipotentUser(), $file); + } + } + } + + public function validateSettings() { + $settings = $this->getSettings(); + + if ($settings['path'] === null || !is_string($settings['path'])) { + return false; + } + if ($settings['artifact'] === null || + !is_string($settings['artifact'])) { + return false; + } + + // TODO: Check if the file artifact is provided by previous build steps. + + return true; + } + + public function getSettingDefinitions() { + return array( + 'path' => array( + 'name' => 'Path', + 'description' => + 'The path of the fragment that will be published.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'artifact' => array( + 'name' => 'File Artifact', + 'description' => + 'The file artifact that will be published to Phragment.', + 'type' => BuildStepImplementation::SETTING_TYPE_ARTIFACT, + 'artifact_type' => HarbormasterBuildArtifact::TYPE_FILE)); + } + +} diff --git a/src/applications/harbormaster/step/SleepBuildStepImplementation.php b/src/applications/harbormaster/step/SleepBuildStepImplementation.php index e80f559e29..120732226d 100644 --- a/src/applications/harbormaster/step/SleepBuildStepImplementation.php +++ b/src/applications/harbormaster/step/SleepBuildStepImplementation.php @@ -16,7 +16,10 @@ final class SleepBuildStepImplementation extends BuildStepImplementation { return pht('Sleep for %s seconds.', $settings['seconds']); } - public function execute(HarbormasterBuild $build) { + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + $settings = $this->getSettings(); sleep($settings['seconds']); diff --git a/src/applications/harbormaster/step/UploadArtifactBuildStepImplementation.php b/src/applications/harbormaster/step/UploadArtifactBuildStepImplementation.php new file mode 100644 index 0000000000..bc19b6ba1a --- /dev/null +++ b/src/applications/harbormaster/step/UploadArtifactBuildStepImplementation.php @@ -0,0 +1,104 @@ +getSettings(); + + return pht( + 'Upload artifact located at \'%s\' on \'%s\'.', + $settings['path'], + $settings['hostartifact']); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $path = $this->mergeVariables( + 'vsprintf', + $settings['path'], + $variables); + + $artifact = $build->loadArtifact($settings['hostartifact']); + + $lease = $artifact->loadDrydockLease(); + + $interface = $lease->getInterface('filesystem'); + + // TODO: Handle exceptions. + $file = $interface->saveFile($path, $settings['name']); + + // Insert the artifact record. + $artifact = $build->createArtifact( + $build_target, + $settings['name'], + HarbormasterBuildArtifact::TYPE_FILE); + $artifact->setArtifactData(array( + 'filePHID' => $file->getPHID())); + $artifact->save(); + } + + public function getArtifactMappings() { + $settings = $this->getSettings(); + + return array( + $settings['name'] => HarbormasterBuildArtifact::TYPE_FILE); + } + + public function validateSettings() { + $settings = $this->getSettings(); + + if ($settings['path'] === null || !is_string($settings['path'])) { + return false; + } + if ($settings['name'] === null || !is_string($settings['name'])) { + return false; + } + if ($settings['hostartifact'] === null || + !is_string($settings['hostartifact'])) { + return false; + } + + // TODO: Check if the host artifact is provided by previous build steps. + + return true; + } + + public function getSettingDefinitions() { + return array( + 'path' => array( + 'name' => 'Path', + 'description' => + 'The path of the file that should be retrieved. Note that on '. + 'Windows machines running FreeSSHD, this path will be relative '. + 'to the SFTP root path (configured under the SFTP tab). You can '. + 'not specify an absolute path for Windows machines.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'name' => array( + 'name' => 'Local Name', + 'description' => + 'The name for the file when it is stored in Phabricator.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'hostartifact' => array( + 'name' => 'Host Artifact', + 'description' => + 'The host artifact that determines what machine the command '. + 'will run on.', + 'type' => BuildStepImplementation::SETTING_TYPE_ARTIFACT, + 'artifact_type' => HarbormasterBuildArtifact::TYPE_HOST)); + } + +} diff --git a/src/applications/harbormaster/step/VariableBuildStepImplementation.php b/src/applications/harbormaster/step/VariableBuildStepImplementation.php new file mode 100644 index 0000000000..fd114ada29 --- /dev/null +++ b/src/applications/harbormaster/step/VariableBuildStepImplementation.php @@ -0,0 +1,52 @@ +[a-z\\.]+)\\}/'; + + $matches = null; + preg_match_all($regexp, $pattern, $matches); + + $argv = array(); + foreach ($matches['name'] as $name) { + if (!array_key_exists($name, $variables)) { + throw new Exception(pht("No such variable '%s'!", $name)); + } + $argv[] = $variables[$name]; + } + + $pattern = str_replace('%', '%%', $pattern); + $pattern = preg_replace($regexp, '%s', $pattern); + + return call_user_func($function, $pattern, $argv); + } + + public function getSettingRemarkupInstructions() { + $variables = HarbormasterBuild::getAvailableBuildVariables(); + $text = ''; + $text .= pht('The following variables are available: ')."\n"; + $text .= "\n"; + foreach ($variables as $name => $desc) { + $text .= ' - `'.$name.'`: '.$desc."\n"; + } + $text .= "\n"; + $text .= "Use `\${name}` to merge a variable into a setting."; + return $text; + } + +} diff --git a/src/applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php b/src/applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php new file mode 100644 index 0000000000..82ae2fddce --- /dev/null +++ b/src/applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php @@ -0,0 +1,110 @@ +getBuildable(); + $object = $buildable->getBuildableObject(); + if (!($object instanceof PhabricatorRepositoryCommit)) { + return; + } + + // Block until all previous builds of the same build plan have + // finished. + $plan = $build->getBuildPlan(); + + $log = null; + $log_start = null; + $blockers = $this->getBlockers($object, $plan, $build); + while (count($blockers) > 0) { + if ($log === null) { + $log = $build->createLog($build_target, "waiting", "blockers"); + $log_start = $log->start(); + } + + $log->append("Blocked by: ".implode(",", $blockers)."\n"); + + // TODO: This should fail temporarily instead after setting the target to + // waiting, and thereby push the build into a waiting status. + sleep(1); + $blockers = $this->getBlockers($object, $plan, $build); + } + if ($log !== null) { + $log->finalize($log_start); + } + } + + private function getBlockers( + PhabricatorRepositoryCommit $commit, + HarbormasterBuildPlan $plan, + HarbormasterBuild $source) { + + $call = new ConduitCall( + 'diffusion.commitparentsquery', + array( + 'commit' => $commit->getCommitIdentifier(), + 'callsign' => $commit->getRepository()->getCallsign() + )); + $call->setUser(PhabricatorUser::getOmnipotentUser()); + $parents = $call->execute(); + + $parents = id(new DiffusionCommitQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withRepository($commit->getRepository()) + ->withIdentifiers($parents) + ->execute(); + + $blockers = array(); + + $build_objects = array(); + foreach ($parents as $parent) { + if (!$parent->isImported()) { + $blockers[] = pht('Commit %s', $parent->getCommitIdentifier()); + } else { + $build_objects[] = $parent->getPHID(); + } + } + + $buildables = id(new HarbormasterBuildableQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withBuildablePHIDs($build_objects) + ->withManualBuildables(false) + ->execute(); + $buildable_phids = mpull($buildables, 'getPHID'); + + $builds = id(new HarbormasterBuildQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withBuildablePHIDs($buildable_phids) + ->withBuildPlanPHIDs(array($plan->getPHID())) + ->execute(); + + foreach ($builds as $build) { + if (!$build->isComplete()) { + $blockers[] = pht('Build %d', $build->getID()); + } + } + + return $blockers; + } +} diff --git a/src/applications/harbormaster/storage/HarbormasterBuildCommand.php b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php new file mode 100644 index 0000000000..8b703cad4d --- /dev/null +++ b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php @@ -0,0 +1,13 @@ +setIsManualBuildable(0) ->setBuildStatus(self::STATUS_WHATEVER) ->setBuildableStatus(self::STATUS_WHATEVER); } + public function getMonogram() { + return 'B'.$this->getID(); + } + + /** + * Returns an existing buildable for the object's PHID or creates a + * new buildable implicitly if needed. + */ + public static function createOrLoadExisting( + PhabricatorUser $actor, + $buildable_object_phid, + $container_object_phid) { + + $buildable = id(new HarbormasterBuildableQuery()) + ->setViewer($actor) + ->withBuildablePHIDs(array($buildable_object_phid)) + ->withManualBuildables(false) + ->setLimit(1) + ->executeOne(); + if ($buildable) { + return $buildable; + } + $buildable = HarbormasterBuildable::initializeNewBuildable($actor) + ->setBuildablePHID($buildable_object_phid) + ->setContainerPHID($container_object_phid); + $buildable->save(); + return $buildable; + } + + /** + * Looks up the plan PHIDs and applies the plans to the specified + * object identified by it's PHID. + */ + public static function applyBuildPlans( + $phid, + $container_phid, + array $plan_phids) { + + if (count($plan_phids) === 0) { + return; + } + + // Skip all of this logic if the Harbormaster application + // isn't currently installed. + + $harbormaster_app = 'PhabricatorApplicationHarbormaster'; + if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { + return; + } + + $buildable = HarbormasterBuildable::createOrLoadExisting( + PhabricatorUser::getOmnipotentUser(), + $phid, + $container_phid); + + $plans = id(new HarbormasterBuildPlanQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($plan_phids) + ->execute(); + foreach ($plans as $plan) { + if ($plan->isDisabled()) { + // TODO: This should be communicated more clearly -- maybe we should + // create the build but set the status to "disabled" or "derelict". + continue; + } + + $buildable->applyPlan($plan); + } + } + + public function applyPlan(HarbormasterBuildPlan $plan) { + $viewer = PhabricatorUser::getOmnipotentUser(); + $build = HarbormasterBuild::initializeNewBuild($viewer) + ->setBuildablePHID($this->getPHID()) + ->setBuildPlanPHID($plan->getPHID()) + ->setBuildStatus(HarbormasterBuild::STATUS_PENDING) + ->save(); + + PhabricatorWorker::scheduleTask( + 'HarbormasterBuildWorker', + array( + 'buildID' => $build->getID() + )); + + return $this; + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -50,6 +142,15 @@ final class HarbormasterBuildable extends HarbormasterDAO return $this->assertAttached($this->containerObject); } + public function attachContainerHandle($container_handle) { + $this->containerHandle = $container_handle; + return $this; + } + + public function getContainerHandle() { + return $this->assertAttached($this->containerHandle); + } + public function attachBuildableHandle($buildable_handle) { $this->buildableHandle = $buildable_handle; return $this; @@ -76,6 +177,7 @@ final class HarbormasterBuildable extends HarbormasterDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -90,9 +192,24 @@ final class HarbormasterBuildable extends HarbormasterDAO } public function describeAutomaticCapability($capability) { - return pht( - 'Users must be able to see the revision or repository to see a '. - 'buildable.'); + return pht('A buildable inherits policies from the underlying object.'); } + + +/* -( HarbormasterBuildableInterface )------------------------------------- */ + + + public function getHarbormasterBuildablePHID() { + // NOTE: This is essentially just for convenience, as it allows you create + // a copy of a buildable by specifying `B123` without bothering to go + // look up the underlying object. + return $this->getBuildablePHID(); + } + + public function getHarbormasterContainerPHID() { + return $this->getContainerPHID(); + } + + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index aa8e6c4f53..569ddef191 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -9,6 +9,7 @@ final class HarbormasterBuild extends HarbormasterDAO private $buildable = self::ATTACHABLE; private $buildPlan = self::ATTACHABLE; + private $unprocessedCommands = self::ATTACHABLE; /** * Not currently being built. @@ -45,11 +46,25 @@ final class HarbormasterBuild extends HarbormasterDAO */ const STATUS_ERROR = 'error'; + /** + * The build has been stopped. + */ + const STATUS_STOPPED = 'stopped'; + public static function initializeNewBuild(PhabricatorUser $actor) { return id(new HarbormasterBuild()) ->setBuildStatus(self::STATUS_INACTIVE); } + public function delete() { + $this->openTransaction(); + $this->deleteUnprocessedCommands(); + $result = parent::delete(); + $this->saveTransaction(); + + return $result; + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -58,7 +73,7 @@ final class HarbormasterBuild extends HarbormasterDAO public function generatePHID() { return PhabricatorPHID::generateNewPHID( - HarbormasterPHIDTypeBuildPlan::TYPECONST); + HarbormasterPHIDTypeBuild::TYPECONST); } public function attachBuildable(HarbormasterBuildable $buildable) { @@ -87,6 +102,208 @@ final class HarbormasterBuild extends HarbormasterDAO return $this->assertAttached($this->buildPlan); } + public function isBuilding() { + return $this->getBuildStatus() === self::STATUS_PENDING || + $this->getBuildStatus() === self::STATUS_WAITING || + $this->getBuildStatus() === self::STATUS_BUILDING; + } + + public function createLog( + HarbormasterBuildTarget $build_target, + $log_source, + $log_type) { + + $log = HarbormasterBuildLog::initializeNewBuildLog($build_target) + ->setLogSource($log_source) + ->setLogType($log_type) + ->save(); + + return $log; + } + + public function createArtifact( + HarbormasterBuildTarget $build_target, + $artifact_key, + $artifact_type) { + + $artifact = + HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target); + $artifact->setArtifactKey($this->getPHID(), $artifact_key); + $artifact->setArtifactType($artifact_type); + $artifact->save(); + return $artifact; + } + + public function loadArtifact($name) { + $artifact = id(new HarbormasterBuildArtifactQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withArtifactKeys( + $this->getPHID(), + array($name)) + ->executeOne(); + if ($artifact === null) { + throw new Exception("Artifact not found!"); + } + return $artifact; + } + + public function retrieveVariablesFromBuild() { + $results = array( + 'buildable.diff' => null, + 'buildable.revision' => null, + 'buildable.commit' => null, + 'repository.callsign' => null, + 'repository.vcs' => null, + 'repository.uri' => null, + 'step.timestamp' => null, + 'build.id' => null); + + $buildable = $this->getBuildable(); + $object = $buildable->getBuildableObject(); + + $repo = null; + if ($object instanceof DifferentialDiff) { + $results['buildable.diff'] = $object->getID(); + $revision = $object->getRevision(); + $results['buildable.revision'] = $revision->getID(); + $repo = $revision->getRepository(); + } else if ($object instanceof PhabricatorRepositoryCommit) { + $results['buildable.commit'] = $object->getCommitIdentifier(); + $repo = $object->getRepository(); + } + + if ($repo) { + $results['repository.callsign'] = $repo->getCallsign(); + $results['repository.vcs'] = $repo->getVersionControlSystem(); + $results['repository.uri'] = $repo->getPublicCloneURI(); + } + + $results['step.timestamp'] = time(); + $results['build.id'] = $this->getID(); + + return $results; + } + + public static function getAvailableBuildVariables() { + return array( + 'buildable.diff' => + pht('The differential diff ID, if applicable.'), + 'buildable.revision' => + pht('The differential revision ID, if applicable.'), + 'buildable.commit' => pht('The commit identifier, if applicable.'), + 'repository.callsign' => + pht('The callsign of the repository in Phabricator.'), + 'repository.vcs' => + pht('The version control system, either "svn", "hg" or "git".'), + 'repository.uri' => + pht('The URI to clone or checkout the repository from.'), + 'step.timestamp' => pht('The current UNIX timestamp.'), + 'build.id' => pht('The ID of the current build.')); + } + + public function isComplete() { + switch ($this->getBuildStatus()) { + case self::STATUS_PASSED: + case self::STATUS_FAILED: + case self::STATUS_ERROR: + case self::STATUS_STOPPED: + return true; + } + + return false; + } + + public function isStopped() { + return ($this->getBuildStatus() == self::STATUS_STOPPED); + } + + +/* -( Build Commands )----------------------------------------------------- */ + + + private function getUnprocessedCommands() { + return $this->assertAttached($this->unprocessedCommands); + } + + public function attachUnprocessedCommands(array $commands) { + $this->unprocessedCommands = $commands; + return $this; + } + + public function canRestartBuild() { + return !$this->isRestarting(); + } + + public function canStopBuild() { + return !$this->isComplete() && + !$this->isStopped() && + !$this->isStopping(); + } + + public function canResumeBuild() { + return $this->isStopped() && + !$this->isResuming(); + } + + public function isStopping() { + $is_stopping = false; + foreach ($this->getUnprocessedCommands() as $command_object) { + $command = $command_object->getCommand(); + switch ($command) { + case HarbormasterBuildCommand::COMMAND_STOP: + $is_stopping = true; + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + case HarbormasterBuildCommand::COMMAND_RESTART: + $is_stopping = false; + break; + } + } + + return $is_stopping; + } + + public function isResuming() { + $is_resuming = false; + foreach ($this->getUnprocessedCommands() as $command_object) { + $command = $command_object->getCommand(); + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + case HarbormasterBuildCommand::COMMAND_RESUME: + $is_resuming = true; + break; + case HarbormasterBuildCommand::COMMAND_STOP: + $is_resuming = false; + break; + } + } + + return $is_resuming; + } + + public function isRestarting() { + $is_restarting = false; + foreach ($this->getUnprocessedCommands() as $command_object) { + $command = $command_object->getCommand(); + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + $is_restarting = true; + break; + } + } + + return $is_restarting; + } + + public function deleteUnprocessedCommands() { + foreach ($this->getUnprocessedCommands() as $key => $command_object) { + $command_object->delete(); + unset($this->unprocessedCommands[$key]); + } + + return $this; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -94,6 +311,7 @@ final class HarbormasterBuild extends HarbormasterDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -108,8 +326,7 @@ final class HarbormasterBuild extends HarbormasterDAO } public function describeAutomaticCapability($capability) { - return pht( - 'Users must be able to see a buildable to view its build plans.'); + return pht('A build inherits policies from its buildable.'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php index f262d3810c..4854e64b5d 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -3,12 +3,23 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO implements PhabricatorPolicyInterface { - protected $buildablePHID; + protected $buildTargetPHID; protected $artifactType; protected $artifactIndex; protected $artifactKey; protected $artifactData = array(); + private $buildTarget = self::ATTACHABLE; + + const TYPE_FILE = 'file'; + const TYPE_HOST = 'host'; + + public static function initializeNewBuildArtifact( + HarbormasterBuildTarget $build_target) { + return id(new HarbormasterBuildArtifact()) + ->setBuildTargetPHID($build_target->getPHID()); + } + public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( @@ -17,21 +28,97 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO ) + parent::getConfiguration(); } - public function attachBuildable(HarbormasterBuildable $buildable) { - $this->buildable = $buildable; + public function attachBuildTarget(HarbormasterBuildTarget $build_target) { + $this->buildTarget = $build_target; return $this; } - public function getBuildable() { - return $this->assertAttached($this->buildable); + public function getBuildTarget() { + return $this->assertAttached($this->buildTarget); } - public function setArtifactKey($key) { - $this->artifactIndex = PhabricatorHash::digestForIndex($key); + public function setArtifactKey($build_phid, $key) { + $this->artifactIndex = + PhabricatorHash::digestForIndex($build_phid.$key); $this->artifactKey = $key; return $this; } + public function getObjectItemView(PhabricatorUser $viewer) { + $data = $this->getArtifactData(); + switch ($this->getArtifactType()) { + case self::TYPE_FILE: + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($data) + ->executeOne(); + + return id(new PHUIObjectItemView()) + ->setObjectName(pht('File')) + ->setHeader($handle->getFullName()) + ->setHref($handle->getURI()); + case self::TYPE_HOST: + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs(array($data["drydock-lease"])) + ->execute(); + $lease = $leases[$data["drydock-lease"]]; + + return id(new PHUIObjectItemView()) + ->setObjectName(pht('Drydock Lease')) + ->setHeader($lease->getID()) + ->setHref('/drydock/lease/'.$lease->getID()); + default: + return null; + } + } + + public function loadDrydockLease() { + if ($this->getArtifactType() !== self::TYPE_HOST) { + throw new Exception( + "`loadDrydockLease` may only be called on host artifacts."); + } + + $data = $this->getArtifactData(); + + // FIXME: Is there a better way of doing this? + // TODO: Policy stuff, etc. + $lease = id(new DrydockLease())->load( + $data['drydock-lease']); + if ($lease === null) { + throw new Exception("Associated Drydock lease not found!"); + } + $resource = id(new DrydockResource())->load( + $lease->getResourceID()); + if ($resource === null) { + throw new Exception("Associated Drydock resource not found!"); + } + $lease->attachResource($resource); + + return $lease; + } + + public function loadPhabricatorFile() { + if ($this->getArtifactType() !== self::TYPE_FILE) { + throw new Exception( + "`loadPhabricatorFile` may only be called on file artifacts."); + } + + $data = $this->getArtifactData(); + + // The data for TYPE_FILE is an array with a single PHID in it. + $phid = $data["filePHID"]; + + $file = id(new PhabricatorFileQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($phid)) + ->executeOne(); + if ($file === null) { + throw new Exception("Associated file not found!"); + } + return $file; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -43,11 +130,11 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO } public function getPolicy($capability) { - return $this->getBuildable()->getPolicy($capability); + return $this->getBuildTarget()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return $this->getBuildable()->hasAutomaticCapability( + return $this->getBuildTarget()->hasAutomaticCapability( $capability, $viewer); } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php index 2e5d05d7eb..e998e82abb 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php @@ -1,7 +1,193 @@ setBuildTargetPHID($build_target->getPHID()) + ->setDuration(null) + ->setLive(0); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + HarbormasterPHIDTypeBuildLog::TYPECONST); + } + + public function attachBuildTarget(HarbormasterBuildTarget $build_target) { + $this->buildTarget = $build_target; + return $this; + } + + public function getBuildTarget() { + return $this->assertAttached($this->buildTarget); + } + + public function getName() { + return pht('Build Log'); + } + + public function start() { + if ($this->getLive()) { + throw new Exception("Live logging has already started for this log."); + } + + $this->setLive(1); + $this->save(); + + return time(); + } + + public function append($content) { + if (!$this->getLive()) { + throw new Exception("Start logging before appending data to the log."); + } + if (strlen($content) === 0) { + return; + } + + // If the length of the content is greater than the chunk size limit, + // then we can never fit the content in a single record. We need to + // split our content out and call append on it for as many parts as there + // are to the content. + if (strlen($content) > self::CHUNK_BYTE_LIMIT) { + $current = $content; + while (strlen($current) > self::CHUNK_BYTE_LIMIT) { + $part = substr($current, 0, self::CHUNK_BYTE_LIMIT); + $current = substr($current, self::CHUNK_BYTE_LIMIT); + $this->append($part); + } + $this->append($current); + return; + } + + // Retrieve the size of last chunk from the DB for this log. If the + // chunk is over 500K, then we need to create a new log entry. + $conn = $this->establishConnection('w'); + $result = queryfx_all( + $conn, + 'SELECT id, size, encoding '. + 'FROM harbormaster_buildlogchunk '. + 'WHERE logID = %d '. + 'ORDER BY id DESC '. + 'LIMIT 1', + $this->getID()); + if (count($result) === 0 || + $result[0]["size"] + strlen($content) > self::CHUNK_BYTE_LIMIT || + $result[0]["encoding"] !== self::ENCODING_TEXT) { + + // We must insert a new chunk because the data we are appending + // won't fit into the existing one, or we don't have any existing + // chunk data. + queryfx( + $conn, + 'INSERT INTO harbormaster_buildlogchunk '. + '(logID, encoding, size, chunk) '. + 'VALUES '. + '(%d, %s, %d, %s)', + $this->getID(), + self::ENCODING_TEXT, + strlen($content), + $content); + } else { + // We have a resulting record that we can append our content onto. + queryfx( + $conn, + 'UPDATE harbormaster_buildlogchunk '. + 'SET chunk = CONCAT(chunk, %s), size = LENGTH(CONCAT(chunk, %s))'. + 'WHERE id = %d', + $content, + $content, + $result[0]["id"]); + } + } + + public function finalize($start = 0) { + if (!$this->getLive()) { + throw new Exception("Start logging before finalizing it."); + } + + // TODO: Encode the log contents in a gzipped format. + $this->reload(); + if ($start > 0) { + $this->setDuration(time() - $start); + } + $this->setLive(0); + $this->save(); + } + + public function getLogText() { + // TODO: This won't cope very well if we're pulling like a 700MB + // log file out of the DB. We should probably implement some sort + // of optional limit parameter so that when we're rendering out only + // 25 lines in the UI, we don't wastefully read in the whole log. + + // We have to read our content out of the database and stitch all of + // the log data back together. + $conn = $this->establishConnection('r'); + $result = queryfx_all( + $conn, + 'SELECT chunk '. + 'FROM harbormaster_buildlogchunk '. + 'WHERE logID = %d '. + 'ORDER BY id ASC', + $this->getID()); + + $content = ""; + foreach ($result as $row) { + $content .= $row["chunk"]; + } + return $content; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getBuildTarget()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getBuildTarget()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'Users must be able to see a build target to view it\'s build log.'); + } - protected $buildItemPHID; } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index 1eec81d2ea..0e228733d0 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -1,10 +1,42 @@ setBuildPHID($build->getPHID()) + ->setBuildStepPHID($build_step->getPHID()) + ->setClassName($build_step->getClassName()) + ->setDetails($build_step->getDetails()) + ->setTargetStatus(self::STATUS_PENDING) + ->setVariables($variables); + } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'details' => self::SERIALIZATION_JSON, + 'variables' => self::SERIALIZATION_JSON, + ) ) + parent::getConfiguration(); } @@ -13,4 +45,109 @@ final class HarbormasterBuildTarget extends HarbormasterDAO { HarbormasterPHIDTypeBuildTarget::TYPECONST); } + public function attachBuild(HarbormasterBuild $build) { + $this->build = $build; + return $this; + } + + public function getBuild() { + return $this->assertAttached($this->build); + } + + public function attachBuildStep(HarbormasterBuildStep $step) { + $this->buildStep = $step; + return $this; + } + + public function getBuildStep() { + return $this->assertAttached($this->buildStep); + } + + public function getDetail($key, $default = null) { + return idx($this->details, $key, $default); + } + + public function setDetail($key, $value) { + $this->details[$key] = $value; + return $this; + } + + public function getVariable($key, $default = null) { + return idx($this->variables, $key, $default); + } + + public function setVariable($key, $value) { + $this->variables[$key] = $value; + return $this; + } + + public function getImplementation() { + if ($this->className === null) { + throw new Exception("No implementation set for the given target."); + } + + static $implementations = null; + if ($implementations === null) { + $implementations = BuildStepImplementation::getImplementations(); + } + + $class = $this->className; + if (!in_array($class, $implementations)) { + throw new Exception( + "Class name '".$class."' does not extend BuildStepImplementation."); + } + $implementation = newv($class, array()); + $implementation->loadSettings($this); + return $implementation; + } + + +/* -( Status )------------------------------------------------------------- */ + + + public function isComplete() { + switch ($this->getTargetStatus()) { + case self::STATUS_PASSED: + case self::STATUS_FAILED: + return true; + } + + return false; + } + + + public function isFailed() { + switch ($this->getTargetStatus()) { + case self::STATUS_FAILED: + return true; + } + + return false; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getBuild()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getBuild()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'Users must be able to see a build to view its build targets.'); + } + } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 8f36057636..e3d505ce57 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -8,11 +8,14 @@ final class HarbormasterBuildPlan extends HarbormasterDAO protected $name; protected $planStatus; + const STATUS_ACTIVE = 'active'; + const STATUS_DISABLED = 'disabled'; + private $buildSteps = self::ATTACHABLE; public static function initializeNewBuildPlan(PhabricatorUser $actor) { return id(new HarbormasterBuildPlan()) - ->setPlanStatus('active'); // TODO: Figure this out. + ->setPlanStatus(self::STATUS_ACTIVE); } public function getConfiguration() { @@ -36,6 +39,23 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return $this->assertAttached($this->buildSteps); } + /** + * Returns a standard, ordered list of build steps for this build plan. + * + * This method should be used to load build steps for a given build plan + * so that the ordering is consistent. + */ + public function loadOrderedBuildSteps() { + return id(new HarbormasterBuildStepQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withBuildPlanPHIDs(array($this->getPHID())) + ->execute(); + } + + public function isDisabled() { + return ($this->getPlanStatus() == self::STATUS_DISABLED); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php index a48ce27073..bf03518d5f 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php @@ -4,6 +4,7 @@ final class HarbormasterBuildPlanTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'harbormaster:name'; + const TYPE_STATUS = 'harbormaster:status'; public function getApplicationName() { return 'harbormaster'; @@ -65,7 +66,16 @@ final class HarbormasterBuildPlanTransaction $old, $new); } - break; + case self::TYPE_STATUS: + if ($new == HarbormasterBuildPlan::STATUS_DISABLED) { + return pht( + '%s disabled this build plan.', + $author_handle); + } else { + return pht( + '%s enabled this build plan.', + $author_handle); + } } return parent::getTitle(); diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index cb9310d2aa..7863518306 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -6,6 +6,7 @@ final class HarbormasterBuildStep extends HarbormasterDAO protected $buildPlanPHID; protected $className; protected $details = array(); + protected $sequence; private $buildPlan = self::ATTACHABLE; diff --git a/src/applications/harbormaster/view/ShellLogView.php b/src/applications/harbormaster/view/ShellLogView.php new file mode 100644 index 0000000000..feaa8ffc07 --- /dev/null +++ b/src/applications/harbormaster/view/ShellLogView.php @@ -0,0 +1,109 @@ +start = $start; + return $this; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function setLines(array $lines) { + $this->lines = $lines; + return $this; + } + + public function setHighlights(array $highlights) { + $this->highlights = array_fuse($highlights); + return $this; + } + + public function render() { + require_celerity_resource('phabricator-source-code-view-css'); + require_celerity_resource('syntax-highlighting-css'); + + Javelin::initBehavior('phabricator-oncopy', array()); + + $line_number = $this->start; + + $rows = array(); + foreach ($this->lines as $line) { + $hit_limit = $this->limit && + ($line_number == $this->limit) && + (count($this->lines) != $this->limit); + + if ($hit_limit) { + $content_number = ''; + $content_line = phutil_tag( + 'span', + array( + 'class' => 'c', + ), + pht('...')); + } else { + $content_number = $line_number; + $content_line = $line; + } + + $row_attributes = array(); + if (isset($this->highlights[$line_number])) { + $row_attributes['class'] = 'phabricator-source-highlight'; + } + + // TODO: Provide nice links. + + $th = phutil_tag( + 'th', + array( + 'class' => 'phabricator-source-line', + 'style' => 'background-color: #fff;', + ), + $content_number); + + $td = phutil_tag( + 'td', + array('class' => 'phabricator-source-code'), + $content_line); + + $rows[] = phutil_tag( + 'tr', + $row_attributes, + array($th, $td)); + + if ($hit_limit) { + break; + } + + $line_number++; + } + + $classes = array(); + $classes[] = 'phabricator-source-code-view'; + $classes[] = 'remarkup-code'; + $classes[] = 'PhabricatorMonospaced'; + + return phutil_tag( + 'div', + array( + 'class' => 'phabricator-source-code-container', + 'style' => 'background-color: black; color: white;' + ), + phutil_tag( + 'table', + array( + 'class' => implode(' ', $classes), + 'style' => 'background-color: black' + ), + phutil_implode_html('', $rows))); + } + +} diff --git a/src/applications/harbormaster/worker/HarbormasterBuildWorker.php b/src/applications/harbormaster/worker/HarbormasterBuildWorker.php index 0306c18d8f..31f37b2f2b 100644 --- a/src/applications/harbormaster/worker/HarbormasterBuildWorker.php +++ b/src/applications/harbormaster/worker/HarbormasterBuildWorker.php @@ -1,69 +1,28 @@ getTaskData(); $id = idx($data, 'buildID'); + $viewer = $this->getViewer(); - // Get a reference to the build. $build = id(new HarbormasterBuildQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withBuildStatuses(array(HarbormasterBuild::STATUS_PENDING)) + ->setViewer($viewer) ->withIDs(array($id)) - ->needBuildPlans(true) ->executeOne(); if (!$build) { throw new PhabricatorWorkerPermanentFailureException( pht('Invalid build ID "%s".', $id)); } - try { - $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); - $build->save(); - - $buildable = $build->getBuildable(); - $plan = $build->getBuildPlan(); - - $steps = id(new HarbormasterBuildStepQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withBuildPlanPHIDs(array($plan->getPHID())) - ->execute(); - - // Perform the build. - foreach ($steps as $step) { - $implementation = $step->getStepImplementation(); - if (!$implementation->validateSettings()) { - $build->setBuildStatus(HarbormasterBuild::STATUS_ERROR); - break; - } - $implementation->execute($build); - if ($build->getBuildStatus() !== HarbormasterBuild::STATUS_BUILDING) { - break; - } - } - - // If we get to here, then the build has finished. Set it to passed - // if no build step explicitly set the status. - if ($build->getBuildStatus() === HarbormasterBuild::STATUS_BUILDING) { - $build->setBuildStatus(HarbormasterBuild::STATUS_PASSED); - } - $build->save(); - } catch (Exception $e) { - // If any exception is raised, the build is marked as a failure and - // the exception is re-thrown (this ensures we don't leave builds - // in an inconsistent state). - $build->setBuildStatus(HarbormasterBuild::STATUS_ERROR); - $build->save(); - throw $e; - } + id(new HarbormasterBuildEngine()) + ->setViewer($viewer) + ->setBuild($build) + ->continueBuild(); } } diff --git a/src/applications/harbormaster/worker/HarbormasterTargetWorker.php b/src/applications/harbormaster/worker/HarbormasterTargetWorker.php new file mode 100644 index 0000000000..33f23afe62 --- /dev/null +++ b/src/applications/harbormaster/worker/HarbormasterTargetWorker.php @@ -0,0 +1,70 @@ +getTaskData(); + $id = idx($data, 'targetID'); + + $target = id(new HarbormasterBuildTargetQuery()) + ->withIDs(array($id)) + ->setViewer($this->getViewer()) + ->executeOne(); + + if (!$target) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Bad build target ID "%d".', + $id)); + } + + return $target; + } + + public function doWork() { + $target = $this->loadBuildTarget(); + $build = $target->getBuild(); + $viewer = $this->getViewer(); + + try { + $implementation = $target->getImplementation(); + if (!$implementation->validateSettings()) { + $target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED); + $target->save(); + } else { + $implementation->execute($build, $target); + $target->setTargetStatus(HarbormasterBuildTarget::STATUS_PASSED); + $target->save(); + } + } catch (Exception $ex) { + phlog($ex); + + try { + $log = $build->createLog($target, 'core', 'exception'); + $start = $log->start(); + $log->append((string)$ex); + $log->finalize($start); + } catch (Exception $log_ex) { + phlog($log_ex); + } + + $target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED); + $target->save(); + } + + id(new HarbormasterBuildEngine()) + ->setViewer($viewer) + ->setBuild($build) + ->continueBuild(); + } + +} diff --git a/src/applications/harbormaster/worker/HarbormasterWorker.php b/src/applications/harbormaster/worker/HarbormasterWorker.php new file mode 100644 index 0000000000..00cc8812a2 --- /dev/null +++ b/src/applications/harbormaster/worker/HarbormasterWorker.php @@ -0,0 +1,9 @@ +getPHID()); + } + + public function getAdapterSortKey() { + return sprintf( + '%08d%s', + $this->getAdapterSortOrder(), + $this->getAdapterContentName()); + } + + public function getAdapterSortOrder() { + return 1000; + } + /* -( Fields )------------------------------------------------------------- */ @@ -135,6 +184,7 @@ abstract class HeraldAdapter { self::FIELD_TITLE => pht('Title'), self::FIELD_BODY => pht('Body'), self::FIELD_AUTHOR => pht('Author'), + self::FIELD_ASSIGNEE => pht('Assignee'), self::FIELD_COMMITTER => pht('Committer'), self::FIELD_REVIEWER => pht('Reviewer'), self::FIELD_REVIEWERS => pht('Reviewers'), @@ -144,7 +194,9 @@ abstract class HeraldAdapter { self::FIELD_DIFF_CONTENT => pht('Any changed file content'), self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'), self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'), + self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'), self::FIELD_REPOSITORY => pht('Repository'), + self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'), self::FIELD_RULE => pht('Another Herald rule'), self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'), self::FIELD_AFFECTED_PACKAGE_OWNER => @@ -152,6 +204,18 @@ abstract class HeraldAdapter { self::FIELD_CONTENT_SOURCE => pht('Content Source'), self::FIELD_ALWAYS => pht('Always'), self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"), + self::FIELD_PROJECTS => pht("Projects"), + self::FIELD_PUSHER => pht('Pusher'), + self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"), + self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), + self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), + self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), + self::FIELD_DIFFERENTIAL_ACCEPTED + => pht('Accepted Differential revision'), + self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'), + self::FIELD_BRANCHES => pht('Commit\'s branches'), + self::FIELD_AUTHOR_RAW => pht('Raw author name'), + self::FIELD_COMMITTER_RAW => pht('Raw committer name'), ); } @@ -166,6 +230,8 @@ abstract class HeraldAdapter { self::CONDITION_IS => pht('is'), self::CONDITION_IS_NOT => pht('is not'), self::CONDITION_IS_ANY => pht('is any of'), + self::CONDITION_IS_TRUE => pht('is true'), + self::CONDITION_IS_FALSE => pht('is false'), self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), @@ -179,6 +245,8 @@ abstract class HeraldAdapter { self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_UNCONDITIONALLY => '', // don't show anything! self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), + self::CONDITION_HAS_BIT => pht('has bit'), + self::CONDITION_NOT_BIT => pht('lacks bit'), ); } @@ -186,6 +254,8 @@ abstract class HeraldAdapter { switch ($field) { case self::FIELD_TITLE: case self::FIELD_BODY: + case self::FIELD_COMMITTER_RAW: + case self::FIELD_AUTHOR_RAW: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, @@ -195,22 +265,38 @@ abstract class HeraldAdapter { ); case self::FIELD_AUTHOR: case self::FIELD_COMMITTER: - case self::FIELD_REPOSITORY: case self::FIELD_REVIEWER: + case self::FIELD_PUSHER: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, ); + case self::FIELD_REPOSITORY: + case self::FIELD_ASSIGNEE: + return array( + self::CONDITION_IS_ANY, + self::CONDITION_IS_NOT_ANY, + self::CONDITION_EXISTS, + self::CONDITION_NOT_EXISTS, + ); case self::FIELD_TAGS: case self::FIELD_REVIEWERS: case self::FIELD_CC: case self::FIELD_AUTHOR_PROJECTS: + case self::FIELD_PROJECTS: + case self::FIELD_AFFECTED_PACKAGE: + case self::FIELD_AFFECTED_PACKAGE_OWNER: + case self::FIELD_PUSHER_PROJECTS: + case self::FIELD_REPOSITORY_PROJECTS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, + self::CONDITION_EXISTS, + self::CONDITION_NOT_EXISTS, ); case self::FIELD_DIFF_FILE: + case self::FIELD_BRANCHES: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, @@ -228,12 +314,6 @@ abstract class HeraldAdapter { self::CONDITION_RULE, self::CONDITION_NOT_RULE, ); - case self::FIELD_AFFECTED_PACKAGE: - case self::FIELD_AFFECTED_PACKAGE_OWNER: - return array( - self::CONDITION_INCLUDE_ANY, - self::CONDITION_INCLUDE_NONE, - ); case self::FIELD_CONTENT_SOURCE: return array( self::CONDITION_IS, @@ -243,6 +323,32 @@ abstract class HeraldAdapter { return array( self::CONDITION_UNCONDITIONALLY, ); + case self::FIELD_DIFFERENTIAL_REVIEWERS: + return array( + self::CONDITION_EXISTS, + self::CONDITION_NOT_EXISTS, + self::CONDITION_INCLUDE_ALL, + self::CONDITION_INCLUDE_ANY, + self::CONDITION_INCLUDE_NONE, + ); + case self::FIELD_DIFFERENTIAL_CCS: + return array( + self::CONDITION_INCLUDE_ALL, + self::CONDITION_INCLUDE_ANY, + self::CONDITION_INCLUDE_NONE, + ); + case self::FIELD_DIFFERENTIAL_REVISION: + case self::FIELD_DIFFERENTIAL_ACCEPTED: + return array( + self::CONDITION_EXISTS, + self::CONDITION_NOT_EXISTS, + ); + case self::FIELD_IS_MERGE_COMMIT: + case self::FIELD_DIFF_ENORMOUS: + return array( + self::CONDITION_IS_TRUE, + self::CONDITION_IS_FALSE, + ); default: throw new Exception( "This adapter does not define conditions for field '{$field}'!"); @@ -313,8 +419,10 @@ abstract class HeraldAdapter { array_fuse($field_value), $condition_value); case self::CONDITION_EXISTS: + case self::CONDITION_IS_TRUE: return (bool)$field_value; case self::CONDITION_NOT_EXISTS: + case self::CONDITION_IS_FALSE: return !$field_value; case self::CONDITION_UNCONDITIONALLY: return (bool)$field_value; @@ -383,6 +491,10 @@ abstract class HeraldAdapter { $result = !$result; } return $result; + case self::CONDITION_HAS_BIT: + return (($condition_value & $field_value) === $condition_value); + case self::CONDITION_NOT_BIT: + return (($condition_value & $field_value) !== $condition_value); default: throw new HeraldInvalidConditionException( "Unknown condition '{$condition_type}'."); @@ -460,6 +572,10 @@ abstract class HeraldAdapter { case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: + case self::CONDITION_HAS_BIT: + case self::CONDITION_NOT_BIT: + case self::CONDITION_IS_TRUE: + case self::CONDITION_IS_FALSE: // No explicit validation for these types, although there probably // should be in some cases. break; @@ -479,6 +595,7 @@ abstract class HeraldAdapter { public function getActionNameMap($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add emails to CC'), @@ -490,6 +607,8 @@ abstract class HeraldAdapter { self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_ADD_REVIEWERS => pht('Add reviewers'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'), + self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'), + self::ACTION_BLOCK => pht('Block change with message'), ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( @@ -541,6 +660,7 @@ abstract class HeraldAdapter { $target = PhabricatorFlagColor::COLOR_BLUE; } break; + case self::ACTION_BLOCK: case self::ACTION_NOTHING: break; default: @@ -597,7 +717,12 @@ abstract class HeraldAdapter { case self::FIELD_AFFECTED_PACKAGE: return self::VALUE_OWNERS_PACKAGE; case self::FIELD_AUTHOR_PROJECTS: + case self::FIELD_PUSHER_PROJECTS: + case self::FIELD_PROJECTS: + case self::FIELD_REPOSITORY_PROJECTS: return self::VALUE_PROJECT; + case self::FIELD_REVIEWERS: + return self::VALUE_USER_OR_PROJECT; default: return self::VALUE_USER; } @@ -607,6 +732,8 @@ abstract class HeraldAdapter { case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: + case self::CONDITION_IS_TRUE: + case self::CONDITION_IS_FALSE: return self::VALUE_NONE; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: @@ -645,16 +772,20 @@ abstract class HeraldAdapter { return self::VALUE_EMAIL; case self::ACTION_NOTHING: return self::VALUE_NONE; - case self::ACTION_AUDIT: case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ASSIGN_TASK: return self::VALUE_USER; + case self::ACTION_AUDIT: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_USER_OR_PROJECT; + case self::ACTION_APPLY_BUILD_PLANS: + return self::VALUE_BUILD_PLAN; + case self::ACTION_BLOCK: + return self::VALUE_TEXT; default: throw new Exception("Unknown or invalid action '{$action}'."); } @@ -719,6 +850,7 @@ abstract class HeraldAdapter { $adapters = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); + $adapters = msort($adapters, 'getAdapterSortKey'); } return $adapters; } @@ -751,7 +883,6 @@ abstract class HeraldAdapter { $map[$type] = $name; } - asort($map); return $map; } @@ -891,6 +1022,11 @@ abstract class HeraldAdapter { } } } + + if ($rule->isObjectRule()) { + $phids[] = $rule->getTriggerObjectPHID(); + } + return $phids; } diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index 5b56446ccf..cd0e869217 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -6,9 +6,6 @@ final class HeraldCommitAdapter extends HeraldAdapter { const FIELD_NEED_AUDIT_FOR_PACKAGE = 'need-audit-for-package'; - const FIELD_DIFFERENTIAL_REVISION = 'differential-revision'; - const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers'; - const FIELD_DIFFERENTIAL_CCS = 'differential-ccs'; const FIELD_REPOSITORY_AUTOCLOSE_BRANCH = 'repository-autoclose-branch'; protected $diff; @@ -22,6 +19,7 @@ final class HeraldCommitAdapter extends HeraldAdapter { protected $emailPHIDs = array(); protected $addCCPHIDs = array(); protected $auditMap = array(); + protected $buildPlans = array(); protected $affectedPaths; protected $affectedRevision; @@ -44,14 +42,54 @@ final class HeraldCommitAdapter extends HeraldAdapter { return pht('Commits'); } + public function getAdapterContentDescription() { + return pht( + "React to new commits appearing in tracked repositories.\n". + "Commit rules can send email, flag commits, trigger audits, ". + "and run build plans."); + } + + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + return true; + default: + return false; + } + } + + public function canTriggerOnObject($object) { + if ($object instanceof PhabricatorRepository) { + return true; + } + if ($object instanceof PhabricatorProject) { + return true; + } + return false; + } + + public function getTriggerObjectPHIDs() { + return array_merge( + array( + $this->repository->getPHID(), + $this->getPHID(), + ), + $this->repository->getProjectPHIDs()); + } + + public function explainValidTriggerObjects() { + return pht( + 'This rule can trigger for **repositories** and **projects**.'); + } + public function getFieldNameMap() { return array( self::FIELD_NEED_AUDIT_FOR_PACKAGE => pht('Affected packages that need audit'), - self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), - self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), - self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), - self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH => pht('On autoclose branch'), + self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH + => pht('Commit is on closing branch'), ) + parent::getFieldNameMap(); } @@ -63,17 +101,21 @@ final class HeraldCommitAdapter extends HeraldAdapter { self::FIELD_COMMITTER, self::FIELD_REVIEWER, self::FIELD_REPOSITORY, + self::FIELD_REPOSITORY_PROJECTS, self::FIELD_DIFF_FILE, self::FIELD_DIFF_CONTENT, self::FIELD_DIFF_ADDED_CONTENT, self::FIELD_DIFF_REMOVED_CONTENT, + self::FIELD_DIFF_ENORMOUS, self::FIELD_RULE, self::FIELD_AFFECTED_PACKAGE, self::FIELD_AFFECTED_PACKAGE_OWNER, self::FIELD_NEED_AUDIT_FOR_PACKAGE, self::FIELD_DIFFERENTIAL_REVISION, + self::FIELD_DIFFERENTIAL_ACCEPTED, self::FIELD_DIFFERENTIAL_REVIEWERS, self::FIELD_DIFFERENTIAL_CCS, + self::FIELD_BRANCHES, self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH, ), parent::getFields()); @@ -81,25 +123,6 @@ final class HeraldCommitAdapter extends HeraldAdapter { public function getConditionsForField($field) { switch ($field) { - case self::FIELD_DIFFERENTIAL_REVIEWERS: - return array( - self::CONDITION_EXISTS, - self::CONDITION_NOT_EXISTS, - self::CONDITION_INCLUDE_ALL, - self::CONDITION_INCLUDE_ANY, - self::CONDITION_INCLUDE_NONE, - ); - case self::FIELD_DIFFERENTIAL_CCS: - return array( - self::CONDITION_INCLUDE_ALL, - self::CONDITION_INCLUDE_ANY, - self::CONDITION_INCLUDE_NONE, - ); - case self::FIELD_DIFFERENTIAL_REVISION: - return array( - self::CONDITION_EXISTS, - self::CONDITION_NOT_EXISTS, - ); case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return array( self::CONDITION_INCLUDE_ANY, @@ -116,11 +139,13 @@ final class HeraldCommitAdapter extends HeraldAdapter { public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_AUDIT, - self::ACTION_NOTHING, + self::ACTION_APPLY_BUILD_PLANS, + self::ACTION_NOTHING ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( @@ -160,6 +185,35 @@ final class HeraldCommitAdapter extends HeraldAdapter { return $object; } + public function setCommit(PhabricatorRepositoryCommit $commit) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withIDs(array($commit->getRepositoryID())) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$repository) { + throw new Exception(pht('Unable to load repository!')); + } + + $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( + 'commitID = %d', + $commit->getID()); + if (!$data) { + throw new Exception(pht('Unable to load commit data!')); + } + + $this->commit = clone $commit; + $this->commit->attachRepository($repository); + $this->commit->attachCommitData($data); + + $this->repository = $repository; + $this->commitData = $data; + + return $this; + } + public function getPHID() { return $this->commit->getPHID(); } @@ -176,6 +230,10 @@ final class HeraldCommitAdapter extends HeraldAdapter { return $this->auditMap; } + public function getBuildPlans() { + return $this->buildPlans; + } + public function getHeraldName() { return 'r'. @@ -249,6 +307,14 @@ final class HeraldCommitAdapter extends HeraldAdapter { return $this->affectedRevision; } + public static function getEnormousByteLimit() { + return 1024 * 1024 * 1024; // 1GB + } + + public static function getEnormousTimeLimit() { + return 60 * 15; // 15 Minutes + } + private function loadCommitDiff() { $drequest = DiffusionRequest::newFromDictionary( array( @@ -257,14 +323,26 @@ final class HeraldCommitAdapter extends HeraldAdapter { 'commit' => $this->commit->getCommitIdentifier(), )); + $byte_limit = self::getEnormousByteLimit(); + $raw = DiffusionQuery::callConduitWithDiffusionRequest( PhabricatorUser::getOmnipotentUser(), $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), - 'timeout' => 60 * 60 * 15, - 'linesOfContext' => 0)); + 'timeout' => self::getEnormousTimeLimit(), + 'byteLimit' => $byte_limit, + 'linesOfContext' => 0, + )); + + if (strlen($raw) >= $byte_limit) { + throw new Exception( + pht( + 'The raw text of this change is enormous (larger than %d bytes). '. + 'Herald can not process it.', + $byte_limit)); + } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); @@ -334,12 +412,17 @@ final class HeraldCommitAdapter extends HeraldAdapter { return $this->loadAffectedPaths(); case self::FIELD_REPOSITORY: return $this->repository->getPHID(); + case self::FIELD_REPOSITORY_PROJECTS: + return $this->repository->getProjectPHIDs(); case self::FIELD_DIFF_CONTENT: return $this->getDiffContent('*'); case self::FIELD_DIFF_ADDED_CONTENT: return $this->getDiffContent('+'); case self::FIELD_DIFF_REMOVED_CONTENT: return $this->getDiffContent('-'); + case self::FIELD_DIFF_ENORMOUS: + $this->getDiffContent('*'); + return ($this->commitDiff instanceof Exception); case self::FIELD_AFFECTED_PACKAGE: $packages = $this->loadAffectedPackages(); return mpull($packages, 'getPHID'); @@ -355,6 +438,19 @@ final class HeraldCommitAdapter extends HeraldAdapter { return null; } return $revision->getID(); + case self::FIELD_DIFFERENTIAL_ACCEPTED: + $revision = $this->loadDifferentialRevision(); + if (!$revision) { + return null; + } + // after a revision is accepted, it can be closed (say via arc land) + // so use this function to figure out if it was accepted at one point + // *and* not later rejected... what a function! + $reviewed_by = $revision->loadReviewedBy(); + if (!$reviewed_by) { + return null; + } + return $revision->getPHID(); case self::FIELD_DIFFERENTIAL_REVIEWERS: $revision = $this->loadDifferentialRevision(); if (!$revision) { @@ -367,6 +463,18 @@ final class HeraldCommitAdapter extends HeraldAdapter { return array(); } return $revision->getCCPHIDs(); + case self::FIELD_BRANCHES: + $params = array( + 'callsign' => $this->repository->getCallsign(), + 'contains' => $this->commit->getCommitIdentifier(), + ); + + $result = id(new ConduitCall('diffusion.branchquery', $params)) + ->setUser(PhabricatorUser::getOmnipotentUser()) + ->execute(); + + $refs = DiffusionRepositoryRef::loadAllFromDictionaries($result); + return mpull($refs, 'getShortName'); case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH: return $this->repository->shouldAutocloseCommit( $this->commit, @@ -422,6 +530,15 @@ final class HeraldCommitAdapter extends HeraldAdapter { true, pht('Triggered an audit.')); break; + case self::ACTION_APPLY_BUILD_PLANS: + foreach ($effect->getTarget() as $phid) { + $this->buildPlans[] = $phid; + } + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Applied build plans.')); + break; case self::ACTION_FLAG: $result[] = parent::applyFlagEffect( $effect, diff --git a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php index 2b0c7ee391..6b3fb49fd0 100644 --- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php @@ -14,6 +14,7 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { protected $emailPHIDs = array(); protected $addReviewerPHIDs = array(); protected $blockingReviewerPHIDs = array(); + protected $buildPlans = array(); protected $repository; protected $affectedPackages; @@ -35,6 +36,24 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { return pht('Differential Revisions'); } + public function getAdapterContentDescription() { + return pht( + "React to revisions being created or updated.\n". + "Revision rules can send email, flag revisions, add reviewers, ". + "and run build plans."); + } + + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return true; + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + default: + return false; + } + } + public function getFields() { return array_merge( array( @@ -45,6 +64,7 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { self::FIELD_REVIEWERS, self::FIELD_CC, self::FIELD_REPOSITORY, + self::FIELD_REPOSITORY_PROJECTS, self::FIELD_DIFF_FILE, self::FIELD_DIFF_CONTENT, self::FIELD_DIFF_ADDED_CONTENT, @@ -117,6 +137,10 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { return $this->blockingReviewerPHIDs; } + public function getBuildPlans() { + return $this->buildPlans; + } + public function getPHID() { return $this->revision->getPHID(); } @@ -129,32 +153,36 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { if ($this->repository === null) { $this->repository = false; - // TODO: (T603) Implement policy stuff in Herald. $viewer = PhabricatorUser::getOmnipotentUser(); + $repository_phid = null; $revision = $this->revision; if ($revision->getRepositoryPHID()) { - $repositories = id(new PhabricatorRepositoryQuery()) + $repository_phid = $revision->getRepositoryPHID(); + } else { + $repository = id(new DifferentialRepositoryLookup()) ->setViewer($viewer) - ->withPHIDs(array($revision->getRepositoryPHID())) - ->execute(); - if ($repositories) { - $this->repository = head($repositories); - return $this->repository; + ->setDiff($this->diff) + ->lookupRepository(); + if ($repository) { + // We want to get the projects for this repository too, so run a + // full query for it below. + $repository_phid = $repository->getPHID(); } } - $repository = id(new DifferentialRepositoryLookup()) - ->setViewer($viewer) - ->setDiff($this->diff) - ->lookupRepository(); - if ($repository) { - $this->repository = $repository; - return $this->repository; + if ($repository_phid) { + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withPHIDs(array($repository_phid)) + ->needProjectPHIDs(true) + ->executeOne(); + if ($repository) { + $this->repository = $repository; + } } - - $repository = false; } + return $this->repository; } @@ -322,6 +350,12 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { return null; } return $repository->getPHID(); + case self::FIELD_REPOSITORY_PROJECTS: + $repository = $this->loadRepository(); + if (!$repository) { + return array(); + } + return $repository->getProjectPHIDs(); case self::FIELD_DIFF_CONTENT: return $this->loadContentDictionary(); case self::FIELD_DIFF_ADDED_CONTENT: @@ -349,6 +383,7 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { self::ACTION_EMAIL, self::ACTION_ADD_REVIEWERS, self::ACTION_ADD_BLOCKING_REVIEWERS, + self::ACTION_APPLY_BUILD_PLANS, self::ACTION_NOTHING, ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: @@ -468,6 +503,15 @@ final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { true, pht('Added blocking reviewers.')); break; + case self::ACTION_APPLY_BUILD_PLANS: + foreach ($effect->getTarget() as $phid) { + $this->buildPlans[] = $phid; + } + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Applied build plans.')); + break; default: throw new Exception("No rules to handle action '{$action}'."); } diff --git a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php index b4893b6eea..a4e5f8bfc0 100644 --- a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php +++ b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php @@ -14,6 +14,22 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { return 'PhabricatorApplicationManiphest'; } + public function getAdapterContentDescription() { + return pht( + 'React to tasks being created or updated.'); + } + + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return true; + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + default: + return false; + } + } + public function setTask(ManiphestTask $task) { $this->task = $task; return $this; @@ -60,8 +76,10 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { self::FIELD_TITLE, self::FIELD_BODY, self::FIELD_AUTHOR, + self::FIELD_ASSIGNEE, self::FIELD_CC, self::FIELD_CONTENT_SOURCE, + self::FIELD_PROJECTS, ), parent::getFields()); } @@ -101,8 +119,12 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { return $this->getTask()->getDescription(); case self::FIELD_AUTHOR: return $this->getTask()->getAuthorPHID(); + case self::FIELD_ASSIGNEE: + return $this->getTask()->getOwnerPHID(); case self::FIELD_CC: return $this->getTask()->getCCPHIDs(); + case self::FIELD_PROJECTS: + return $this->getTask()->getProjectPHIDs(); } return parent::getHeraldField($field); diff --git a/src/applications/herald/adapter/HeraldPholioMockAdapter.php b/src/applications/herald/adapter/HeraldPholioMockAdapter.php index d117214de5..5a6404d182 100644 --- a/src/applications/herald/adapter/HeraldPholioMockAdapter.php +++ b/src/applications/herald/adapter/HeraldPholioMockAdapter.php @@ -12,6 +12,11 @@ final class HeraldPholioMockAdapter extends HeraldAdapter { return 'PhabricatorApplicationPholio'; } + public function getAdapterContentDescription() { + return pht( + 'React to mocks being created or updated.'); + } + public function getObject() { return $this->mock; } @@ -36,6 +41,17 @@ final class HeraldPholioMockAdapter extends HeraldAdapter { return pht('Pholio Mocks'); } + public function supportsRuleType($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return true; + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + default: + return false; + } + } + public function getFields() { return array_merge( array( diff --git a/src/applications/herald/application/PhabricatorApplicationHerald.php b/src/applications/herald/application/PhabricatorApplicationHerald.php index 1e0da3354f..4dfec83b6c 100644 --- a/src/applications/herald/application/PhabricatorApplicationHerald.php +++ b/src/applications/herald/application/PhabricatorApplicationHerald.php @@ -30,12 +30,17 @@ final class PhabricatorApplicationHerald extends PhabricatorApplication { return self::GROUP_ORGANIZATION; } + public function getRemarkupRules() { + return array( + new HeraldRemarkupRule(), + ); + } + public function getRoutes() { return array( '/herald/' => array( '(?:query/(?P[^/]+)/)?' => 'HeraldRuleListController', - 'new/(?:(?P[^/]+)/(?:(?P[^/]+)/)?)?' - => 'HeraldNewController', + 'new/' => 'HeraldNewController', 'rule/(?P[1-9]\d*)/' => 'HeraldRuleViewController', 'edit/(?:(?P[1-9]\d*)/)?' => 'HeraldRuleController', 'disable/(?P[1-9]\d*)/(?P\w+)/' => diff --git a/src/applications/herald/config/HeraldRuleTypeConfig.php b/src/applications/herald/config/HeraldRuleTypeConfig.php index 6d9712134a..d55c435f46 100644 --- a/src/applications/herald/config/HeraldRuleTypeConfig.php +++ b/src/applications/herald/config/HeraldRuleTypeConfig.php @@ -3,12 +3,14 @@ final class HeraldRuleTypeConfig { const RULE_TYPE_GLOBAL = 'global'; + const RULE_TYPE_OBJECT = 'object'; const RULE_TYPE_PERSONAL = 'personal'; public static function getRuleTypeMap() { $map = array( - self::RULE_TYPE_GLOBAL => pht('Global'), - self::RULE_TYPE_PERSONAL => pht('Personal'), + self::RULE_TYPE_PERSONAL => pht('Personal'), + self::RULE_TYPE_OBJECT => pht('Object'), + self::RULE_TYPE_GLOBAL => pht('Global'), ); return $map; } diff --git a/src/applications/herald/controller/HeraldController.php b/src/applications/herald/controller/HeraldController.php index a4f4eab1e0..5bb4d92f17 100644 --- a/src/applications/herald/controller/HeraldController.php +++ b/src/applications/herald/controller/HeraldController.php @@ -39,7 +39,7 @@ abstract class HeraldController extends PhabricatorController { $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); if ($for_app) { - $nav->addFilter('create', pht('Create Rule')); + $nav->addFilter('new', pht('Create Rule')); } id(new HeraldRuleSearchEngine()) diff --git a/src/applications/herald/controller/HeraldDisableController.php b/src/applications/herald/controller/HeraldDisableController.php index 13c8ad0807..1ded3d615a 100644 --- a/src/applications/herald/controller/HeraldDisableController.php +++ b/src/applications/herald/controller/HeraldDisableController.php @@ -28,7 +28,7 @@ final class HeraldDisableController extends HeraldController { return new Aphront404Response(); } - if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) { + if ($rule->isGlobalRule()) { $this->requireApplicationCapability( HeraldCapabilityManageGlobalRules::CAPABILITY); } diff --git a/src/applications/herald/controller/HeraldNewController.php b/src/applications/herald/controller/HeraldNewController.php index bb866d66ec..c207c41b82 100644 --- a/src/applications/herald/controller/HeraldNewController.php +++ b/src/applications/herald/controller/HeraldNewController.php @@ -2,33 +2,253 @@ final class HeraldNewController extends HeraldController { - private $contentType; - private $ruleType; - - public function willProcessRequest(array $data) { - $this->contentType = idx($data, 'type'); - $this->ruleType = idx($data, 'rule_type'); - } - public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); - - $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); - if (empty($content_type_map[$this->contentType])) { - $this->contentType = head_key($content_type_map); - } + $viewer = $request->getUser(); + $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); - if (empty($rule_type_map[$this->ruleType])) { - $this->ruleType = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; + + $errors = array(); + + $e_type = null; + $e_rule = null; + $e_object = null; + + $step = $request->getInt('step'); + if ($request->isFormPost()) { + $content_type = $request->getStr('content_type'); + if (empty($content_type_map[$content_type])) { + $errors[] = pht('You must choose a content type for this rule.'); + $e_type = pht('Required'); + $step = 0; + } + + if (!$errors && $step > 1) { + $rule_type = $request->getStr('rule_type'); + if (empty($rule_type_map[$rule_type])) { + $errors[] = pht('You must choose a rule type for this rule.'); + $e_rule = pht('Required'); + $step = 1; + } + } + + if (!$errors && $step >= 2) { + $target_phid = null; + $object_name = $request->getStr('objectName'); + $done = false; + if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) { + $done = true; + } else if (strlen($object_name)) { + $target_object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames(array($object_name)) + ->executeOne(); + if ($target_object) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $target_object, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + $errors[] = pht( + 'You can not create a rule for that object, because you do '. + 'not have permission to edit it. You can only create rules '. + 'for objects you can edit.'); + $e_object = pht('Not Editable'); + $step = 2; + } else { + $adapter = HeraldAdapter::getAdapterForContentType($content_type); + if (!$adapter->canTriggerOnObject($target_object)) { + $errors[] = pht( + 'This object is not of an allowed type for the rule. '. + 'Rules can only trigger on certain objects.'); + $e_object = pht('Invalid'); + $step = 2; + } else { + $target_phid = $target_object->getPHID(); + $done = true; + } + } + } else { + $errors[] = pht('No object exists by that name.'); + $e_object = pht('Invalid'); + $step = 2; + } + } else if ($step > 2) { + $errors[] = pht( + 'You must choose an object to associate this rule with.'); + $e_object = pht('Required'); + $step = 2; + } + + if (!$errors && $done) { + $uri = id(new PhutilURI('edit/')) + ->setQueryParams( + array( + 'content_type' => $content_type, + 'rule_type' => $rule_type, + 'targetPHID' => $target_phid, + )); + $uri = $this->getApplicationURI($uri); + return id(new AphrontRedirectResponse())->setURI($uri); + } + } } - // Reorder array to put "personal" first. + $content_type = $request->getStr('content_type'); + $rule_type = $request->getStr('rule_type'); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->setAction($this->getApplicationURI('new/')); + + switch ($step) { + case 0: + default: + $content_types = $this->renderContentTypeControl( + $content_type_map, + $e_type); + + $form + ->addHiddenInput('step', 1) + ->appendChild($content_types); + + $cancel_text = null; + $cancel_uri = $this->getApplicationURI(); + break; + case 1: + $rule_types = $this->renderRuleTypeControl( + $rule_type_map, + $e_rule); + + $form + ->addHiddenInput('content_type', $content_type) + ->addHiddenInput('step', 2) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Rule for')) + ->setValue( + phutil_tag( + 'strong', + array(), + idx($content_type_map, $content_type)))) + ->appendChild($rule_types); + + $cancel_text = pht('Back'); + $cancel_uri = id(new PhutilURI('new/')) + ->setQueryParams( + array( + 'content_type' => $content_type, + 'step' => 0, + )); + $cancel_uri = $this->getApplicationURI($cancel_uri); + break; + case 2: + $adapter = HeraldAdapter::getAdapterForContentType($content_type); + $form + ->addHiddenInput('content_type', $content_type) + ->addHiddenInput('rule_type', $rule_type) + ->addHiddenInput('step', 3) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Rule for')) + ->setValue( + phutil_tag( + 'strong', + array(), + idx($content_type_map, $content_type)))) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Rule Type')) + ->setValue( + phutil_tag( + 'strong', + array(), + idx($rule_type_map, $rule_type)))) + ->appendRemarkupInstructions( + pht( + 'Choose the object this rule will act on (for example, enter '. + '`rX` to act on the `rX` repository, or `#project` to act on '. + 'a project).')) + ->appendRemarkupInstructions( + $adapter->explainValidTriggerObjects()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('objectName') + ->setError($e_object) + ->setValue($request->getStr('objectName')) + ->setLabel(pht('Object'))); + + $cancel_text = pht('Back'); + $cancel_uri = id(new PhutilURI('new/')) + ->setQueryParams( + array( + 'content_type' => $content_type, + 'rule_type' => $rule_type, + 'step' => 1, + )); + $cancel_uri = $this->getApplicationURI($cancel_uri); + break; + } + + + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Continue')) + ->addCancelButton($cancel_uri, $cancel_text)); + + $form_box = id(new PHUIObjectBoxView()) + ->setFormErrors($errors) + ->setHeaderText(pht('Create Herald Rule')) + ->setForm($form); + + $crumbs = $this + ->buildApplicationCrumbs() + ->addTextCrumb(pht('Create Rule')); + + return $this->buildApplicationPage( + array( + $crumbs, + $form_box, + ), + array( + 'title' => pht('Create Herald Rule'), + 'device' => true, + )); + } + + private function renderContentTypeControl(array $content_type_map, $e_type) { + $request = $this->getRequest(); + + $radio = id(new AphrontFormRadioButtonControl()) + ->setLabel(pht('New Rule for')) + ->setName('content_type') + ->setValue($request->getStr('content_type')) + ->setError($e_type); + + foreach ($content_type_map as $value => $name) { + $adapter = HeraldAdapter::getAdapterForContentType($value); + $radio->addButton( + $value, + $name, + phutil_escape_html_newlines($adapter->getAdapterContentDescription())); + } + + return $radio; + } + + + private function renderRuleTypeControl(array $rule_type_map, $e_rule) { + $request = $this->getRequest(); + + // Reorder array to put less powerful rules first. $rule_type_map = array_select_keys( $rule_type_map, array( HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, + HeraldRuleTypeConfig::RULE_TYPE_OBJECT, + HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, )) + $rule_type_map; list($can_global, $global_link) = $this->explainApplicationCapability( @@ -42,6 +262,12 @@ final class HeraldNewController extends HeraldController { 'Personal rules notify you about events. You own them, but they can '. 'only affect you. Personal rules only trigger for objects you have '. 'permission to see.'), + HeraldRuleTypeConfig::RULE_TYPE_OBJECT => + pht( + 'Object rules notify anyone about events. They are bound to an '. + 'object (like a repository) and can only act on that object. You '. + 'must be able to edit an object to create object rules for it. '. + 'Other users who an edit the object can edit its rules.'), HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => array( pht( @@ -52,56 +278,40 @@ final class HeraldNewController extends HeraldController { ); $radio = id(new AphrontFormRadioButtonControl()) - ->setLabel(pht('Type')) + ->setLabel(pht('Rule Type')) ->setName('rule_type') - ->setValue($this->ruleType); + ->setValue($request->getStr('rule_type')) + ->setError($e_rule); + + $adapter = HeraldAdapter::getAdapterForContentType( + $request->getStr('content_type')); foreach ($rule_type_map as $value => $name) { + $caption = idx($captions, $value); $disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) && (!$can_global); + if (!$adapter->supportsRuleType($value)) { + $disabled = true; + $caption = array( + $caption, + "\n\n", + phutil_tag( + 'em', + array(), + pht( + 'This rule type is not supported by the selected content type.')), + ); + } + $radio->addButton( $value, $name, - idx($captions, $value), + phutil_escape_html_newlines($caption), $disabled ? 'disabled' : null, $disabled); } - $form = id(new AphrontFormView()) - ->setUser($user) - ->setAction('/herald/edit/') - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('New Rule for')) - ->setName('content_type') - ->setValue($this->contentType) - ->setOptions($content_type_map)) - ->appendChild($radio) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Create Rule')) - ->addCancelButton($this->getApplicationURI())); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Create Herald Rule')) - ->setForm($form); - - $crumbs = $this - ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Rule'))); - - return $this->buildApplicationPage( - array( - $crumbs, - $form_box, - ), - array( - 'title' => pht('Create Herald Rule'), - 'device' => true, - )); + return $radio; } - } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index adfa8fef49..36a904038a 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -46,10 +46,42 @@ final class HeraldRuleController extends HeraldController { } $rule->setRuleType($rule_type); + $adapter = HeraldAdapter::getAdapterForContentType( + $rule->getContentType()); + + if (!$adapter->supportsRuleType($rule->getRuleType())) { + throw new Exception( + pht( + "This rule's content type does not support the selected rule ". + "type.")); + } + + if ($rule->isObjectRule()) { + $rule->setTriggerObjectPHID($request->getStr('targetPHID')); + $object = id(new PhabricatorObjectQuery()) + ->setViewer($user) + ->withPHIDs(array($rule->getTriggerObjectPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + throw new Exception( + pht('No valid object provided for object rule!')); + } + + if (!$adapter->canTriggerOnObject($object)) { + throw new Exception( + pht('Object is of wrong type for adapter!')); + } + } + $cancel_uri = $this->getApplicationURI(); } - if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) { + if ($rule->isGlobalRule()) { $this->requireApplicationCapability( HeraldCapabilityManageGlobalRules::CAPABILITY); } @@ -59,9 +91,10 @@ final class HeraldRuleController extends HeraldController { $local_version = id(new HeraldRule())->getConfigVersion(); if ($rule->getConfigVersion() > $local_version) { throw new Exception( - "This rule was created with a newer version of Herald. You can not ". - "view or edit it in this older version. Upgrade your Phabricator ". - "deployment."); + pht( + "This rule was created with a newer version of Herald. You can not ". + "view or edit it in this older version. Upgrade your Phabricator ". + "deployment.")); } // Upgrade rule version to our version, since we might add newly-defined @@ -85,14 +118,6 @@ final class HeraldRuleController extends HeraldController { } } - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } else { - $error_view = null; - } - $must_match_selector = $this->renderMustMatchSelector($rule); $repetition_selector = $this->renderRepetitionSelector($rule, $adapter); @@ -126,6 +151,16 @@ final class HeraldRuleController extends HeraldController { ->setError($e_name) ->setValue($rule->getName())); + $trigger_object_control = false; + if ($rule->isObjectRule()) { + $trigger_object_control = id(new AphrontFormStaticControl()) + ->setValue( + pht( + 'This rule triggers for %s.', + $handles[$rule->getTriggerObjectPHID()]->renderLink())); + } + + $form ->appendChild( id(new AphrontFormMarkupControl()) @@ -133,6 +168,7 @@ final class HeraldRuleController extends HeraldController { "This %s rule triggers for %s.", phutil_tag('strong', array(), $rule_type_name), phutil_tag('strong', array(), $content_type_name)))) + ->appendChild($trigger_object_control) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Conditions')) @@ -189,14 +225,12 @@ final class HeraldRuleController extends HeraldController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + ->addTextCrumb($title); return $this->buildApplicationPage( array( @@ -349,6 +383,7 @@ final class HeraldRuleController extends HeraldController { switch ($action->getAction()) { case HeraldAdapter::ACTION_FLAG: + case HeraldAdapter::ACTION_BLOCK: $current_value = $action->getTarget(); break; default: @@ -414,12 +449,42 @@ final class HeraldRuleController extends HeraldController { 'root' => 'herald-rule-edit-form', 'conditions' => (object)$serial_conditions, 'actions' => (object)$serial_actions, + 'select' => array( + HeraldAdapter::VALUE_CONTENT_SOURCE => array( + 'options' => PhabricatorContentSource::getSourceNameMap(), + 'default' => PhabricatorContentSource::SOURCE_WEB, + ), + HeraldAdapter::VALUE_FLAG_COLOR => array( + 'options' => PhabricatorFlagColor::getColorNameMap(), + 'default' => PhabricatorFlagColor::COLOR_BLUE, + ), + HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array( + 'options' => array( + PhabricatorRepositoryPushLog::REFTYPE_BRANCH + => pht('branch (git/hg)'), + PhabricatorRepositoryPushLog::REFTYPE_TAG + => pht('tag (git)'), + PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK + => pht('bookmark (hg)'), + ), + 'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH, + ), + HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array( + 'options' => array( + PhabricatorRepositoryPushLog::CHANGEFLAG_ADD => + pht('change creates ref'), + PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE => + pht('change deletes ref'), + PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE => + pht('change rewrites ref'), + PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS => + pht('dangerous change'), + ), + 'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD, + ), + ), 'template' => $this->buildTokenizerTemplates() + array( 'rules' => $all_rules, - 'colors' => PhabricatorFlagColor::getColorNameMap(), - 'defaultColor' => PhabricatorFlagColor::COLOR_BLUE, - 'contentSources' => PhabricatorContentSource::getSourceNameMap(), - 'defaultSource' => PhabricatorContentSource::SOURCE_WEB ), 'author' => array($rule->getAuthorPHID() => $handles[$rule->getAuthorPHID()]->getName()), @@ -453,6 +518,10 @@ final class HeraldRuleController extends HeraldController { $phids[] = $rule->getAuthorPHID(); + if ($rule->isObjectRule()) { + $phids[] = $rule->getTriggerObjectPHID(); + } + return $this->loadViewerHandles($phids); } @@ -511,6 +580,7 @@ final class HeraldRuleController extends HeraldController { 'package' => '/typeahead/common/packages/', 'project' => '/typeahead/common/projects/', 'userorproject' => '/typeahead/common/accountsorprojects/', + 'buildplan' => '/typeahead/common/buildplans/', ), 'markup' => $template, ); @@ -531,7 +601,17 @@ final class HeraldRuleController extends HeraldController { ->withContentTypes(array($rule->getContentType())) ->execute(); - if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { + if ($rule->isObjectRule()) { + // Object rules may depend on other rules for the same object. + $all_rules += id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT)) + ->withContentTypes(array($rule->getContentType())) + ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID())) + ->execute(); + } + + if ($rule->isPersonalRule()) { // Personal rules may depend upon your other personal rules. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) diff --git a/src/applications/herald/controller/HeraldRuleEditHistoryController.php b/src/applications/herald/controller/HeraldRuleEditHistoryController.php index 03ad026c01..7419272286 100644 --- a/src/applications/herald/controller/HeraldRuleEditHistoryController.php +++ b/src/applications/herald/controller/HeraldRuleEditHistoryController.php @@ -37,10 +37,9 @@ final class HeraldRuleEditHistoryController extends HeraldController { $crumbs = $this ->buildApplicationCrumbs($can_create = false) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit History')) - ->setHref($this->getApplicationURI('herald/history'))); + ->addTextCrumb( + pht('Edit History'), + $this->getApplicationURI('herald/history')); $nav = $this->buildSideNavView(); $nav->selectFilter('history'); diff --git a/src/applications/herald/controller/HeraldRuleListController.php b/src/applications/herald/controller/HeraldRuleListController.php index bb52435d65..8d5ea7f95f 100644 --- a/src/applications/herald/controller/HeraldRuleListController.php +++ b/src/applications/herald/controller/HeraldRuleListController.php @@ -41,7 +41,7 @@ final class HeraldRuleListController extends HeraldController $id = $rule->getID(); $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Rule %s', $rule->getID())) + ->setObjectName("H{$id}") ->setHeader($rule->getName()) ->setHref($this->getApplicationURI("rule/{$id}/")); diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index d4c9577805..ea5c6b44e2 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -41,10 +41,10 @@ final class HeraldRuleViewController extends HeraldController { $actions = $this->buildActionView($rule); $properties = $this->buildPropertyView($rule, $actions); + $id = $rule->getID(); + $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Rule %d', $rule->getID()))); + $crumbs->addTextCrumb("H{$id}"); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) @@ -131,6 +131,7 @@ final class HeraldRuleViewController extends HeraldController { $this->getHandle($rule->getAuthorPHID())->renderLink()); } + $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { $view->addProperty( @@ -139,6 +140,12 @@ final class HeraldRuleViewController extends HeraldController { HeraldAdapter::getEnabledAdapterMap($viewer), $rule->getContentType())); + if ($rule->isObjectRule()) { + $view->addProperty( + pht('Trigger Object'), + $this->getHandle($rule->getTriggerObjectPHID())->renderLink()); + } + $view->invokeWillRenderEvent(); $view->addSectionHeader(pht('Rule Description')); diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 4ee5cab5d2..7ec11b9d19 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -39,13 +39,8 @@ final class HeraldTestConsoleController extends HeraldController { $object, $object->loadActiveDiff()); } else if ($object instanceof PhabricatorRepositoryCommit) { - $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( - 'commitID = %d', - $object->getID()); - $adapter = HeraldCommitAdapter::newLegacyAdapter( - $object->getRepository(), - $object, - $data); + $adapter = id(new HeraldCommitAdapter()) + ->setCommit($object); } else if ($object instanceof ManiphestTask) { $adapter = id(new HeraldManiphestTaskAdapter()) ->setTask($object); @@ -79,23 +74,13 @@ final class HeraldTestConsoleController extends HeraldController { } } - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } else { - $error_view = null; - } - - $text = pht( - 'Enter an object to test rules for, like a Diffusion commit (e.g., '. - 'rX123) or a Differential revision (e.g., D123). You will be shown '. - 'the results of a dry run on the object.'); - $form = id(new AphrontFormView()) ->setUser($user) - ->appendChild(hsprintf( - '

%s

', $text)) + ->appendRemarkupInstructions( + pht( + 'Enter an object to test rules for, like a Diffusion commit (e.g., '. + '`rX123`) or a Differential revision (e.g., `D123`). You will be '. + 'shown the results of a dry run on the object.')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Object Name')) @@ -108,17 +93,14 @@ final class HeraldTestConsoleController extends HeraldController { $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Herald Test Console')) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = id($this->buildApplicationCrumbs()) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Transcripts')) - ->setHref($this->getApplicationURI('/transcript/'))) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Test Console'))); + ->addTextCrumb( + pht('Transcripts'), + $this->getApplicationURI('/transcript/')) + ->addTextCrumb(pht('Test Console')); return $this->buildApplicationPage( $box, diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php index 7cdf5aade1..c08f54d3e8 100644 --- a/src/applications/herald/controller/HeraldTranscriptController.php +++ b/src/applications/herald/controller/HeraldTranscriptController.php @@ -16,7 +16,7 @@ final class HeraldTranscriptController extends HeraldController { $map = $this->getFilterMap(); $this->filter = idx($data, 'filter'); if (empty($map[$this->filter])) { - $this->filter = self::FILTER_AFFECTED; + $this->filter = self::FILTER_ALL; } } @@ -97,13 +97,10 @@ final class HeraldTranscriptController extends HeraldController { } $crumbs = id($this->buildApplicationCrumbs()) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Transcripts')) - ->setHref($this->getApplicationURI('/transcript/'))) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($xscript->getID())); + ->addTextCrumb( + pht('Transcripts'), + $this->getApplicationURI('/transcript/')) + ->addTextCrumb($xscript->getID()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( @@ -133,7 +130,7 @@ final class HeraldTranscriptController extends HeraldController { $value = implode(', ', $value); } - return hsprintf('%s', $value); + return phutil_tag('span', array('class' => 'condition-test-value'), $value); } private function buildSideNav() { @@ -152,9 +149,9 @@ final class HeraldTranscriptController extends HeraldController { protected function getFilterMap() { return array( - self::FILTER_AFFECTED => pht('Rules that Affected Me'), - self::FILTER_OWNED => pht('Rules I Own'), self::FILTER_ALL => pht('All Rules'), + self::FILTER_OWNED => pht('Rules I Own'), + self::FILTER_AFFECTED => pht('Rules that Affected Me'), ); } @@ -293,6 +290,10 @@ final class HeraldTranscriptController extends HeraldController { case HeraldAdapter::ACTION_FLAG: $target = PhabricatorFlagColor::getColorName($target); break; + case HeraldAdapter::ACTION_BLOCK: + // Target is a text string. + $target = $target; + break; default: if ($target) { foreach ($target as $k => $phid) { @@ -306,13 +307,15 @@ final class HeraldTranscriptController extends HeraldController { } if ($apply_xscript->getApplied()) { - $success = pht('SUCCESS'); - $outcome = - hsprintf('%s', $success); + $outcome = phutil_tag( + 'span', + array('class' => 'outcome-success'), + pht('SUCCESS')); } else { - $failure = pht('FAILURE'); - $outcome = - hsprintf('%s', $failure); + $outcome = phutil_tag( + 'span', + array('class' => 'outcome-failure'), + pht('FAILURE')); } $rows[] = array( @@ -366,23 +369,21 @@ final class HeraldTranscriptController extends HeraldController { $cond_markup = array(); foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) { if ($cond->getNote()) { - $note = hsprintf( - '
%s
', - $cond->getNote()); + $note = phutil_tag_div('herald-condition-note', $cond->getNote()); } else { $note = null; } if ($cond->getResult()) { - $result = hsprintf( - ''. - "\xE2\x9C\x93". - ''); + $result = phutil_tag( + 'span', + array('class' => 'herald-outcome condition-pass'), + "\xE2\x9C\x93"); } else { - $result = hsprintf( - ''. - "\xE2\x9C\x98". - ''); + $result = phutil_tag( + 'span', + array('class' => 'herald-outcome condition-fail'), + "\xE2\x9C\x98"); } $cond_markup[] = phutil_tag( @@ -398,18 +399,23 @@ final class HeraldTranscriptController extends HeraldController { } if ($rule->getResult()) { - $pass = pht('PASS'); - $result = hsprintf( - '%s', $pass); + $result = phutil_tag( + 'span', + array('class' => 'herald-outcome rule-pass'), + pht('PASS')); $class = 'herald-rule-pass'; } else { - $fail = pht('FAIL'); - $result = hsprintf( - '%s', $fail); + $result = phutil_tag( + 'span', + array('class' => 'herald-outcome rule-fail'), + pht('FAIL')); $class = 'herald-rule-fail'; } - $cond_markup[] = hsprintf('
  • %s %s
  • ', $result, $rule->getReason()); + $cond_markup[] = phutil_tag( + 'li', + array(), + array($result, $rule->getReason())); $user_phid = $this->getRequest()->getUser()->getPHID(); $name = $rule->getRuleName(); @@ -420,11 +426,11 @@ final class HeraldTranscriptController extends HeraldController { array( 'class' => $class, ), - hsprintf( - '
    %s %s
    %s', - $name, - $handles[$rule->getRuleOwner()]->getName(), - phutil_tag('ul', array(), $cond_markup))); + phutil_tag_div('rule-name', array( + phutil_tag('strong', array(), $name), + ' ', + phutil_tag('ul', array(), $cond_markup), + ))); } $panel = ''; diff --git a/src/applications/herald/controller/HeraldTranscriptListController.php b/src/applications/herald/controller/HeraldTranscriptListController.php index 64335b5350..29004e0379 100644 --- a/src/applications/herald/controller/HeraldTranscriptListController.php +++ b/src/applications/herald/controller/HeraldTranscriptListController.php @@ -72,9 +72,7 @@ final class HeraldTranscriptListController extends HeraldController { $nav->appendChild($panel); $crumbs = id($this->buildApplicationCrumbs()) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Transcripts'))); + ->addTextCrumb(pht('Transcripts')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( diff --git a/src/applications/herald/engine/HeraldEffect.php b/src/applications/herald/engine/HeraldEffect.php index 95b6a87b22..1d4955c7ed 100644 --- a/src/applications/herald/engine/HeraldEffect.php +++ b/src/applications/herald/engine/HeraldEffect.php @@ -2,14 +2,15 @@ final class HeraldEffect { - protected $objectPHID; - protected $action; - protected $target; + private $objectPHID; + private $action; + private $target; - protected $ruleID; - protected $effector; + private $ruleID; + private $rulePHID; + private $effector; - protected $reason; + private $reason; public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; @@ -47,6 +48,15 @@ final class HeraldEffect { return $this->ruleID; } + public function setRulePHID($rule_phid) { + $this->rulePHID = $rule_phid; + return $this; + } + + public function getRulePHID() { + return $this->rulePHID; + } + public function setEffector($effector) { $this->effector = $effector; return $this; diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php index 71961db083..f2915d703e 100644 --- a/src/applications/herald/engine/HeraldEngine.php +++ b/src/applications/herald/engine/HeraldEngine.php @@ -24,8 +24,8 @@ final class HeraldEngine { return idx($this->rules, $id); } - public static function loadAndApplyRules(HeraldAdapter $adapter) { - $rules = id(new HeraldRuleQuery()) + public function loadRulesForAdapter(HeraldAdapter $adapter) { + return id(new HeraldRuleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDisabled(false) ->withContentTypes(array($adapter->getAdapterContentType())) @@ -33,8 +33,12 @@ final class HeraldEngine { ->needAppliedToPHIDs(array($adapter->getPHID())) ->needValidateAuthors(true) ->execute(); + } + public static function loadAndApplyRules(HeraldAdapter $adapter) { $engine = new HeraldEngine(); + + $rules = $engine->loadRulesForAdapter($adapter); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); @@ -252,6 +256,11 @@ final class HeraldEngine { "Rule failed automatically because it is a personal rule and its ". "owner can not see the object."); $result = false; + } else if (!$this->canRuleApplyToObject($rule, $object)) { + $reason = pht( + "Rule failed automatically because it is an object rule which is ". + "not relevant for this object."); + $result = false; } else { foreach ($conditions as $condition) { $match = $this->doesConditionMatch($rule, $condition, $object); @@ -359,11 +368,14 @@ final class HeraldEngine { $effect->setTarget($action->getTarget()); $effect->setRuleID($rule->getID()); + $effect->setRulePHID($rule->getPHID()); $name = $rule->getName(); $id = $rule->getID(); $effect->setReason( - 'Conditions were met for Herald rule "'.$name.'" (#'.$id.').'); + pht( + 'Conditions were met for %s', + "H{$id} {$name}")); $effects[] = $effect; } @@ -374,8 +386,8 @@ final class HeraldEngine { HeraldRule $rule, HeraldAdapter $adapter) { - // Authorship is irrelevant for global rules. - if ($rule->isGlobalRule()) { + // Authorship is irrelevant for global rules and object rules. + if ($rule->isGlobalRule() || $rule->isObjectRule()) { return true; } @@ -398,4 +410,25 @@ final class HeraldEngine { PhabricatorPolicyCapability::CAN_VIEW); } + private function canRuleApplyToObject( + HeraldRule $rule, + HeraldAdapter $adapter) { + + // Rules which are not object rules can apply to anything. + if (!$rule->isObjectRule()) { + return true; + } + + $trigger_phid = $rule->getTriggerObjectPHID(); + $object_phids = $adapter->getTriggerObjectPHIDs(); + + if ($object_phids) { + if (in_array($trigger_phid, $object_phids)) { + return true; + } + } + + return false; + } + } diff --git a/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php b/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php new file mode 100644 index 0000000000..18e3ce0b7d --- /dev/null +++ b/src/applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php @@ -0,0 +1,31 @@ +establishConnection('w'); + + queryfx( + $conn_w, + 'UPDATE %T SET + objectTranscript = "", + ruleTranscripts = "", + conditionTranscripts = "", + applyTranscripts = "", + garbageCollected = 1 + WHERE garbageCollected = 0 AND `time` < %d + LIMIT 100', + $table->getTableName(), + time() - $ttl); + + return ($conn_w->getAffectedRows() == 100); + } + +} diff --git a/src/applications/herald/phid/HeraldPHIDTypeRule.php b/src/applications/herald/phid/HeraldPHIDTypeRule.php index a3022d3c0f..62b05c696d 100644 --- a/src/applications/herald/phid/HeraldPHIDTypeRule.php +++ b/src/applications/herald/phid/HeraldPHIDTypeRule.php @@ -35,9 +35,39 @@ final class HeraldPHIDTypeRule extends PhabricatorPHIDType { $id = $rule->getID(); $name = $rule->getName(); - $handle->setName($name); + $handle->setName("H{$id}"); + $handle->setFullName("H{$id} {$name}"); $handle->setURI("/herald/rule/{$id}/"); } } + public function canLoadNamedObject($name) { + return preg_match('/^H\d*[1-9]\d*$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + $id_map = array(); + foreach ($names as $name) { + $id = (int)substr($name, 1); + $id_map[$id][] = $name; + } + + $objects = id(new HeraldRuleQuery()) + ->setViewer($query->getViewer()) + ->withIDs(array_keys($id_map)) + ->execute(); + + $results = array(); + foreach ($objects as $id => $object) { + foreach (idx($id_map, $id, array()) as $name) { + $results[$name] = $object; + } + } + + return $results; + } + } diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php index e9d3d05d5f..c509c62023 100644 --- a/src/applications/herald/query/HeraldRuleQuery.php +++ b/src/applications/herald/query/HeraldRuleQuery.php @@ -9,6 +9,7 @@ final class HeraldRuleQuery private $ruleTypes; private $contentTypes; private $disabled; + private $triggerObjectPHIDs; private $needConditionsAndActions; private $needAppliedToPHIDs; @@ -49,6 +50,11 @@ final class HeraldRuleQuery return $this; } + public function withTriggerObjectPHIDs(array $phids) { + $this->triggerObjectPHIDs = $phids; + return $this; + } + public function needConditionsAndActions($need) { $this->needConditionsAndActions = $need; return $this; @@ -137,6 +143,35 @@ final class HeraldRuleQuery } } + $object_phids = array(); + foreach ($rules as $rule) { + if ($rule->isObjectRule()) { + $object_phids[] = $rule->getTriggerObjectPHID(); + } + } + + if ($object_phids) { + $objects = id(new PhabricatorObjectQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($object_phids) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + } else { + $objects = array(); + } + + foreach ($rules as $key => $rule) { + if ($rule->isObjectRule()) { + $object = idx($objects, $rule->getTriggerObjectPHID()); + if (!$object) { + unset($rules[$key]); + continue; + } + $rule->attachTriggerObject($object); + } + } + return $rules; } @@ -185,6 +220,13 @@ final class HeraldRuleQuery (int)$this->disabled); } + if ($this->triggerObjectPHIDs) { + $where[] = qsprintf( + $conn_r, + 'rule.triggerObjectPHID IN (%Ls)', + $this->triggerObjectPHIDs); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); @@ -192,9 +234,9 @@ final class HeraldRuleQuery private function validateRuleAuthors(array $rules) { - // "Global" rules always have valid authors. + // "Global" and "Object" rules always have valid authors. foreach ($rules as $key => $rule) { - if ($rule->isGlobalRule()) { + if ($rule->isGlobalRule() || $rule->isObjectRule()) { $rule->attachValidAuthor(true); unset($rules[$key]); continue; @@ -219,7 +261,7 @@ final class HeraldRuleQuery $rule->attachValidAuthor(false); continue; } - if ($users[$author_phid]->getIsDisabled()) { + if (!$users[$author_phid]->isUserActivated()) { $rule->attachValidAuthor(false); continue; } diff --git a/src/applications/herald/remarkup/HeraldRemarkupRule.php b/src/applications/herald/remarkup/HeraldRemarkupRule.php new file mode 100644 index 0000000000..be84526103 --- /dev/null +++ b/src/applications/herald/remarkup/HeraldRemarkupRule.php @@ -0,0 +1,18 @@ +getEngine()->getConfig('viewer'); + return id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + } + +} diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index ddc3cfbbfa..82eb9926b6 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -15,14 +15,17 @@ final class HeraldRule extends HeraldDAO protected $repetitionPolicy; protected $ruleType; protected $isDisabled = 0; + protected $triggerObjectPHID; - protected $configVersion = 14; + protected $configVersion = 28; - private $ruleApplied = self::ATTACHABLE; // phids for which this rule has been applied + // phids for which this rule has been applied + private $ruleApplied = self::ATTACHABLE; private $validAuthor = self::ATTACHABLE; private $author = self::ATTACHABLE; private $conditions; private $actions; + private $triggerObject = self::ATTACHABLE; public function getConfiguration() { return array( @@ -131,8 +134,7 @@ final class HeraldRule extends HeraldDAO $child->setRuleID($this->getID()); } -// TODO: -// $this->openTransaction(); + $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', @@ -141,13 +143,11 @@ final class HeraldRule extends HeraldDAO foreach ($children as $child) { $child->save(); } -// $this->saveTransaction(); + $this->saveTransaction(); } public function delete() { - -// TODO: -// $this->openTransaction(); + $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', @@ -158,8 +158,10 @@ final class HeraldRule extends HeraldDAO 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldAction())->getTableName(), $this->getID()); - parent::delete(); -// $this->saveTransaction(); + $result = parent::delete(); + $this->saveTransaction(); + + return $result; } public function hasValidAuthor() { @@ -188,6 +190,19 @@ final class HeraldRule extends HeraldDAO return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); } + public function isObjectRule() { + return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT); + } + + public function attachTriggerObject($trigger_object) { + $this->triggerObject = $trigger_object; + return $this; + } + + public function getTriggerObject() { + return $this->assertAttached($this->triggerObject); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -210,6 +225,8 @@ final class HeraldRule extends HeraldDAO $global = HeraldCapabilityManageGlobalRules::CAPABILITY; return $herald->getPolicy($global); } + } else if ($this->isObjectRule()) { + return $this->getTriggerObject()->getPolicy($capability); } else { return PhabricatorPolicies::POLICY_NOONE; } @@ -226,6 +243,8 @@ final class HeraldRule extends HeraldDAO public function describeAutomaticCapability($capability) { if ($this->isPersonalRule()) { return pht("A personal rule's owner can always view and edit it."); + } else if ($this->isObjectRule()) { + return pht("Object rules inherit the policies of their objects."); } return null; diff --git a/src/applications/herald/storage/__tests__/HeraldTranscriptTestCase.php b/src/applications/herald/storage/__tests__/HeraldTranscriptTestCase.php new file mode 100644 index 0000000000..799c8b9e34 --- /dev/null +++ b/src/applications/herald/storage/__tests__/HeraldTranscriptTestCase.php @@ -0,0 +1,47 @@ +"; + + $long_array = array( + 'a' => $long_string, + 'b' => $long_string, + ); + + $mixed_array = array( + 'a' => 'abc', + 'b' => 'def', + 'c' => $long_string, + ); + + $fields = array( + 'ls' => $long_string, + 'la' => $long_array, + 'ma' => $mixed_array, + ); + + $truncated_fields = id(new HeraldObjectTranscript()) + ->setFields($fields) + ->getFields(); + + $this->assertEqual($short_string, $truncated_fields['ls']); + + $this->assertEqual( + array('a', '<...>'), + array_keys($truncated_fields['la'])); + $this->assertEqual( + $short_string.'!<...>', + implode('!', $truncated_fields['la'])); + + $this->assertEqual( + array('a', 'b', 'c'), + array_keys($truncated_fields['ma'])); + $this->assertEqual( + 'abc!def!'.substr($short_string, 6), + implode('!', $truncated_fields['ma'])); + + } +} \ No newline at end of file diff --git a/src/applications/herald/storage/transcript/HeraldObjectTranscript.php b/src/applications/herald/storage/transcript/HeraldObjectTranscript.php index 701bb132fd..e63dc7d6b2 100644 --- a/src/applications/herald/storage/transcript/HeraldObjectTranscript.php +++ b/src/applications/herald/storage/transcript/HeraldObjectTranscript.php @@ -35,6 +35,10 @@ final class HeraldObjectTranscript { } public function setFields(array $fields) { + foreach ($fields as $key => $value) { + $fields[$key] = self::truncateValue($value, 4096); + } + $this->fields = $fields; return $this; } @@ -42,4 +46,30 @@ final class HeraldObjectTranscript { public function getFields() { return $this->fields; } + + private static function truncateValue($value, $length) { + if (is_string($value)) { + if (strlen($value) <= $length) { + return $value; + } else { + // NOTE: phutil_utf8_shorten() has huge runtime for giant strings. + return phutil_utf8ize(substr($value, 0, $length)."\n<...>"); + } + } else if (is_array($value)) { + foreach ($value as $key => $v) { + if ($length <= 0) { + $value['<...>'] = '<...>'; + unset($value[$key]); + } else { + $v = self::truncateValue($v, $length); + $length -= strlen($v); + $value[$key] = $v; + } + } + return $value; + } else { + return $value; + } + } + } diff --git a/src/applications/home/application/PhabricatorApplicationHome.php b/src/applications/home/application/PhabricatorApplicationHome.php new file mode 100644 index 0000000000..34ab9db4a9 --- /dev/null +++ b/src/applications/home/application/PhabricatorApplicationHome.php @@ -0,0 +1,109 @@ +(?:jump))/)?' => 'PhabricatorHomeMainController', + '/home/' => array( + 'create/' => 'PhabricatorHomeQuickCreateController', + ), + ); + } + + public function shouldAppearInLaunchView() { + return false; + } + + public function canUninstall() { + return false; + } + + public function getApplicationOrder() { + return 9; + } + + public function buildMainMenuItems( + PhabricatorUser $user, + PhabricatorController $controller = null) { + + $items = array(); + + if ($user->isLoggedIn() && $user->isUserActivated()) { + $create_id = celerity_generate_unique_node_id(); + Javelin::initBehavior( + 'aphlict-dropdown', + array( + 'bubbleID' => $create_id, + 'dropdownID' => 'phabricator-quick-create-menu', + 'local' => true, + 'desktop' => true, + 'right' => true, + )); + + $item = id(new PHUIListItemView()) + ->setName(pht('Create New...')) + ->setIcon('new-sm') + ->addClass('core-menu-item') + ->setHref('/home/create/') + ->addSigil('quick-create-menu') + ->setID($create_id) + ->setOrder(300); + $items[] = $item; + } + + return $items; + } + + public function loadAllQuickCreateItems(PhabricatorUser $viewer) { + $applications = id(new PhabricatorApplicationQuery()) + ->setViewer($viewer) + ->withInstalled(true) + ->execute(); + + $items = array(); + foreach ($applications as $application) { + $app_items = $application->getQuickCreateItems($viewer); + foreach ($app_items as $app_item) { + $items[] = $app_item; + } + } + + return $items; + } + + public function buildMainMenuExtraNodes( + PhabricatorUser $viewer, + PhabricatorController $controller = null) { + + $items = $this->loadAllQuickCreateItems($viewer); + + $view = new PHUIListView(); + $view->newLabel(pht('Create New...')); + foreach ($items as $item) { + $view->addMenuItem($item); + } + + return phutil_tag( + 'div', + array( + 'id' => 'phabricator-quick-create-menu', + 'class' => 'phabricator-main-menu-dropdown phui-list-sidenav', + 'style' => 'display: none', + ), + $view); + } + +} diff --git a/src/applications/directory/controller/PhabricatorDirectoryController.php b/src/applications/home/controller/PhabricatorHomeController.php similarity index 98% rename from src/applications/directory/controller/PhabricatorDirectoryController.php rename to src/applications/home/controller/PhabricatorHomeController.php index 5c5348f8e0..89f144af13 100644 --- a/src/applications/directory/controller/PhabricatorDirectoryController.php +++ b/src/applications/home/controller/PhabricatorHomeController.php @@ -1,6 +1,6 @@ filter = idx($data, 'filter'); } @@ -136,18 +140,10 @@ final class PhabricatorDirectoryMainController 'Nothing appears to be critically broken right now.'); } + $href = '/maniphest/?statuses[]=0&priorities[]='.$unbreak_now.'#R'; + $title = pht('Unbreak Now!'); $panel = new AphrontPanelView(); - $panel->setHeader('Unbreak Now!'); - $panel->setCaption('Open tasks with "Unbreak Now!" priority.'); - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => '/maniphest/?statuses[]=0&priorities[]='.$unbreak_now.'#R', - 'class' => 'grey button', - ), - "View All Unbreak Now \xC2\xBB")); - + $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); @@ -188,21 +184,11 @@ final class PhabricatorDirectoryMainController 'need triage.')); } + $title = pht('Needs Triage'); + $href = '/maniphest/?statuses[]=0&priorities[]='.$needs_triage. + '&userProjects[]='.$user->getPHID().'#R'; $panel = new AphrontPanelView(); - $panel->setHeader('Needs Triage'); - $panel->setCaption(hsprintf( - 'Open tasks with "Needs Triage" priority in '. - 'projects you are a member of.')); - - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => '/maniphest/?statuses[]=0&priorities[]='.$needs_triage. - '&userProjects[]='.$user->getPHID().'#R', - 'class' => 'grey button', - ), - "View All Triage \xC2\xBB")); + $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); @@ -235,18 +221,10 @@ final class PhabricatorDirectoryMainController 'No revisions are waiting on you.'); } + $title = pht('Revisions Waiting on You'); + $href = '/differential'; $panel = new AphrontPanelView(); - $panel->setHeader('Revisions Waiting on You'); - $panel->setCaption('Revisions waiting for you for review or commit.'); - - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => '/differential/', - 'class' => 'button grey', - ), - "View Active Revisions \xC2\xBB")); + $panel->setHeader($this->renderSectionHeader($title, $href)); $revision_view = id(new DifferentialRevisionListView()) ->setHighlightAge(true) @@ -317,17 +295,10 @@ final class PhabricatorDirectoryMainController 'You have no assigned tasks.'); } + $title = pht('Assigned Tasks'); + $href = '/maniphest'; $panel = new AphrontPanelView(); - $panel->setHeader('Assigned Tasks'); - - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => '/maniphest/', - 'class' => 'button grey', - ), - "View Active Tasks \xC2\xBB")); + $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); @@ -420,6 +391,16 @@ final class PhabricatorDirectoryMainController return $container; } + private function renderSectionHeader($title, $href) { + $header = phutil_tag( + 'a', + array( + 'href' => $href, + ), + $title); + return $header; + } + private function renderMiniPanel($title, $body) { $panel = new AphrontMiniPanelView(); $panel->appendChild( @@ -468,18 +449,11 @@ final class PhabricatorDirectoryMainController $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); + $title = pht('Audits'); + $href = '/audit/'; $panel = new AphrontPanelView(); - $panel->setHeader('Audits'); - $panel->setCaption('Commits awaiting your audit.'); + $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => '/audit/', - 'class' => 'button grey', - ), - "View Active Audits \xC2\xBB")); $panel->setNoBackground(); return $panel; @@ -516,18 +490,11 @@ final class PhabricatorDirectoryMainController $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); + $title = pht('Problem Commits'); + $href = '/audit/'; $panel = new AphrontPanelView(); - $panel->setHeader('Problem Commits'); - $panel->setCaption('Commits which auditors have raised concerns about.'); + $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => '/audit/', - 'class' => 'button grey', - ), - "View Problem Commits \xC2\xBB")); $panel->setNoBackground(); return $panel; diff --git a/src/applications/home/controller/PhabricatorHomeQuickCreateController.php b/src/applications/home/controller/PhabricatorHomeQuickCreateController.php new file mode 100644 index 0000000000..82b39cb688 --- /dev/null +++ b/src/applications/home/controller/PhabricatorHomeQuickCreateController.php @@ -0,0 +1,36 @@ +getRequest()->getUser(); + + $items = $this->getCurrentApplication()->loadAllQuickCreateItems($viewer); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($items as $item) { + $list->addItem( + id(new PHUIObjectItemView()) + ->setHeader($item->getName()) + ->setWorkflow($item->getWorkflow()) + ->setHref($item->getHref())); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Quick Create')); + + return $this->buildApplicationPage( + array( + $crumbs, + $list, + ), + array( + 'title' => pht('Quick Create'), + )); + } + + +} diff --git a/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php b/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php index 9c381ed621..62b61fa97d 100644 --- a/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php +++ b/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php @@ -29,11 +29,6 @@ final class PhabricatorApplicationLegalpad extends PhabricatorApplication { return self::GROUP_COMMUNICATION; } - public function getQuickCreateURI() { - return $this->getBaseURI().'create/'; - } - - public function isBeta() { return true; } @@ -48,9 +43,21 @@ final class PhabricatorApplicationLegalpad extends PhabricatorApplication { 'edit/(?P\d+)/' => 'LegalpadDocumentEditController', 'comment/(?P\d+)/' => 'LegalpadDocumentCommentController', 'view/(?P\d+)/' => 'LegalpadDocumentViewController', + 'verify/(?P[^/]+)/' => + 'LegalpadDocumentSignatureVerificationController', + 'signatures/(?P\d+)/' => 'LegalpadDocumentSignatureListController', 'document/' => array( 'preview/' => 'PhabricatorMarkupPreviewController'), )); } + protected function getCustomCapabilities() { + return array( + LegalpadCapabilityDefaultView::CAPABILITY => array( + ), + LegalpadCapabilityDefaultEdit::CAPABILITY => array( + ), + ); + } + } diff --git a/src/applications/legalpad/capability/LegalpadCapabilityDefaultEdit.php b/src/applications/legalpad/capability/LegalpadCapabilityDefaultEdit.php new file mode 100644 index 0000000000..2ea6b73719 --- /dev/null +++ b/src/applications/legalpad/capability/LegalpadCapabilityDefaultEdit.php @@ -0,0 +1,16 @@ +id) { $is_create = true; - $document = id(new LegalpadDocument()) - ->setVersions(0) - ->setCreatorPHID($user->getPHID()) - ->setContributorCount(0) - ->setRecentContributorPHIDs(array()) - ->setViewPolicy(PhabricatorPolicies::POLICY_USER) - ->setEditPolicy(PhabricatorPolicies::POLICY_USER); + $document = LegalpadDocument::initializeNewDocument($user); $body = id(new LegalpadDocumentBody()) ->setCreatorPHID($user->getPHID()); $document->attachDocumentBody($body); @@ -37,6 +31,7 @@ final class LegalpadDocumentEditController extends LegalpadController { $document = id(new LegalpadDocumentQuery()) ->setViewer($user) ->needDocumentBodies(true) + ->needSignatures(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -102,11 +97,7 @@ final class LegalpadDocumentEditController extends LegalpadController { } } - $error_view = null; if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('A Fatal Omission!')) - ->setErrors($errors); // set these to what was specified in the form on post $document->setViewPolicy($can_view); $document->setEditPolicy($can_edit); @@ -151,6 +142,7 @@ final class LegalpadDocumentEditController extends LegalpadController { ->setPolicies($policies) ->setName('can_edit')); + $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $submit = new AphrontFormSubmitControl(); if ($is_create) { $submit->setValue(pht('Create Document')); @@ -162,6 +154,17 @@ final class LegalpadDocumentEditController extends LegalpadController { $this->getApplicationURI('view/'.$document->getID())); $title = pht('Update Document'); $short = pht('Update'); + $signatures = $document->getSignatures(); + if ($signatures) { + $form->appendInstructions(pht( + 'Warning: there are %d signature(s) already for this document. '. + 'Updating the title or text will invalidate these signatures and '. + 'users will need to sign again. Proceed carefully.', + count($signatures))); + } + $crumbs->addTextCrumb( + $document->getMonogram(), + $this->getApplicationURI('view/'.$document->getID())); } $form @@ -169,13 +172,10 @@ final class LegalpadDocumentEditController extends LegalpadController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView())->setName($short)); - + $crumbs->addTextCrumb($short); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index ea6d3ac07a..16361a84f7 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -1,12 +1,13 @@ id = $data['id']; } @@ -25,38 +26,70 @@ final class LegalpadDocumentSignController extends LegalpadController { return new Aphront404Response(); } - $signature = id(new LegalpadDocumentSignature()) - ->loadOneWhere( - 'documentPHID = %s AND documentVersion = %d AND signerPHID = %s', - $document->getPHID(), - $document->getVersions(), - $user->getPHID()); + $signer_phid = null; + $signature = null; + $signature_data = array(); + if ($user->isLoggedIn()) { + $signer_phid = $user->getPHID(); + $signature_data = array( + 'email' => $user->loadPrimaryEmailAddress()); + } else if ($request->isFormPost()) { + $email = new PhutilEmailAddress($request->getStr('email')); + $email_obj = id(new PhabricatorUserEmail()) + ->loadOneWhere('address = %s', $email->getAddress()); + if ($email_obj) { + return $this->signInResponse(); + } + $external_account = id(new PhabricatorExternalAccountQuery()) + ->setViewer($user) + ->withAccountTypes(array('email')) + ->withAccountDomains(array($email->getDomainName())) + ->withAccountIDs(array($email->getAddress())) + ->loadOneOrCreate(); + if ($external_account->getUserPHID()) { + return $this->signInResponse(); + } + $signer_phid = $external_account->getPHID(); + } + + if ($signer_phid) { + $signature = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($user) + ->withDocumentPHIDs(array($document->getPHID())) + ->withSignerPHIDs(array($signer_phid)) + ->withDocumentVersions(array($document->getVersions())) + ->executeOne(); + } if (!$signature) { $has_signed = false; $error_view = null; $signature = id(new LegalpadDocumentSignature()) - ->setSignerPHID($user->getPHID()) + ->setSignerPHID($signer_phid) ->setDocumentPHID($document->getPHID()) - ->setDocumentVersion($document->getVersions()); - $data = array( - 'name' => $user->getRealName(), - 'email' => $user->loadPrimaryEmailAddress()); - $signature->setSignatureData($data); + ->setDocumentVersion($document->getVersions()) + ->setSignatureData($signature_data); } else { $has_signed = true; + if ($signature->isVerified()) { + $title = pht('Already Signed'); + $body = $this->getVerifiedSignatureBlurb(); + } else { + $title = pht('Already Signed but...'); + $body = $this->getUnverifiedSignatureBlurb(); + } $error_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setTitle(pht('Already Signed')) - ->appendChild(pht('Thank you for signing and agreeing')); - $data = $signature->getSignatureData(); + ->setTitle($title) + ->appendChild($body); + $signature_data = $signature->getSignatureData(); } $e_name = true; $e_email = true; $e_address_1 = true; $errors = array(); - if ($request->isFormPost()) { + if ($request->isFormPost() && !$has_signed) { $name = $request->getStr('name'); $email = $request->getStr('email'); $address_1 = $request->getStr('address_1'); @@ -68,8 +101,9 @@ final class LegalpadDocumentSignController extends LegalpadController { $e_name = pht('Required'); $errors[] = pht('Name field is required.'); } - $data['name'] = $name; + $signature_data['name'] = $name; + $addr_obj = null; if (!$email) { $e_email = pht('Required'); $errors[] = pht('Email field is required.'); @@ -81,29 +115,47 @@ final class LegalpadDocumentSignController extends LegalpadController { $errors[] = pht('A valid email is required.'); } } - $data['email'] = $email; + $signature_data['email'] = $email; if (!$address_1) { $e_address_1 = pht('Required'); $errors[] = pht('Address line 1 field is required.'); } - $data['address_1'] = $address_1; - $data['address_2'] = $address_2; - $data['phone'] = $phone; - $signature->setSignatureData($data); + $signature_data['address_1'] = $address_1; + $signature_data['address_2'] = $address_2; + $signature_data['phone'] = $phone; + $signature->setSignatureData($signature_data); if (!$agree) { $errors[] = pht( 'You must check "I agree to the terms laid forth above."'); } + $verified = LegalpadDocumentSignature::UNVERIFIED; + if ($user->isLoggedIn() && $addr_obj) { + $email_obj = id(new PhabricatorUserEmail()) + ->loadOneWhere('address = %s', $addr_obj->getAddress()); + if ($email_obj && $email_obj->getUserPHID() == $user->getPHID()) { + $verified = LegalpadDocumentSignature::VERIFIED; + } + } + $signature->setVerified($verified); + if (!$errors) { $signature->save(); $has_signed = true; + if ($signature->isVerified()) { + $body = $this->getVerifiedSignatureBlurb(); + } else { + $body = $this->getUnverifiedSignatureBlurb(); + $this->sendVerifySignatureEmail( + $document, + $signature); + } $error_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setTitle(pht('Signature successful')) - ->appendChild(pht('Thank you for signing and agreeing')); + ->setTitle(pht('Signature Successful')) + ->appendChild($body); } else { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Error in submission.')) @@ -125,9 +177,10 @@ final class LegalpadDocumentSignController extends LegalpadController { ->setHeader($title); $content = array( - id(new PHUIDocumentView()) - ->setHeader($header) - ->appendChild($this->buildDocument($engine, $document_body)), + $this->buildDocument( + $header, + $engine, + $document_body), $this->buildSignatureForm( $document_body, $signature, @@ -147,10 +200,17 @@ final class LegalpadDocumentSignController extends LegalpadController { } private function buildDocument( - PhabricatorMarkupEngine - $engine, LegalpadDocumentBody $body) { + PHUIHeaderView $header, + PhabricatorMarkupEngine $engine, + LegalpadDocumentBody $body) { - return $engine->getOutput($body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); + $this->requireResource('legalpad-document-css'); + return id(new PHUIDocumentView()) + ->addClass('legalpad') + ->setHeader($header) + ->appendChild($engine->getOutput( + $body, + LegalpadDocumentBody::MARKUP_FIELD_TEXT)); } private function buildSignatureForm( @@ -218,10 +278,61 @@ final class LegalpadDocumentSignController extends LegalpadController { ->setValue(pht('Sign and Agree')) ->setDisabled($has_signed)); - return id(new PHUIObjectBoxView()) + $view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Sign and Agree')) - ->setFormError($error_view) ->setForm($form); + if ($error_view) { + $view->setErrorView($error_view); + } + return $view; + } + + private function getVerifiedSignatureBlurb() { + return pht('Thank you for signing and agreeing.'); + } + + private function getUnverifiedSignatureBlurb() { + return pht('Thank you for signing and agreeing. However, you must '. + 'verify your email address. Please check your email '. + 'and follow the instructions.'); + } + + private function sendVerifySignatureEmail( + LegalpadDocument $doc, + LegalpadDocumentSignature $signature) { + + $signature_data = $signature->getSignatureData(); + $email = new PhutilEmailAddress($signature_data['email']); + $doc_link = PhabricatorEnv::getProductionURI($doc->getMonogram()); + $path = $this->getApplicationURI(sprintf( + '/verify/%s/', + $signature->getSecretKey())); + $link = PhabricatorEnv::getProductionURI($path); + + $body = <<addRawTos(array($email->getAddress())) + ->setSubject(pht('[Legalpad] Signature Verification')) + ->setBody($body) + ->setRelatedPHID($signature->getDocumentPHID()) + ->saveAndSend(); + } + + private function signInResponse() { + return id(new Aphront403Response()) + ->setForbiddenText(pht( + 'The email address specified is associated with an account. '. + 'Please login to that account and sign this document again.')); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php new file mode 100644 index 0000000000..84717af2da --- /dev/null +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php @@ -0,0 +1,122 @@ +documentId = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $document = id(new LegalpadDocumentQuery()) + ->setViewer($user) + ->withIDs(array($this->documentId)) + ->executeOne(); + + if (!$document) { + return new Aphront404Response(); + } + + $title = pht('Signatures for %s', $document->getMonogram()); + + $pager = id(new AphrontCursorPagerView()) + ->readFromRequest($request); + $signatures = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($user) + ->withDocumentPHIDs(array($document->getPHID())) + ->executeWithCursorPager($pager); + + $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); + $crumbs->addTextCrumb( + $document->getMonogram(), + $this->getApplicationURI('view/'.$document->getID())); + + $crumbs->addTextCrumb( + pht('Signatures')); + $list = $this->renderResultsList($document, $signatures); + $list->setPager($pager); + + return $this->buildApplicationPage( + array( + $crumbs, + $list, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + + private function renderResultsList( + LegalpadDocument $document, + array $signatures) { + assert_instances_of($signatures, 'LegalpadDocumentSignature'); + + $user = $this->getRequest()->getUser(); + + $list = new PHUIObjectItemListView(); + $list->setUser($user); + + foreach ($signatures as $signature) { + $created = phabricator_date($signature->getDateCreated(), $user); + + $data = $signature->getSignatureData(); + + $sig_data = phutil_tag( + 'div', + array(), + array( + phutil_tag( + 'div', + array(), + phutil_tag( + 'a', + array( + 'href' => 'mailto:'.$data['email'], + ), + $data['email'])), + phutil_tag( + 'div', + array(), + $data['address_1']), + phutil_tag( + 'div', + array(), + $data['address_2']), + phutil_tag( + 'div', + array(), + $data['phone']) + )); + + $item = id(new PHUIObjectItemView()) + ->setObject($signature) + ->setHeader($data['name']) + ->setSubhead($sig_data) + ->addIcon('none', pht('Signed %s', $created)); + + $good_sig = true; + if (!$signature->isVerified()) { + $item->addFootIcon('disable', 'Unverified Email'); + $good_sig = false; + } + if ($signature->getDocumentVersion() != $document->getVersions()) { + $item->addFootIcon('delete', 'Stale Signature'); + $good_sig = false; + } + + if ($good_sig) { + $item->setBarColor('green'); + } + + $list->addItem($item); + } + + return $list; + } + +} diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php new file mode 100644 index 0000000000..19072f2caf --- /dev/null +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php @@ -0,0 +1,94 @@ +code = $data['code']; + } + + public function shouldRequireEmailVerification() { + return false; + } + + public function shouldRequireLogin() { + return false; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + // this page can be accessed by not logged in users to valid their + // signatures. use the omnipotent user for these cases. + if (!$user->isLoggedIn()) { + $viewer = PhabricatorUser::getOmnipotentUser(); + } else { + $viewer = $user; + } + + $signature = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($viewer) + ->withSecretKeys(array($this->code)) + ->executeOne(); + + if (!$signature) { + $title = pht('Unable to Verify Signature'); + $content = pht( + 'The verification code you provided is incorrect or the signature '. + 'has been removed. '. + 'Make sure you followed the link in the email correctly.'); + $uri = $this->getApplicationURI(); + $continue = pht('Rats!'); + } else { + $document = id(new LegalpadDocumentQuery()) + ->setViewer($user) + ->withPHIDs(array($signature->getDocumentPHID())) + ->executeOne(); + // the document could be deleted or have its permissions changed + // 4oh4 time + if (!$document) { + return new Aphront404Response(); + } + $uri = '/'.$document->getMonogram(); + if ($signature->isVerified()) { + $title = pht('Signature Already Verified'); + $content = pht( + 'This signature has already been verified.'); + $continue = pht('Continue to Legalpad Document'); + } else { + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $signature + ->setVerified(LegalpadDocumentSignature::VERIFIED) + ->save(); + unset($guard); + $title = pht('Signature Verified'); + $content = pht('The signature is now verified.'); + $continue = pht('Continue to Legalpad Document'); + } + } + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle($title) + ->setMethod('GET') + ->addCancelButton($uri, $continue) + ->appendChild($content); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Verify Signature')); + + return $this->buildApplicationPage( + array( + $crumbs, + $dialog, + ), + array( + 'title' => pht('Verify Signature'), + 'device' => true, + )); + } + +} diff --git a/src/applications/legalpad/controller/LegalpadDocumentViewController.php b/src/applications/legalpad/controller/LegalpadDocumentViewController.php index 814508ce26..9a6ce73ab3 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentViewController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentViewController.php @@ -81,10 +81,9 @@ final class LegalpadDocumentViewController extends LegalpadController { $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('L'.$document->getID()) - ->setHref($this->getApplicationURI('view/'.$document->getID()))); + $crumbs->addTextCrumb( + $document->getMonogram(), + $this->getApplicationURI('view/'.$document->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) @@ -111,7 +110,9 @@ final class LegalpadDocumentViewController extends LegalpadController { PhabricatorMarkupEngine $engine, LegalpadDocumentBody $body) { + $this->requireResource('legalpad-document-css'); $view = new PHUIPropertyListView(); + $view->addClass('legalpad'); $view->addSectionHeader(pht('Document')); $view->addTextContent( $engine->getOutput($body, LegalpadDocumentBody::MARKUP_FIELD_TEXT)); @@ -133,14 +134,28 @@ final class LegalpadDocumentViewController extends LegalpadController { $document, PhabricatorPolicyCapability::CAN_EDIT); + $doc_id = $document->getID(); + $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Document')) - ->setHref($this->getApplicationURI('/edit/'.$document->getID().'/')) + ->setHref($this->getApplicationURI('/edit/'.$doc_id.'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('like') + ->setName(pht('Sign Document')) + ->setHref('/'.$document->getMonogram())); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('transcript') + ->setName(pht('View Signatures')) + ->setHref($this->getApplicationURI('/signatures/'.$doc_id.'/'))); + return $actions; } @@ -196,9 +211,6 @@ final class LegalpadDocumentViewController extends LegalpadController { ? pht('Add Comment') : pht('Debate Legislation'); - $header = id(new PHUIHeaderView()) - ->setHeader($title); - $button_name = $is_serious ? pht('Add Comment') : pht('Commence Filibuster'); @@ -207,15 +219,13 @@ final class LegalpadDocumentViewController extends LegalpadController { ->setUser($user) ->setObjectPHID($document->getPHID()) ->setFormID($comment_form_id) + ->setHeaderText($title) ->setDraft($draft) ->setSubmitButtonName($button_name) ->setAction($this->getApplicationURI('/comment/'.$document->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); - return id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($header) - ->appendChild($form); + return $form; } diff --git a/src/applications/legalpad/query/LegalpadDocumentQuery.php b/src/applications/legalpad/query/LegalpadDocumentQuery.php index 22db1808b0..b178f443cb 100644 --- a/src/applications/legalpad/query/LegalpadDocumentQuery.php +++ b/src/applications/legalpad/query/LegalpadDocumentQuery.php @@ -1,8 +1,5 @@ ids = $ids; @@ -36,6 +35,11 @@ final class LegalpadDocumentQuery return $this; } + public function withSignerPHIDs(array $phids) { + $this->signerPHIDs = $phids; + return $this; + } + public function needDocumentBodies($need_bodies) { $this->needDocumentBodies = $need_bodies; return $this; @@ -46,6 +50,11 @@ final class LegalpadDocumentQuery return $this; } + public function needSignatures($need_signatures) { + $this->needSignatures = $need_signatures; + return $this; + } + public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; @@ -75,6 +84,28 @@ final class LegalpadDocumentQuery } protected function willFilterPage(array $documents) { + if ($this->signerPHIDs) { + $document_map = mpull($documents, null, 'getPHID'); + $signatures = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($this->getViewer()) + ->withDocumentPHIDs(array_keys($document_map)) + ->withSignerPHIDs($this->signerPHIDs) + ->execute(); + $signatures = mgroup($signatures, 'getDocumentPHID'); + foreach ($document_map as $document_phid => $document) { + $sigs = idx($signatures, $document_phid, array()); + foreach ($sigs as $index => $sig) { + if ($sig->getDocumentVersion() != $document->getVersions()) { + unset($sigs[$index]); + } + } + $signer_phids = mpull($sigs, 'getSignerPHID'); + if (array_diff($this->signerPHIDs, $signer_phids)) { + unset($documents[$document->getID()]); + } + } + } + if ($this->needDocumentBodies) { $documents = $this->loadDocumentBodies($documents); } @@ -83,6 +114,10 @@ final class LegalpadDocumentQuery $documents = $this->loadContributors($documents); } + if ($this->needSignatures) { + $documents = $this->loadSignatures($documents); + } + return $documents; } @@ -181,6 +216,28 @@ final class LegalpadDocumentQuery return $documents; } + private function loadSignatures(array $documents) { + $document_map = mpull($documents, null, 'getPHID'); + + $signatures = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($this->getViewer()) + ->withDocumentPHIDs(array_keys($document_map)) + ->execute(); + $signatures = mgroup($signatures, 'getDocumentPHID'); + + foreach ($documents as $document) { + $sigs = idx($signatures, $document->getPHID(), array()); + foreach ($sigs as $index => $sig) { + if ($sig->getDocumentVersion() != $document->getVersions()) { + unset($sigs[$index]); + } + } + $document->attachSignatures($sigs); + } + + return $documents; + } + public function getQueryApplicationClass() { return 'PhabricatorApplicationLegalpad'; } diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php b/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php new file mode 100644 index 0000000000..ade76679c3 --- /dev/null +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php @@ -0,0 +1,101 @@ +ids = $ids; + return $this; + } + + public function withDocumentPHIDs(array $phids) { + $this->documentPHIDs = $phids; + return $this; + } + + public function withSignerPHIDs(array $phids) { + $this->signerPHIDs = $phids; + return $this; + } + + public function withDocumentVersions(array $versions) { + $this->documentVersions = $versions; + return $this; + } + + public function withSecretKeys(array $keys) { + $this->secretKeys = $keys; + return $this; + } + + protected function loadPage() { + $table = new LegalpadDocumentSignature(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + $documents = $table->loadAllFromArray($data); + + return $documents; + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + $where[] = $this->buildPagingClause($conn_r); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->documentPHIDs) { + $where[] = qsprintf( + $conn_r, + 'documentPHID IN (%Ls)', + $this->documentPHIDs); + } + + if ($this->signerPHIDs) { + $where[] = qsprintf( + $conn_r, + 'signerPHID IN (%Ls)', + $this->signerPHIDs); + } + + if ($this->documentVersions) { + $where[] = qsprintf( + $conn_r, + 'documentVersion IN (%Ld)', + $this->documentVersions); + } + + if ($this->secretKeys) { + $where[] = qsprintf( + $conn_r, + 'secretKey IN (%Ls)', + $this->secretKeys); + } + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationLegalpad'; + } + +} diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 1b1764acc0..99208d81e1 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -1,8 +1,5 @@ setViewer($actor) + ->withClasses(array('PhabricatorApplicationLegalpad')) + ->executeOne(); + + $view_policy = $app->getPolicy(LegalpadCapabilityDefaultView::CAPABILITY); + $edit_policy = $app->getPolicy(LegalpadCapabilityDefaultEdit::CAPABILITY); + + return id(new LegalpadDocument()) + ->setVersions(0) + ->setCreatorPHID($actor->getPHID()) + ->setContributorCount(0) + ->setRecentContributorPHIDs(array()) + ->attachSignatures(array()) + ->setViewPolicy($view_policy) + ->setEditPolicy($edit_policy); + } public function getConfiguration() { return array( @@ -54,6 +71,15 @@ final class LegalpadDocument extends LegalpadDAO return $this; } + public function getSignatures() { + return $this->assertAttached($this->signatures); + } + + public function attachSignatures(array $signatures) { + $this->signatures = $signatures; + return $this; + } + public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); @@ -61,6 +87,10 @@ final class LegalpadDocument extends LegalpadDAO return parent::save(); } + public function getMonogram() { + return 'L'.$this->getID(); + } + /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { diff --git a/src/applications/legalpad/storage/LegalpadDocumentSignature.php b/src/applications/legalpad/storage/LegalpadDocumentSignature.php index bf04ce7307..5c66b06d19 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentSignature.php +++ b/src/applications/legalpad/storage/LegalpadDocumentSignature.php @@ -1,14 +1,18 @@ getSecretKey()) { + $this->setSecretKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + public function isVerified() { + return $this->getVerified() != self::UNVERIFIED; + } +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::POLICY_USER; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } } diff --git a/src/applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php b/src/applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php index 615b0bbc2d..3be1b6210e 100644 --- a/src/applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php +++ b/src/applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php @@ -61,7 +61,7 @@ final class PhabricatorLipsumGenerateWorkflow echo "\n"; while (true) { $type = $supported_types[array_rand($supported_types)]; - $admin = PhabricatorUser::getOmnipotentUser(); + $admin = $this->getViewer(); try { $taskgen = newv($type, array()); $object = $taskgen->generate(); diff --git a/src/applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php b/src/applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php index a62a7dec63..ea5176403d 100644 --- a/src/applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php +++ b/src/applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php @@ -1,10 +1,6 @@ getBaseURI().'create/'; - } - public function getRoutes() { return array( '/macro/' => array( diff --git a/src/applications/macro/controller/PhabricatorMacroAudioController.php b/src/applications/macro/controller/PhabricatorMacroAudioController.php index 8b4b9cfdb6..287e7f9f03 100644 --- a/src/applications/macro/controller/PhabricatorMacroAudioController.php +++ b/src/applications/macro/controller/PhabricatorMacroAudioController.php @@ -82,14 +82,6 @@ final class PhabricatorMacroAudioController } } - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } else { - $error_view = null; - } - $form = id(new AphrontFormView()) ->addHiddenInput('behaviorForm', 1) ->setUser($viewer); @@ -132,15 +124,8 @@ final class PhabricatorMacroAudioController $title = pht('Edit Audio Behavior'); $crumb = pht('Edit Audio'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($view_uri) - ->setName(pht('Macro "%s"', $macro->getName()))); - - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($request->getRequestURI()) - ->setName($crumb)); + $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); + $crumbs->addTextCrumb($crumb, $request->getRequestURI()); $upload_form = id(new AphrontFormView()) ->setEncType('multipart/form-data') @@ -159,7 +144,7 @@ final class PhabricatorMacroAudioController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/macro/controller/PhabricatorMacroDisableController.php b/src/applications/macro/controller/PhabricatorMacroDisableController.php index 5b4a4f8d5e..293911f50f 100644 --- a/src/applications/macro/controller/PhabricatorMacroDisableController.php +++ b/src/applications/macro/controller/PhabricatorMacroDisableController.php @@ -19,11 +19,6 @@ final class PhabricatorMacroDisableController $macro = id(new PhabricatorMacroQuery()) ->setViewer($user) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) ->withIDs(array($this->id)) ->executeOne(); if (!$macro) { diff --git a/src/applications/macro/controller/PhabricatorMacroEditController.php b/src/applications/macro/controller/PhabricatorMacroEditController.php index decd173560..667170def8 100644 --- a/src/applications/macro/controller/PhabricatorMacroEditController.php +++ b/src/applications/macro/controller/PhabricatorMacroEditController.php @@ -135,14 +135,6 @@ final class PhabricatorMacroEditController } } - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } else { - $error_view = null; - } - $current_file = null; if ($macro->getFilePHID()) { $current_file = $macro->getFile(); @@ -219,19 +211,13 @@ final class PhabricatorMacroEditController $title = pht('Edit Image Macro'); $crumb = pht('Edit Macro'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($view_uri) - ->setName(pht('Macro "%s"', $macro->getName()))); + $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); } else { $title = pht('Create Image Macro'); $crumb = pht('Create Macro'); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($request->getRequestURI()) - ->setName($crumb)); + $crumbs->addTextCrumb($crumb, $request->getRequestURI()); $upload = null; if ($macro->getID()) { @@ -263,7 +249,7 @@ final class PhabricatorMacroEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php b/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php index 4fd9259bb8..307290dd72 100644 --- a/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php +++ b/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php @@ -7,20 +7,20 @@ final class PhabricatorMacroMemeDialogController $request = $this->getRequest(); $user = $request->getUser(); - $name = $request->getStr('macro'); + $phid = head($request->getArr('macro')); $above = $request->getStr('above'); $below = $request->getStr('below'); $e_macro = true; $errors = array(); if ($request->isDialogFormPost()) { - if (!$name) { + if (!$phid) { $e_macro = pht('Required'); $errors[] = pht('Macro name is required.'); } else { $macro = id(new PhabricatorMacroQuery()) ->setViewer($user) - ->withNames(array($name)) + ->withPHIDs(array($phid)) ->executeOne(); if (!$macro) { $e_macro = pht('Invalid'); @@ -31,7 +31,7 @@ final class PhabricatorMacroMemeDialogController if (!$errors) { $options = new PhutilSimpleOptions(); $data = array( - 'src' => $name, + 'src' => $macro->getName(), 'above' => $above, 'below' => $below, ); @@ -46,10 +46,11 @@ final class PhabricatorMacroMemeDialogController $view = id(new PHUIFormLayoutView()) ->appendChild( - id(new AphrontFormTextControl()) + id(new AphrontFormTokenizerControl()) ->setLabel(pht('Macro')) ->setName('macro') - ->setValue($name) + ->setLimit(1) + ->setDatasource('/typeahead/common/macros/') ->setError($e_macro)) ->appendChild( id(new AphrontFormTextControl()) diff --git a/src/applications/macro/controller/PhabricatorMacroViewController.php b/src/applications/macro/controller/PhabricatorMacroViewController.php index dbd7952e00..9faf00f2f9 100644 --- a/src/applications/macro/controller/PhabricatorMacroViewController.php +++ b/src/applications/macro/controller/PhabricatorMacroViewController.php @@ -30,10 +30,9 @@ final class PhabricatorMacroViewController $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($this->getApplicationURI('/view/'.$macro->getID().'/')) - ->setName($title_short)); + $crumbs->addTextCrumb( + $title_short, + $this->getApplicationURI('/view/'.$macro->getID().'/')); $properties = $this->buildPropertyView($macro, $actions); if ($file) { @@ -76,19 +75,17 @@ final class PhabricatorMacroViewController if ($macro->getIsDisabled()) { $header->addTag( - id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) ->setName(pht('Macro Disabled')) - ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK)); + ->setBackgroundColor(PHUITagView::COLOR_BLACK)); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $add_comment_header = id(new PHUIHeaderView()) - ->setHeader( - $is_serious - ? pht('Add Comment') - : pht('Grovel in Awe')); + $comment_header = $is_serious + ? pht('Add Comment') + : pht('Grovel in Awe'); $submit_button_name = $is_serious ? pht('Add Comment') @@ -100,6 +97,7 @@ final class PhabricatorMacroViewController ->setUser($user) ->setObjectPHID($macro->getPHID()) ->setDraft($draft) + ->setHeaderText($comment_header) ->setAction($this->getApplicationURI('/comment/'.$macro->getID().'/')) ->setSubmitButtonName($submit_button_name); @@ -111,17 +109,12 @@ final class PhabricatorMacroViewController $object_box->addPropertyList($file_view); } - $comment_box = id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($add_comment_header) - ->appendChild($add_comment_form); - return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, - $comment_box, + $add_comment_form, ), array( 'title' => $title_short, diff --git a/src/applications/macro/phid/PhabricatorMacroPHIDTypeMacro.php b/src/applications/macro/phid/PhabricatorMacroPHIDTypeMacro.php index b5fbc351b7..106d2c1af7 100644 --- a/src/applications/macro/phid/PhabricatorMacroPHIDTypeMacro.php +++ b/src/applications/macro/phid/PhabricatorMacroPHIDTypeMacro.php @@ -41,8 +41,4 @@ final class PhabricatorMacroPHIDTypeMacro extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 0dd6f02cdc..0bd9e238d8 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -13,6 +13,7 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO protected $isDisabled = 0; protected $audioPHID; protected $audioBehavior = self::AUDIO_BEHAVIOR_NONE; + protected $mailKey; private $file = self::ATTACHABLE; private $audio = self::ATTACHABLE; @@ -50,10 +51,18 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO PhabricatorMacroPHIDTypeMacro::TYPECONST); } - public function isAutomaticallySubscribed($phid) { - return false; + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + public function getApplicationTransactionEditor() { return new PhabricatorMacroEditor(); } @@ -62,6 +71,18 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO return new PhabricatorMacroTransaction(); } + +/* -( PhabricatorSubscribableInterface )----------------------------------- */ + + + public function isAutomaticallySubscribed($phid) { + return false; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, diff --git a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php index 09b24383cf..ccf6e42e18 100644 --- a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php +++ b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php @@ -74,13 +74,6 @@ final class PhabricatorMailingListsEditController } } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($list->getID()) { @@ -117,18 +110,14 @@ final class PhabricatorMailingListsEditController ->addCancelButton($this->getApplicationURI())); if ($list->getID()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Mailing List'))); + $crumbs->addTextCrumb(pht('Edit Mailing List')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Mailing List'))); + $crumbs->addTextCrumb(pht('Create Mailing List')); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php b/src/applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php index ccd14ae2e8..1b6d08c844 100644 --- a/src/applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php +++ b/src/applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php @@ -37,8 +37,4 @@ final class PhabricatorMailingListPHIDTypeList extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php index a6ac1a71f0..e6e98db2ff 100644 --- a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php +++ b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php @@ -86,6 +86,18 @@ final class PhabricatorApplicationManiphest extends PhabricatorApplication { return $status; } + public function getQuickCreateItems(PhabricatorUser $viewer) { + $items = array(); + + $item = id(new PHUIListItemView()) + ->setName(pht('Maniphest Task')) + ->setAppIcon('maniphest-dark') + ->setHref($this->getBaseURI().'task/create/'); + $items[] = $item; + + return $items; + } + protected function getCustomCapabilities() { return array( ManiphestCapabilityDefaultView::CAPABILITY => array( diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index 86df791337..d05870371e 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -55,8 +55,7 @@ final class ManiphestBatchEditController extends ManiphestController { ->setURI('/maniphest/?ids='.$task_ids); } - $handle_phids = mpull($tasks, 'getOwnerPHID'); - $handles = $this->loadViewerHandles($handle_phids); + $handles = ManiphestTaskListView::loadTaskHandles($user, $tasks); $list = new ManiphestTaskListView(); $list->setTasks($tasks); @@ -145,9 +144,7 @@ final class ManiphestBatchEditController extends ManiphestController { $title = pht('Batch Editor'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Batch Edit Tasks')) diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 24eeeb8855..5999a64ed6 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -56,9 +56,7 @@ final class ManiphestReportController extends ManiphestController { $nav->appendChild($core); $nav->setCrumbs( $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Reports')))); + ->addTextCrumb(pht('Reports'))); return $this->buildApplicationPage( $nav, @@ -253,7 +251,7 @@ final class ManiphestReportController extends ManiphestController { "of this project in the past but no longer is, it is not ". "counted at all."); $header = pht("Task Burn Rate for Project %s", $handle->renderLink()); - $caption = hsprintf("

    %s

    ", $inst); + $caption = phutil_tag('p', array(), $inst); } else { $header = pht("Task Burn Rate for All Tasks"); $caption = null; @@ -297,6 +295,7 @@ final class ManiphestReportController extends ManiphestController { $burn_y, ), 'xformat' => 'epoch', + 'yformat' => 'int', )); return array($filter, $chart, $panel); @@ -363,9 +362,9 @@ final class ManiphestReportController extends ManiphestController { $fmt = number_format($delta); if ($delta > 0) { $fmt = '+'.$fmt; - $fmt = hsprintf('%s', $fmt); + $fmt = phutil_tag('span', array('class' => 'red'), $fmt); } else { - $fmt = hsprintf('%s', $fmt); + $fmt = phutil_tag('span', array('class' => 'green'), $fmt); } return array( diff --git a/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php b/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php index ee185362c5..98725245f1 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php @@ -18,9 +18,7 @@ final class ManiphestTaskDescriptionPreviewController ManiphestTask::MARKUP_FIELD_DESCRIPTION, $request->getUser()); - $content = hsprintf( - '
    %s
    ', - $output); + $content = phutil_tag_div('phabricator-remarkup', $output); return id(new AphrontAjaxResponse()) ->setContent($content); diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index dc10ebcf79..1b86a0f22d 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -347,16 +347,18 @@ final class ManiphestTaskDetailController extends ManiphestController { )); } - $comment_header = id(new PHUIHeaderView()) - ->setHeader($is_serious ? pht('Add Comment') : pht('Weigh In')); + $comment_header = $is_serious + ? pht('Add Comment') + : pht('Weigh In'); - $preview_panel = hsprintf( - '
    -
    -
    %s
    -
    -
    ', - pht('Loading preview...')); + $preview_panel = phutil_tag_div( + 'aphront-panel-preview', + phutil_tag( + 'div', + array('id' => 'transaction-preview'), + phutil_tag_div( + 'aphront-panel-preview-loading-text', + pht('Loading preview...')))); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) @@ -367,11 +369,8 @@ final class ManiphestTaskDetailController extends ManiphestController { $object_name = 'T'.$task->getID(); $actions = $this->buildActionView($task); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($object_name) - ->setHref('/'.$object_name)) + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($object_name, '/'.$object_name) ->setActionList($actions); $header = $this->buildHeaderView($task); @@ -382,10 +381,15 @@ final class ManiphestTaskDetailController extends ManiphestController { if (!$user->isLoggedIn()) { // TODO: Eventually, everything should run through this. For now, we're // only using it to get a consistent "Login to Comment" button. - $comment_form = id(new PhabricatorApplicationTransactionCommentView()) + $comment_box = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI()); $preview_panel = null; + } else { + $comment_box = id(new PHUIObjectBoxView()) + ->setFlush(true) + ->setHeaderText($comment_header) + ->appendChild($comment_form); } $object_box = id(new PHUIObjectBoxView()) @@ -396,11 +400,6 @@ final class ManiphestTaskDetailController extends ManiphestController { $object_box->addPropertyList($description); } - $comment_box = id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($comment_header) - ->appendChild($comment_form); - return $this->buildApplicationPage( array( $crumbs, @@ -551,11 +550,52 @@ final class ManiphestTaskDetailController extends ManiphestController { $source)); } - $view->addProperty( - pht('Projects'), - $task->getProjectPHIDs() - ? $this->renderHandlesForPHIDs($task->getProjectPHIDs(), ',') - : phutil_tag('em', array(), pht('None'))); + $project_phids = $task->getProjectPHIDs(); + if ($project_phids) { + // If we end up with real-world projects with many hundreds of columns, it + // might be better to just load all the edges, then load those columns and + // work backward that way, or denormalize this data more. + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs($project_phids) + ->execute(); + $columns = mpull($columns, null, 'getPHID'); + + $column_edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; + $all_column_phids = array_keys($columns); + + $column_edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($task->getPHID())) + ->withEdgeTypes(array($column_edge_type)) + ->withDestinationPHIDs($all_column_phids); + $column_edge_query->execute(); + $in_column_phids = array_fuse($column_edge_query->getDestinationPHIDs()); + + $column_groups = mgroup($columns, 'getProjectPHID'); + + $project_rows = array(); + foreach ($project_phids as $project_phid) { + $row = array(); + + $handle = $this->getHandle($project_phid); + $row[] = $handle->renderLink(); + + $columns = idx($column_groups, $project_phid, array()); + $column = head(array_intersect_key($columns, $in_column_phids)); + if ($column) { + if (!$column->isDefaultColumn()) { + $row[] = pht(' (%s)', $column->getDisplayName()); + } + } + + $project_rows[] = phutil_tag('div', array(), $row); + } + } else { + $project_rows = phutil_tag('em', array(), pht('None')); + } + + $view->addProperty(pht('Projects'), $project_rows); $edge_types = array( PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 4f11f1d1c7..2ec70e142a 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -382,6 +382,8 @@ final class ManiphestTaskEditController extends ManiphestController { $task->setProjectPHIDs($template_task->getProjectPHIDs()); $task->setOwnerPHID($template_task->getOwnerPHID()); $task->setPriority($template_task->getPriority()); + $task->setViewPolicy($template_task->getViewPolicy()); + $task->setEditPolicy($template_task->getEditPolicy()); $template_fields = PhabricatorCustomField::getObjectFields( $template_task, @@ -429,7 +431,6 @@ final class ManiphestTaskEditController extends ManiphestController { if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); - $error_view->setTitle(pht('Form Errors')); } $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); @@ -746,7 +747,7 @@ final class ManiphestTaskEditController extends ManiphestController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($header_name) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) @@ -761,9 +762,12 @@ final class ManiphestTaskEditController extends ManiphestController { } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($header_name)); + + if ($task->getID()) { + $crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID()); + } + + $crumbs->addTextCrumb($header_name); return $this->buildApplicationPage( array( diff --git a/src/applications/maniphest/controller/ManiphestTaskListController.php b/src/applications/maniphest/controller/ManiphestTaskListController.php index 3127b4ea11..c6dd5f0309 100644 --- a/src/applications/maniphest/controller/ManiphestTaskListController.php +++ b/src/applications/maniphest/controller/ManiphestTaskListController.php @@ -47,7 +47,7 @@ final class ManiphestTaskListController $group_parameter = nonempty($query->getParameter('group'), 'priority'); $order_parameter = nonempty($query->getParameter('order'), 'priority'); - $handles = $this->loadTaskHandles($tasks); + $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); $groups = $this->groupTasks( $tasks, $group_parameter, @@ -124,34 +124,6 @@ final class ManiphestTaskListController )); } - private function loadTaskHandles(array $tasks) { - assert_instances_of($tasks, 'ManiphestTask'); - - $phids = array(); - foreach ($tasks as $task) { - $assigned_phid = $task->getOwnerPHID(); - if ($assigned_phid) { - $phids[] = $assigned_phid; - } - $author_phid = $task->getAuthorPHID(); - if ($author_phid) { - $phids[] = $author_phid; - } - foreach ($task->getProjectPHIDs() as $project_phid) { - $phids[] = $project_phid; - } - } - - if (!$phids) { - return array(); - } - - return id(new PhabricatorHandleQuery()) - ->setViewer($this->getRequest()->getUser()) - ->withPHIDs($phids) - ->execute(); - } - private function groupTasks(array $tasks, $group, array $handles) { assert_instances_of($tasks, 'ManiphestTask'); assert_instances_of($handles, 'PhabricatorObjectHandle'); diff --git a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php index af9f38e80d..be8e4c066b 100644 --- a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php +++ b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php @@ -96,6 +96,11 @@ final class ManiphestTransactionPreviewController extends ManiphestController { $transaction->setOldValue($task->getProjectPHIDs()); $transaction->setNewValue($value); break; + case ManiphestTransaction::TYPE_STATUS: + $phids = array(); + $transaction->setOldvalue($task->getStatus()); + $transaction->setNewValue($value); + break; default: $phids = array(); $transaction->setNewValue($value); diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 8c5666b838..7a99c1cb36 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -117,7 +117,26 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_DESCRIPTION: return $object->setDescription($xaction->getNewValue()); case ManiphestTransaction::TYPE_OWNER: - return $object->setOwnerPHID($xaction->getNewValue()); + $phid = $xaction->getNewValue(); + + // Update the "ownerOrdering" column to contain the full name of the + // owner, if the task is assigned. + + $handle = null; + if ($phid) { + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($phid)) + ->executeOne(); + } + + if ($handle) { + $object->setOwnerOrdering($handle->getName()); + } else { + $object->setOwnerOrdering(null); + } + + return $object->setOwnerPHID($phid); case ManiphestTransaction::TYPE_CCS: return $object->setCCPHIDs($xaction->getNewValue()); case ManiphestTransaction::TYPE_PROJECTS: diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index 4207cbd57b..e4026c3e9d 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -401,6 +401,7 @@ final class ManiphestTaskSearchEngine if ($this->requireViewer()->isLoggedIn()) { $names['assigned'] = pht('Assigned'); $names['authored'] = pht('Authored'); + $names['subscribed'] = pht('Subscribed'); } return $names; @@ -422,6 +423,10 @@ final class ManiphestTaskSearchEngine return $query ->setParameter('assignedPHIDs', array($viewer_phid)) ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); + case 'subscribed': + return $query + ->setParameter('subscriberPHIDs', array($viewer_phid)) + ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); case 'open': return $query ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)) diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php index 025362660d..08d587e8b9 100644 --- a/src/applications/maniphest/view/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/ManiphestTaskListView.php @@ -53,20 +53,13 @@ final class ManiphestTaskListView extends ManiphestView { $item->setHref('/T'.$task->getID()); if ($task->getOwnerPHID()) { - $owner = idx($handles, $task->getOwnerPHID()); - // TODO: This should be guaranteed, see T3817. - if ($owner) { - $item->addByline(pht('Assigned: %s', $owner->renderLink())); - } + $owner = $handles[$task->getOwnerPHID()]; + $item->addByline(pht('Assigned: %s', $owner->renderLink())); } $status = $task->getStatus(); if ($status != ManiphestTaskStatus::STATUS_OPEN) { - $item->addFootIcon( - ($status == ManiphestTaskStatus::STATUS_CLOSED_RESOLVED) - ? 'enable-white' - : 'delete-white', - idx($status_map, $status, 'Unknown')); + $item->setDisabled(true); } $item->setBarColor(idx($color_map, $task->getPriority(), 'grey')); @@ -119,4 +112,34 @@ final class ManiphestTaskListView extends ManiphestView { return $list; } + public static function loadTaskHandles( + PhabricatorUser $viewer, + array $tasks) { + assert_instances_of($tasks, 'ManiphestTask'); + + $phids = array(); + foreach ($tasks as $task) { + $assigned_phid = $task->getOwnerPHID(); + if ($assigned_phid) { + $phids[] = $assigned_phid; + } + $author_phid = $task->getAuthorPHID(); + if ($author_phid) { + $phids[] = $author_phid; + } + foreach ($task->getProjectPHIDs() as $project_phid) { + $phids[] = $project_phid; + } + } + + if (!$phids) { + return array(); + } + + return id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->execute(); + } + } diff --git a/src/applications/maniphest/view/ManiphestView.php b/src/applications/maniphest/view/ManiphestView.php index 5065915ef4..e15765507f 100644 --- a/src/applications/maniphest/view/ManiphestView.php +++ b/src/applications/maniphest/view/ManiphestView.php @@ -9,8 +9,8 @@ abstract class ManiphestView extends AphrontView { $status = $task->getStatus(); $status_name = ManiphestTaskStatus::getTaskStatusFullName($status); - return id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) + return id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) ->setName($status_name); } diff --git a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php index 0b756f8532..ba85f5e9de 100644 --- a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php +++ b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php @@ -24,9 +24,7 @@ final class PhabricatorApplicationDetailViewController $title = $selected->getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($selected->getName())); + $crumbs->addTextCrumb($selected->getName()); $header = id(new PHUIHeaderView()) ->setHeader($title) diff --git a/src/applications/meta/controller/PhabricatorApplicationEditController.php b/src/applications/meta/controller/PhabricatorApplicationEditController.php index f18663ce27..aa4952205b 100644 --- a/src/applications/meta/controller/PhabricatorApplicationEditController.php +++ b/src/applications/meta/controller/PhabricatorApplicationEditController.php @@ -146,13 +146,8 @@ final class PhabricatorApplicationEditController ->addCancelButton($view_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($application->getName()) - ->setHref($view_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Policies'))); + $crumbs->addTextCrumb($application->getName(), $view_uri); + $crumbs->addTextCrumb(pht('Edit Policies')); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Policies: %s', $application->getName())); diff --git a/src/applications/meta/phid/PhabricatorApplicationPHIDTypeApplication.php b/src/applications/meta/phid/PhabricatorApplicationPHIDTypeApplication.php index 718557266f..6ce6c27c6c 100644 --- a/src/applications/meta/phid/PhabricatorApplicationPHIDTypeApplication.php +++ b/src/applications/meta/phid/PhabricatorApplicationPHIDTypeApplication.php @@ -38,8 +38,4 @@ final class PhabricatorApplicationPHIDTypeApplication } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/meta/view/PhabricatorApplicationLaunchView.php b/src/applications/meta/view/PhabricatorApplicationLaunchView.php index 97c79bc9be..ed27d64d5f 100644 --- a/src/applications/meta/view/PhabricatorApplicationLaunchView.php +++ b/src/applications/meta/view/PhabricatorApplicationLaunchView.php @@ -125,31 +125,9 @@ final class PhabricatorApplicationLaunchView extends AphrontView { 'style' => nonempty(implode('; ', $styles), null), ), ''); - - $classes = array(); - if ($application->getQuickCreateURI()) { - $classes[] = 'phabricator-application-create-icon'; - $classes[] = 'sprite-icons'; - $classes[] = 'icons-new-white'; - $plus_icon = phutil_tag( - 'span', - array( - 'class' => implode(' ', $classes), - ), - ''); - - $create_button = phutil_tag( - 'a', - array( - 'href' => $application->getQuickCreateURI(), - 'class' => 'phabricator-application-launch-create', - ), - $plus_icon); - $classes = array(); - $classes[] = 'application-tile-create'; - } } + $classes = array(); $classes[] = 'phabricator-application-launch-container'; if ($this->fullWidth) { $classes[] = 'application-tile-full'; diff --git a/src/applications/metamta/PhabricatorMetaMTAWorker.php b/src/applications/metamta/PhabricatorMetaMTAWorker.php index 50e0b38230..d9defcd8df 100644 --- a/src/applications/metamta/PhabricatorMetaMTAWorker.php +++ b/src/applications/metamta/PhabricatorMetaMTAWorker.php @@ -3,45 +3,42 @@ final class PhabricatorMetaMTAWorker extends PhabricatorWorker { - private $message; + public function getMaximumRetryCount() { + return 250; + } public function getWaitBeforeRetry(PhabricatorWorkerTask $task) { - $message = $this->loadMessage(); - if (!$message) { - return null; - } - - $wait = max($message->getNextRetry() - time(), 0) + 15; - return $wait; + return ($task->getFailureCount() * 15); } public function doWork() { $message = $this->loadMessage(); - if (!$message - || $message->getStatus() != PhabricatorMetaMTAMail::STATUS_QUEUE) { + if (!$message) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Unable to load message!')); + } + + if ($message->getStatus() != PhabricatorMetaMTAMail::STATUS_QUEUE) { return; } + $id = $message->getID(); $message->sendNow(); + // task failed if the message is still queued // (instead of sent, void, or failed) if ($message->getStatus() == PhabricatorMetaMTAMail::STATUS_QUEUE) { - throw new Exception('Failed to send message'); + throw new Exception( + pht('Failed to send message.')); } } private function loadMessage() { - if (!$this->message) { - $message_id = $this->getTaskData(); - $this->message = id(new PhabricatorMetaMTAMail())->load($message_id); - if (!$this->message) { - return null; - } - } - return $this->message; + $message_id = $this->getTaskData(); + return id(new PhabricatorMetaMTAMail())->load($message_id); } - public function renderForDisplay() { + public function renderForDisplay(PhabricatorUser $viewer) { return phutil_tag( 'pre', array( diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php new file mode 100644 index 0000000000..030963e150 --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php @@ -0,0 +1,122 @@ +params['from'] = $email; + $this->params['from-name'] = $name; + return $this; + } + + public function addReplyTo($email, $name = '') { + if (empty($this->params['reply-to'])) { + $this->params['reply-to'] = array(); + } + $this->params['reply-to'][] = array( + 'email' => $email, + 'name' => $name, + ); + return $this; + } + + public function addTos(array $emails) { + foreach ($emails as $email) { + $this->params['tos'][] = $email; + } + return $this; + } + + public function addCCs(array $emails) { + foreach ($emails as $email) { + $this->params['ccs'][] = $email; + } + return $this; + } + + public function addAttachment($data, $filename, $mimetype) { + // TODO: implement attachments. Requires changes in HTTPSFuture + throw new Exception( + "Mailgun adapter does not currently support attachments."); + } + + public function addHeader($header_name, $header_value) { + $this->params['headers'][] = array($header_name, $header_value); + return $this; + } + + public function setBody($body) { + $this->params['body'] = $body; + return $this; + } + + public function setSubject($subject) { + $this->params['subject'] = $subject; + return $this; + } + + public function setIsHTML($is_html) { + $this->params['is-html'] = $is_html; + return $this; + } + + public function supportsMessageIDHeader() { + return false; + } + + public function send() { + $key = PhabricatorEnv::getEnvConfig('mailgun.api-key'); + $domain = PhabricatorEnv::getEnvConfig('mailgun.domain'); + $params = array(); + + $params['to'] = idx($this->params, 'tos', array()); + $params['subject'] = idx($this->params, 'subject'); + + if (idx($this->params, 'is-html')) { + $params['html'] = idx($this->params, 'body'); + } else { + $params['text'] = idx($this->params, 'body'); + } + + $from = idx($this->params, 'from'); + if (idx($this->params, 'from-name')) { + $params['from'] = "{$this->params['from-name']} <{$from}>"; + } else { + $params['from'] = $from; + } + + if (idx($this->params, 'reply-to')) { + $replyto = $this->params['reply-to']; + $params['h:reply-to'] = $replyto; + } + + if (idx($this->params, 'ccs')) { + $params['cc'] = $this->params['ccs']; + } + + $future = new HTTPSFuture( + "https://api:{$key}@api.mailgun.net/v2/{$domain}/messages", + $params); + $future->setMethod('POST'); + + list($body) = $future->resolvex(); + + $response = json_decode($body, true); + if (!is_array($response)) { + throw new Exception("Failed to JSON decode response: {$body}"); + } + + if (!idx($response, 'id')) { + $message = $response['message']; + throw new Exception("Request failed with errors: {$message}."); + } + + return true; + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php index 6581e56753..3af3fbb1e0 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php @@ -33,6 +33,7 @@ final class PhabricatorMailImplementationPHPMailerAdapter $protocol = PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'); if ($protocol) { + $protocol = phutil_utf8_strtolower($protocol); $this->mailer->SMTPSecure = $protocol; } } else if ($mailer == 'sendmail') { diff --git a/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php b/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php index d993aedfd9..844eca2ec0 100644 --- a/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php +++ b/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php @@ -34,6 +34,7 @@ final class PhabricatorApplicationMetaMTA extends PhabricatorApplication { return array( $this->getBaseURI() => array( 'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController', + 'mailgun/' => 'PhabricatorMetaMTAMailgunReceiveController', ), ); } diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php index 21d2caa0e4..8c062ba880 100644 --- a/src/applications/metamta/contentsource/PhabricatorContentSource.php +++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php @@ -9,6 +9,7 @@ final class PhabricatorContentSource { const SOURCE_MOBILE = 'mobile'; const SOURCE_TABLET = 'tablet'; const SOURCE_FAX = 'fax'; + const SOURCE_CONSOLE = 'console'; const SOURCE_LEGACY = 'legacy'; private $source; @@ -39,6 +40,12 @@ final class PhabricatorContentSource { return $obj; } + public static function newConsoleSource() { + return self::newForSource( + PhabricatorContentSource::SOURCE_CONSOLE, + array()); + } + public static function newFromRequest(AphrontRequest $request) { return self::newForSource( PhabricatorContentSource::SOURCE_WEB, @@ -61,6 +68,7 @@ final class PhabricatorContentSource { self::SOURCE_MOBILE => pht('Mobile'), self::SOURCE_TABLET => pht('Tablet'), self::SOURCE_FAX => pht('Fax'), + self::SOURCE_CONSOLE => pht('Console'), self::SOURCE_LEGACY => pht('Legacy'), self::SOURCE_UNKNOWN => pht('Other'), ); diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php new file mode 100644 index 0000000000..d6bd298504 --- /dev/null +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php @@ -0,0 +1,76 @@ +getRequest(); + $timestamp = $request->getStr('timestamp'); + $token = $request->getStr('token'); + $sig = $request->getStr('signature'); + return hash_hmac('sha256', $timestamp.$token, $api_key) == $sig; + + } + public function processRequest() { + + // No CSRF for Mailgun. + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + if (!$this->verifyMessage()) { + throw new Exception( + 'Mail signature is not valid. Check your Mailgun API key.'); + } + + $request = $this->getRequest(); + $user = $request->getUser(); + + $raw_headers = $request->getStr('headers'); + $raw_headers = explode("\n", rtrim($raw_headers)); + $raw_dict = array(); + foreach (array_filter($raw_headers) as $header) { + list($name, $value) = explode(':', $header, 2); + $raw_dict[$name] = ltrim($value); + } + + $headers = array( + 'to' => $request->getStr('recipient'), + 'from' => $request->getStr('from'), + 'subject' => $request->getStr('subject'), + ) + $raw_dict; + + $received = new PhabricatorMetaMTAReceivedMail(); + $received->setHeaders($headers); + $received->setBodies(array( + 'text' => $request->getStr('stripped-text'), + 'html' => $request->getStr('stripped-html'), + )); + + $file_phids = array(); + foreach ($_FILES as $file_raw) { + try { + $file = PhabricatorFile::newFromPHPUpload( + $file_raw, + array( + 'authorPHID' => $user->getPHID(), + )); + $file_phids[] = $file->getPHID(); + } catch (Exception $ex) { + phlog($ex); + } + } + $received->setAttachments($file_phids); + $received->save(); + + $received->processReceivedMail(); + + $response = new AphrontWebpageResponse(); + $response->setContent(pht("Got it! Thanks, Mailgun!\n")); + return $response; + } + +} diff --git a/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php index 6edc66e722..e103513422 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php @@ -1,7 +1,7 @@ getViewer(); $mails = id(new PhabricatorMetaMTAReceivedMail())->loadAllWhere( '1 = 1 ORDER BY id DESC LIMIT %d', diff --git a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php index 0d0a9a59bc..6d84d15669 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php @@ -1,7 +1,7 @@ getViewer(); $mails = id(new PhabricatorMetaMTAMail())->loadAllWhere( '1 = 1 ORDER BY id DESC LIMIT %d', diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php index 9e601b9264..22eb00e419 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php @@ -1,7 +1,7 @@ setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withUsernames(array($as)) ->executeOne(); if (!$user) { diff --git a/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php index ff9e0559a5..1a4ef6ba70 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php @@ -1,7 +1,7 @@ setStatus(PhabricatorMetaMTAMail::STATUS_QUEUE); - $message->setRetryCount(0); - $message->setNextRetry(time()); - $message->save(); $mailer_task = PhabricatorWorker::scheduleTask( diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php index a61a785e33..ac18387ac7 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php @@ -1,7 +1,7 @@ getViewer(); $from = $args->getArg('from'); if ($from) { diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php index 5226bd2dc2..138436a66e 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php @@ -1,7 +1,7 @@ getHeaders() as $key => $value) { + if (is_array($value)) { + $value = implode("\n", $value); + } $info[] = pht('%s: %s', $key, $value); } diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php index 8ab0a973e4..31989fa261 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -1,7 +1,7 @@ getID()); $info[] = pht('Status: %s', $message->getStatus()); - $info[] = pht('Retry Count: %s', $message->getRetryCount()); - $info[] = pht('Next Retry: %s', $message->getNextRetry()); $info[] = pht('Related PHID: %s', $message->getRelatedPHID()); $info[] = pht('Message: %s', $message->getMessage()); diff --git a/src/applications/metamta/management/PhabricatorMailManagementWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementWorkflow.php index ddd4811ae7..0b4662fb54 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementWorkflow.php @@ -1,10 +1,6 @@ ?\s*On\b.*\bwrote:.*?/msU', - '', - $body); + + // Look for "On , wrote:". This may be split across multiple + // lines. We need to be careful not to remove all of a message like this: + // + // On which day do you want to meet? + // + // On , wrote: + // > Let's set up a meeting. + + $start = null; + $lines = phutil_split_lines($body); + foreach ($lines as $key => $line) { + if (preg_match('/^\s*>?\s*On\b/', $line)) { + $start = $key; + } + if ($start !== null) { + if (preg_match('/\bwrote:/', $line)) { + $lines = array_slice($lines, 0, $start); + $body = implode('', $lines); + break; + } + } + } // Outlook english $body = preg_replace( diff --git a/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php b/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php index d4361c6e16..86cc547b4e 100644 --- a/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php +++ b/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php @@ -31,6 +31,20 @@ final class PhabricatorMetaMTAEmailBodyParserTestCase } } + public function testFalsePositiveForOnWrote() { + $body = << Hey bro do you want to go ride horses tomorrow? +EOEMAIL; + + $parser = new PhabricatorMetaMTAEmailBodyParser(); + $stripped = $parser->stripTextBody($body); + $this->assertEqual("On which horse shall you ride?", $stripped); + } + private function getEmailBodiesWithFullCommands() { $bodies = $this->getEmailBodies(); $with_commands = array(); diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php index 2e893dc93f..ad94b9ff31 100644 --- a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php @@ -82,6 +82,11 @@ final class PhabricatorMetaMTAActorQuery extends PhabricatorQuery { $actor->setUndeliverable( pht('This user is a bot; bot accounts do not receive mail.')); } + + // NOTE: We do send email to unapproved users, and to unverified users, + // because it would otherwise be impossible to get them to verify their + // email addresses. Possibly we should white-list this kind of mail and + // deny all other types of mail. } $email = idx($emails, $phid); diff --git a/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php new file mode 100644 index 0000000000..b0be1ab34e --- /dev/null +++ b/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php @@ -0,0 +1,69 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function execute() { + $phids = array_fuse($this->phids); + $actors = array(); + $type_map = array(); + foreach ($phids as $phid) { + $type_map[phid_get_type($phid)][] = $phid; + } + + // TODO: Generalize this somewhere else. + + $results = array(); + foreach ($type_map as $type => $phids) { + switch ($type) { + case PhabricatorProjectPHIDTypeProject::TYPECONST: + // TODO: For now, project members are always on the "mailing list" + // implied by the project, but we should differentiate members and + // subscribers (i.e., allow you to unsubscribe from mail about + // a project). + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->needMembers(true) + ->withPHIDs($phids) + ->execute(); + + $projects = mpull($projects, null, 'getPHID'); + foreach ($phids as $phid) { + $project = idx($projects, $phid); + if (!$project) { + $results[$phid] = array(); + } else { + $results[$phid] = $project->getMemberPHIDs(); + } + } + break; + default: + break; + } + } + + return $results; + } + +} diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php index d621497ec1..fd74d391da 100644 --- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php @@ -19,16 +19,13 @@ abstract class PhabricatorMailReceiver { PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { - if ($sender->getIsDisabled()) { + if (!$sender->isUserActivated()) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER, pht( - "Sender '%s' has a disabled user account.", + "Sender '%s' does not have an activated user account.", $sender->getUsername())); } - - - return; } /** @@ -83,20 +80,13 @@ abstract class PhabricatorMailReceiver { $email_key = 'phabricator.allow-email-users'; $allow_email_users = PhabricatorEnv::getEnvConfig($email_key); if ($allow_email_users) { - $xuser = id(new PhabricatorExternalAccount())->loadOneWhere( - 'accountType = %s AND accountDomain = %s and accountID = %s', - 'email', - 'self', - $from); - if (!$xuser) { - $xuser = id(new PhabricatorExternalAccount()) - ->setAccountID($from) - ->setAccountType('email') - ->setAccountDomain('self') - ->setDisplayName($from) - ->setEmail($from) - ->save(); - } + $from_obj = new PhutilEmailAddress($from); + $xuser = id(new PhabricatorExternalAccountQuery()) + ->setViewer($user) + ->withAccountTypes(array('email')) + ->withAccountDomains(array($from_obj->getDomainName(), 'self')) + ->withAccountIDs(array($from_obj->getAddress())) + ->loadOneOrCreate(); return $xuser->getPhabricatorUser(); } else { $reasons[] = pht( diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php index 9202e710f5..8c88e4e1f5 100644 --- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php @@ -156,7 +156,7 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { '(?P\w+)'. '\\+'. '(?P[a-f0-9]{16})'. - '$)U'; + '$)Ui'; return $regexp; } @@ -166,7 +166,9 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { PhabricatorUser $sender) { $parts = $this->matchObjectAddressInMail($mail); - return $this->loadObject($parts['pattern'], $sender); + return $this->loadObject( + phutil_utf8_strtoupper($parts['pattern']), + $sender); } public static function computeMailHash($mail_key, $phid) { diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index 4278ce2bc6..82adabbf05 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -198,6 +198,15 @@ EOBODY; } } + // TODO: This is pretty messy. We should really be doing all of this + // multiplexing in the task queue, but that requires significant rewriting + // in the general case. ApplicationTransactions can do it fairly easily, + // but other mail sites currently can not, so we need to support this + // junky version until they catch up and we can swap things over. + + $to_handles = $this->expandRecipientHandles($to_handles); + $cc_handles = $this->expandRecipientHandles($cc_handles); + $tos = mpull($to_handles, null, 'getPHID'); $ccs = mpull($cc_handles, null, 'getPHID'); @@ -219,6 +228,8 @@ EOBODY; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); foreach ($recipients as $phid => $recipient) { + + $mail = clone $mail_template; if (isset($to_handles[$phid])) { $mail->addTos(array($phid)); @@ -332,4 +343,32 @@ EOBODY; return rtrim($body); } + private function expandRecipientHandles(array $handles) { + if (!$handles) { + return array(); + } + + $phids = mpull($handles, 'getPHID'); + $map = id(new PhabricatorMetaMTAMemberQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($phids) + ->execute(); + + $results = array(); + foreach ($phids as $phid) { + if (isset($map[$phid])) { + foreach ($map[$phid] as $expanded_phid) { + $results[$expanded_phid] = $expanded_phid; + } + } else { + $results[$phid] = $phid; + } + } + + return id(new PhabricatorHandleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($results) + ->execute(); + } + } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 49e90a3513..160ba28180 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -1,8 +1,6 @@ status = self::STATUS_QUEUE; - $this->retryCount = 0; - $this->nextRetry = time(); $this->parameters = array(); parent::__construct(); @@ -228,15 +222,6 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { return $this; } - public function getSimulatedFailureCount() { - return nonempty($this->getParam('simulated-failures'), 0); - } - - public function setSimulatedFailureCount($count) { - $this->setParam('simulated-failures', $count); - return $this; - } - public function getWorkerTaskID() { return $this->getParam('worker-task'); } @@ -289,25 +274,13 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { } /** - * Save a newly created mail to the database and attempt to send it - * immediately if the server is configured for immediate sends. When - * applications generate new mail they should generally use this method to - * deliver it. If the server doesn't use immediate sends, this has the same - * effect as calling save(): the mail will eventually be delivered by the - * MetaMTA daemon. + * Save a newly created mail to the database. The mail will eventually be + * delivered by the MetaMTA daemon. * * @return this */ public function saveAndSend() { - $ret = null; - - if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) { - $ret = $this->sendNow(); - } else { - $ret = $this->save(); - } - - return $ret; + return $this->save(); } protected function didWriteData() { @@ -351,10 +324,6 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { if ($this->getStatus() != self::STATUS_QUEUE) { throw new Exception("Trying to send an already-sent mail!"); } - - if (time() < $this->getNextRetry()) { - throw new Exception("Trying to send an email before next retry!"); - } } try { @@ -411,7 +380,8 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { $mailer->addReplyTo($value, $reply_to_name); break; case 'to': - $to_actors = array_select_keys($deliverable_actors, $value); + $to_phids = $this->expandRecipients($value); + $to_actors = array_select_keys($deliverable_actors, $to_phids); $add_to = array_merge( $add_to, mpull($to_actors, 'getEmailAddress')); @@ -420,7 +390,8 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { $add_to = array_merge($add_to, $value); break; case 'cc': - $cc_actors = array_select_keys($deliverable_actors, $value); + $cc_phids = $this->expandRecipients($value); + $cc_actors = array_select_keys($deliverable_actors, $cc_phids); $add_cc = array_merge( $add_cc, mpull($cc_actors, 'getEmailAddress')); @@ -623,32 +594,20 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { return $this->save(); } - if ($this->getRetryCount() < $this->getSimulatedFailureCount()) { + try { + $ok = $mailer->send(); + $error = null; + } catch (PhabricatorMetaMTAPermanentFailureException $ex) { + $this->setStatus(self::STATUS_FAIL); + $this->setMessage($ex->getMessage()); + return $this->save(); + } catch (Exception $ex) { $ok = false; - $error = 'Simulated failure.'; - } else { - try { - $ok = $mailer->send(); - $error = null; - } catch (PhabricatorMetaMTAPermanentFailureException $ex) { - $this->setStatus(self::STATUS_FAIL); - $this->setMessage($ex->getMessage()); - return $this->save(); - } catch (Exception $ex) { - $ok = false; - $error = $ex->getMessage()."\n".$ex->getTraceAsString(); - } + $error = $ex->getMessage()."\n".$ex->getTraceAsString(); } if (!$ok) { $this->setMessage($error); - if ($this->getRetryCount() > self::MAX_RETRIES) { - $this->setStatus(self::STATUS_FAIL); - } else { - $this->setRetryCount($this->getRetryCount() + 1); - $next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY); - $this->setNextRetry($next_retry); - } } else { $this->setStatus(self::STATUS_SENT); } @@ -729,9 +688,54 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { $this->getToPHIDs(), $this->getCcPHIDs()); + $this->loadRecipientExpansions($actor_phids); + $actor_phids = $this->expandRecipients($actor_phids); + return $this->loadActors($actor_phids); } + private function loadRecipientExpansions(array $phids) { + $expansions = id(new PhabricatorMetaMTAMemberQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($phids) + ->execute(); + + $this->recipientExpansionMap = $expansions; + + return $this; + } + + /** + * Expand a list of recipient PHIDs (possibly including aggregate recipients + * like projects) into a deaggregated list of individual recipient PHIDs. + * For example, this will expand project PHIDs into a list of the project's + * members. + * + * @param list List of recipient PHIDs, possibly including aggregate + * recipients. + * @return list Deaggregated list of mailable recipients. + */ + private function expandRecipients(array $phids) { + if ($this->recipientExpansionMap === null) { + throw new Exception( + pht( + 'Call loadRecipientExpansions() before expandRecipients()!')); + } + + $results = array(); + foreach ($phids as $phid) { + if (!isset($this->recipientExpansionMap[$phid])) { + $results[$phid] = $phid; + } else { + foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) { + $results[$recipient_phid] = $recipient_phid; + } + } + } + + return array_keys($results); + } + private function filterDeliverableActors(array $actors) { assert_instances_of($actors, 'PhabricatorMetaMTAActor'); $deliverable_actors = array(); diff --git a/src/applications/notification/builder/PhabricatorNotificationBuilder.php b/src/applications/notification/builder/PhabricatorNotificationBuilder.php index c0687799f8..c3fa3d5426 100644 --- a/src/applications/notification/builder/PhabricatorNotificationBuilder.php +++ b/src/applications/notification/builder/PhabricatorNotificationBuilder.php @@ -3,11 +3,17 @@ final class PhabricatorNotificationBuilder { private $stories; + private $user = null; public function __construct(array $stories) { $this->stories = $stories; } + public function setUser($user) { + $this->user = $user; + return $this; + } + public function buildView() { $stories = $this->stories; @@ -124,8 +130,7 @@ final class PhabricatorNotificationBuilder { foreach ($stories as $story) { $view = $story->renderView(); - - $null_view->appendChild($view->renderNotification()); + $null_view->appendChild($view->renderNotification($this->user)); } return $null_view; diff --git a/src/applications/notification/controller/PhabricatorNotificationClearController.php b/src/applications/notification/controller/PhabricatorNotificationClearController.php index 2b7f9a59ec..06a99d9f76 100644 --- a/src/applications/notification/controller/PhabricatorNotificationClearController.php +++ b/src/applications/notification/controller/PhabricatorNotificationClearController.php @@ -38,7 +38,7 @@ final class PhabricatorNotificationClearController } $dialog->addCancelButton('/notification/'); - $dialog->addSubmitButton('Mark All Read'); + $dialog->addSubmitButton(pht('Mark All Read')); return id(new AphrontDialogResponse())->setDialog($dialog); } diff --git a/src/applications/notification/controller/PhabricatorNotificationController.php b/src/applications/notification/controller/PhabricatorNotificationController.php index 8bf67bfae1..576625eab8 100644 --- a/src/applications/notification/controller/PhabricatorNotificationController.php +++ b/src/applications/notification/controller/PhabricatorNotificationController.php @@ -7,7 +7,7 @@ abstract class PhabricatorNotificationController $page = $this->buildStandardPageView(); - $page->setApplicationName('Notification'); + $page->setApplicationName(pht('Notification')); $page->setBaseURI('/notification/'); $page->setTitle(idx($data, 'title')); $page->setGlyph('!'); diff --git a/src/applications/notification/controller/PhabricatorNotificationListController.php b/src/applications/notification/controller/PhabricatorNotificationListController.php index 1557c8acc8..e9cb0f2076 100644 --- a/src/applications/notification/controller/PhabricatorNotificationListController.php +++ b/src/applications/notification/controller/PhabricatorNotificationListController.php @@ -15,8 +15,8 @@ final class PhabricatorNotificationListController $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/notification/')); - $nav->addFilter('all', 'All Notifications'); - $nav->addFilter('unread', 'Unread Notifications'); + $nav->addFilter('all', pht('All Notifications')); + $nav->addFilter('unread', pht('Unread Notifications')); $filter = $nav->selectFilter($this->filter, 'all'); $pager = new AphrontPagerView(); @@ -43,38 +43,46 @@ final class PhabricatorNotificationListController if ($notifications) { $builder = new PhabricatorNotificationBuilder($notifications); + $builder->setUser($user); $view = $builder->buildView()->render(); } else { - $view = hsprintf( - '
    %s
    ', + $view = phutil_tag_div( + 'phabricator-notification no-notifications', $no_data); } - $view = hsprintf( - '
    %s
    ', - $view); + $view = id(new PHUIBoxView()) + ->addPadding(PHUI::PADDING_MEDIUM) + ->addClass('phabricator-notification-list') + ->appendChild($view); - $panel = new AphrontPanelView(); - $panel->setHeader($header); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); - $panel->addButton( - javelin_tag( - 'a', - array( - 'href' => '/notification/clear/', - 'class' => 'button', - 'sigil' => 'workflow', - ), - 'Mark All Read')); - $panel->appendChild($view); - $panel->appendChild($pager); + $image = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('preview'); + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor(PHUIButtonView::SIMPLE) + ->setHref('/notification/clear/') + ->addSigil('workflow') + ->setIcon($image) + ->setText(pht('Mark All Read')); - $nav->appendChild($panel); + $notif_header = id(new PHUIHeaderView()) + ->setHeader($header) + ->addActionLink($button); - return $this->buildStandardPageResponse( + $box = id(new PHUIObjectBoxView()) + ->setHeader($notif_header) + ->appendChild($view); + + $nav->appendChild($box); + $nav->appendChild($pager); + + return $this->buildApplicationPage( $nav, array( - 'title' => 'Notifications', + 'title' => pht('Notifications'), + 'device' => true, )); } diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index 567aec38d5..98328bc791 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -20,8 +20,8 @@ final class PhabricatorNotificationPanelController $notifications_view = $builder->buildView(); $content = $notifications_view->render(); } else { - $content = hsprintf( - '
    %s
    ', + $content = phutil_tag_div( + 'phabricator-notification no-notifications', pht('You have no notifications.')); } @@ -36,7 +36,7 @@ final class PhabricatorNotificationPanelController array( 'href' => '/notification/', ), - 'View All Notifications')); + pht('View All Notifications'))); $unread_count = id(new PhabricatorFeedStoryNotification()) ->countUnread($user); diff --git a/src/applications/nuance/application/PhabricatorApplicationNuance.php b/src/applications/nuance/application/PhabricatorApplicationNuance.php index 7d70973d1e..b2d123ee2e 100644 --- a/src/applications/nuance/application/PhabricatorApplicationNuance.php +++ b/src/applications/nuance/application/PhabricatorApplicationNuance.php @@ -23,6 +23,10 @@ final class PhabricatorApplicationNuance extends PhabricatorApplication { return true; } + public function getBaseURI() { + return '/nuance/'; + } + public function getRoutes() { return array( '/nuance/' => array( @@ -50,5 +54,20 @@ final class PhabricatorApplicationNuance extends PhabricatorApplication { ); } + protected function getCustomCapabilities() { + return array( + NuanceCapabilitySourceDefaultView::CAPABILITY => array( + 'caption' => pht( + 'Default view policy for newly created sources.'), + ), + NuanceCapabilitySourceDefaultEdit::CAPABILITY => array( + 'caption' => pht( + 'Default edit policy for newly created sources.'), + ), + NuanceCapabilitySourceManage::CAPABILITY => array( + ), + ); + } + } diff --git a/src/applications/nuance/capability/NuanceCapabilitySourceDefaultEdit.php b/src/applications/nuance/capability/NuanceCapabilitySourceDefaultEdit.php new file mode 100644 index 0000000000..8dcf6e4c03 --- /dev/null +++ b/src/applications/nuance/capability/NuanceCapabilitySourceDefaultEdit.php @@ -0,0 +1,16 @@ + 'required string', + 'sourcePHID' => 'required string', + 'ownerPHID' => 'optional string', + ); + } + + public function defineReturnType() { + return 'nonempty dict'; + } + + public function defineErrorTypes() { + return array( + 'ERR-NO-REQUESTOR-PHID' => pht('Items must have a requestor.'), + 'ERR-NO-SOURCE-PHID' => pht('Items must have a source.'), + ); + } + + protected function execute(ConduitAPIRequest $request) { + $source_phid = $request->getValue('sourcePHID'); + $owner_phid = $request->getValue('ownerPHID'); + $requestor_phid = $request->getValue('requestorPHID'); + + $user = $request->getUser(); + + $item = NuanceItem::initializeNewItem($user); + $xactions = array(); + + if ($source_phid) { + $xactions[] = id(new NuanceItemTransaction()) + ->setTransactionType(NuanceItemTransaction::TYPE_SOURCE) + ->setNewValue($source_phid); + } else { + throw new ConduitException('ERR-NO-SOURCE-PHID'); + } + + if ($owner_phid) { + $xactions[] = id(new NuanceItemTransaction()) + ->setTransactionType(NuanceItemTransaction::TYPE_OWNER) + ->setNewValue($owner_phid); + } + + if ($requestor_phid) { + $xactions[] = id(new NuanceItemTransaction()) + ->setTransactionType(NuanceItemTransaction::TYPE_REQUESTOR) + ->setNewValue($requestor_phid); + } else { + throw new ConduitException('ERR-NO-REQUESTOR-PHID'); + } + + $source = PhabricatorContentSource::newFromConduitRequest($request); + $editor = id(new NuanceItemEditor()) + ->setActor($user) + ->setContentSource($source) + ->applyTransactions($item, $xactions); + + return $item->toDictionary(); + } + +} diff --git a/src/applications/nuance/constants/NuanceConstants.php b/src/applications/nuance/constants/NuanceConstants.php new file mode 100644 index 0000000000..e671b8d43a --- /dev/null +++ b/src/applications/nuance/constants/NuanceConstants.php @@ -0,0 +1,5 @@ +setViewer($user) diff --git a/src/applications/nuance/controller/NuanceSourceEditController.php b/src/applications/nuance/controller/NuanceSourceEditController.php index 15d7263205..b0c73c77b9 100644 --- a/src/applications/nuance/controller/NuanceSourceEditController.php +++ b/src/applications/nuance/controller/NuanceSourceEditController.php @@ -17,6 +17,9 @@ final class NuanceSourceEditController extends NuanceController { } public function processRequest() { + $can_edit = $this->requireApplicationCapability( + NuanceCapabilitySourceManage::CAPABILITY); + $request = $this->getRequest(); $user = $request->getUser(); @@ -24,12 +27,16 @@ final class NuanceSourceEditController extends NuanceController { $is_new = !$source_id; if ($is_new) { - $source = new NuanceSource(); - + $source = NuanceSource::initializeNewSource($user); } else { $source = id(new NuanceSourceQuery()) ->setViewer($user) ->withIDs(array($source_id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); } @@ -37,13 +44,23 @@ final class NuanceSourceEditController extends NuanceController { return new Aphront404Response(); } - $crumbs = $this->buildApplicationCrumbs(); - $title = 'TODO'; + $definition = NuanceSourceDefinition::getDefinitionForSource($source); + $definition->setActor($user); + $response = $definition->buildEditLayout($request); + if ($response instanceof AphrontResponse) { + return $response; + } + $layout = $response; + + $crumbs = $this->buildApplicationCrumbs(); return $this->buildApplicationPage( - $crumbs, array( - 'title' => $title, + $crumbs, + $layout, + ), + array( + 'title' => $definition->getEditTitle(), 'device' => true)); } } diff --git a/src/applications/nuance/controller/NuanceSourceViewController.php b/src/applications/nuance/controller/NuanceSourceViewController.php index 254ca59f1e..e0031d1d80 100644 --- a/src/applications/nuance/controller/NuanceSourceViewController.php +++ b/src/applications/nuance/controller/NuanceSourceViewController.php @@ -18,11 +18,11 @@ final class NuanceSourceViewController extends NuanceController { public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $source_id = $this->getSourceID(); $source = id(new NuanceSourceQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($source_id)) ->executeOne(); @@ -30,13 +30,105 @@ final class NuanceSourceViewController extends NuanceController { return new Aphront404Response(); } + $source_phid = $source->getPHID(); + $xactions = id(new NuanceSourceTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($source_phid)) + ->execute(); + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer); + + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($source_phid) + ->setMarkupEngine($engine) + ->setTransactions($xactions); + + $title = pht('%s', $source->getName()); $crumbs = $this->buildApplicationCrumbs(); - $title = 'TODO'; + $crumbs->addTextCrumb($title); + + $header = $this->buildHeaderView($source); + $actions = $this->buildActionView($source); + $properties = $this->buildPropertyView($source, $actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); return $this->buildApplicationPage( - $crumbs, + array( + $crumbs, + $box, + $timeline, + ), array( 'title' => $title, - 'device' => true)); + 'device' => true, + )); + + } + + + private function buildHeaderView(NuanceSource $source) { + $viewer = $this->getRequest()->getUser(); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($source->getName()) + ->setPolicyObject($source); + + return $header; + } + + private function buildActionView(NuanceSource $source) { + $viewer = $this->getRequest()->getUser(); + $id = $source->getID(); + + $actions = id(new PhabricatorActionListView()) + ->setObjectURI($source->getURI()) + ->setUser($viewer); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $source, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Source')) + ->setIcon('edit') + ->setHref($this->getApplicationURI("source/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $actions; + } + + private function buildPropertyView( + NuanceSource $source, + PhabricatorActionListView $actions) { + $viewer = $this->getRequest()->getUser(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($source) + ->setActionList($actions); + + $definition = NuanceSourceDefinition::getDefinitionForSource($source); + $properties->addProperty( + pht('Source Type'), + $definition->getName()); + + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( + $viewer, + $source); + + $properties->addProperty( + pht('Editable By'), + $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); + + return $properties; } } diff --git a/src/applications/nuance/editor/NuanceItemEditor.php b/src/applications/nuance/editor/NuanceItemEditor.php index 0e7d0927b1..92e877db92 100644 --- a/src/applications/nuance/editor/NuanceItemEditor.php +++ b/src/applications/nuance/editor/NuanceItemEditor.php @@ -6,6 +6,10 @@ final class NuanceItemEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); + $types[] = NuanceItemTransaction::TYPE_OWNER; + $types[] = NuanceItemTransaction::TYPE_SOURCE; + $types[] = NuanceItemTransaction::TYPE_REQUESTOR; + $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; @@ -14,4 +18,65 @@ final class NuanceItemEditor return $types; } + protected function getCustomTransactionOldValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceItemTransaction::TYPE_REQUESTOR: + return $object->getRequestorPHID(); + case NuanceItemTransaction::TYPE_SOURCE: + return $object->getSourcePHID(); + case NuanceItemTransaction::TYPE_OWNER: + return $object->getOwnerPHID(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceItemTransaction::TYPE_REQUESTOR: + case NuanceItemTransaction::TYPE_SOURCE: + case NuanceItemTransaction::TYPE_OWNER: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceItemTransaction::TYPE_REQUESTOR: + $object->setRequestorPHID($xaction->getNewValue()); + break; + case NuanceItemTransaction::TYPE_SOURCE: + $object->setSourcePHID($xaction->getNewValue()); + break; + case NuanceItemTransaction::TYPE_OWNER: + $object->setOwnerPHID($xaction->getNewValue()); + break; + } + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceItemTransaction::TYPE_REQUESTOR: + case NuanceItemTransaction::TYPE_SOURCE: + case NuanceItemTransaction::TYPE_OWNER: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + } diff --git a/src/applications/nuance/editor/NuanceSourceEditor.php b/src/applications/nuance/editor/NuanceSourceEditor.php index b470265974..0d17ba00f3 100644 --- a/src/applications/nuance/editor/NuanceSourceEditor.php +++ b/src/applications/nuance/editor/NuanceSourceEditor.php @@ -6,6 +6,8 @@ final class NuanceSourceEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); + $types[] = NuanceSourceTransaction::TYPE_NAME; + $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; @@ -14,4 +16,80 @@ final class NuanceSourceEditor return $types; } + protected function getCustomTransactionOldValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceSourceTransaction::TYPE_NAME: + return $object->getName(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceSourceTransaction::TYPE_NAME: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceSourceTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + break; + } + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case NuanceSourceTransaction::TYPE_NAME: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case NuanceSourceTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Source name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + } diff --git a/src/applications/nuance/phid/NuancePHIDTypeItem.php b/src/applications/nuance/phid/NuancePHIDTypeItem.php index 73b3c589d9..41008847fd 100644 --- a/src/applications/nuance/phid/NuancePHIDTypeItem.php +++ b/src/applications/nuance/phid/NuancePHIDTypeItem.php @@ -39,8 +39,4 @@ final class NuancePHIDTypeItem } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/nuance/phid/NuancePHIDTypeQueue.php b/src/applications/nuance/phid/NuancePHIDTypeQueue.php index 76ef4ee0fd..964fe8d2ef 100644 --- a/src/applications/nuance/phid/NuancePHIDTypeQueue.php +++ b/src/applications/nuance/phid/NuancePHIDTypeQueue.php @@ -39,8 +39,4 @@ final class NuancePHIDTypeQueue } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/nuance/phid/NuancePHIDTypeRequestor.php b/src/applications/nuance/phid/NuancePHIDTypeRequestor.php index 1789502870..d0d37f98d3 100644 --- a/src/applications/nuance/phid/NuancePHIDTypeRequestor.php +++ b/src/applications/nuance/phid/NuancePHIDTypeRequestor.php @@ -39,8 +39,4 @@ final class NuancePHIDTypeRequestor } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/nuance/phid/NuancePHIDTypeSource.php b/src/applications/nuance/phid/NuancePHIDTypeSource.php index d9b281fb49..78db3fb0e8 100644 --- a/src/applications/nuance/phid/NuancePHIDTypeSource.php +++ b/src/applications/nuance/phid/NuancePHIDTypeSource.php @@ -39,8 +39,4 @@ final class NuancePHIDTypeSource } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/nuance/query/NuanceSourceQuery.php b/src/applications/nuance/query/NuanceSourceQuery.php index a8d71ff35a..d56dbe0eda 100644 --- a/src/applications/nuance/query/NuanceSourceQuery.php +++ b/src/applications/nuance/query/NuanceSourceQuery.php @@ -35,7 +35,7 @@ final class NuanceSourceQuery $data = queryfx_all( $conn_r, - 'SELECT FROM %T %Q %Q %Q', + 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), diff --git a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php new file mode 100644 index 0000000000..aa1befd917 --- /dev/null +++ b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php @@ -0,0 +1,43 @@ +actor = $actor; + return $this; + } + public function getActor() { + return $this->actor; + } + public function requireActor() { + $actor = $this->getActor(); + if (!$actor) { + throw new Exception('You must "setActor()" first!'); + } + return $actor; + } + + public function setSourceObject(NuanceSource $source) { + $source->setType($this->getSourceTypeConstant()); + $this->sourceObject = $source; + return $this; + } + public function getSourceObject() { + return $this->sourceObject; + } + public function requireSourceObject() { + $source = $this->getSourceObject(); + if (!$source) { + throw new Exception('You must "setSourceObject()" first!'); + } + return $source; + } + + public static function getSelectOptions() { + $definitions = self::getAllDefinitions(); + + $options = array(); + foreach ($definitions as $definition) { + $key = $definition->getSourceTypeConstant(); + $name = $definition->getName(); + $options[$key] = $name; + } + + return $options; + } + + /** + * Gives a @{class:NuanceSourceDefinition} object for a given + * @{class:NuanceSource}. Note you still need to @{method:setActor} + * before the @{class:NuanceSourceDefinition} object will be useful. + */ + public static function getDefinitionForSource(NuanceSource $source) { + $definitions = self::getAllDefinitions(); + $map = mpull($definitions, null, 'getSourceTypeConstant'); + $definition = $map[$source->getType()]; + $definition->setSourceObject($source); + + return $definition; + } + + public static function getAllDefinitions() { + static $definitions; + + if ($definitions === null) { + $objects = id(new PhutilSymbolLoader()) + ->setAncestorClass(__CLASS__) + ->loadObjects(); + foreach ($objects as $definition) { + $key = $definition->getSourceTypeConstant(); + $name = $definition->getName(); + if (isset($definitions[$key])) { + $conflict = $definitions[$key]; + throw new Exception(sprintf( + 'Defintion %s conflicts with definition %s. This is a programming '. + 'error.', + $conflict, + $name)); + } + } + $definitions = $objects; + } + return $definitions; + } + + /** + * A human readable string like "Twitter" or "Phabricator Form". + */ + abstract public function getName(); + + /** + * This should be a any VARCHAR(32). + * + * @{method:getAllDefinitions} will throw if you choose a string that + * collides with another @{class:NuanceSourceDefinition} class. + */ + abstract public function getSourceTypeConstant(); + + /** + * Code to create and update @{class:NuanceItem}s and + * @{class:NuanceRequestor}s via daemons goes here. + * + * If that does not make sense for the @{class:NuanceSource} you are + * defining, simply return null. For example, + * @{class:NuancePhabricatorFormSourceDefinition} since these are one-way + * contact forms. + */ + abstract public function updateItems(); + + private function loadSourceObjectPolicies( + PhabricatorUser $user, + NuanceSource $source) { + + $user = $this->requireActor(); + $source = $this->requireSourceObject(); + return id(new PhabricatorPolicyQuery()) + ->setViewer($user) + ->setObject($source) + ->execute(); + } + + final public function getEditTitle() { + $source = $this->requireSourceObject(); + if ($source->getPHID()) { + $title = pht('Edit "%s" source.', $source->getName()); + } else { + $title = pht('Create a new "%s" source.', $this->getName()); + } + + return $title; + } + + final public function buildEditLayout(AphrontRequest $request) { + $actor = $this->requireActor(); + $source = $this->requireSourceObject(); + + $form_errors = array(); + $error_messages = array(); + $transactions = array(); + $validation_exception = null; + if ($request->isFormPost()) { + $transactions = $this->buildTransactions($request); + try { + $editor = id(new NuanceSourceEditor()) + ->setActor($actor) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->applyTransactions($source, $transactions); + + return id(new AphrontRedirectResponse()) + ->setURI($source->getURI()); + + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + } + + } + + $form = $this->renderEditForm($validation_exception); + $layout = id(new PHUIObjectBoxView()) + ->setHeaderText($this->getEditTitle()) + ->setValidationException($validation_exception) + ->setFormErrors($error_messages) + ->setForm($form); + + return $layout; + } + + /** + * Code to create a form to edit the @{class:NuanceItem} you are defining. + * + * return @{class:AphrontFormView} + */ + private function renderEditForm( + PhabricatorApplicationTransactionValidationException $ex = null) { + $user = $this->requireActor(); + $source = $this->requireSourceObject(); + $policies = $this->loadSourceObjectPolicies($user, $source); + $e_name = null; + if ($ex) { + $e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setError($e_name) + ->setValue($source->getName())) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Type')) + ->setName('type') + ->setOptions(self::getSelectOptions()) + ->setValue($source->getType())); + + $form = $this->augmentEditForm($form, $ex); + + $form + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($user) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicyObject($source) + ->setPolicies($policies) + ->setName('viewPolicy')) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($user) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicyObject($source) + ->setPolicies($policies) + ->setName('editPolicy')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($source->getURI()) + ->setValue(pht('Save'))); + + return $form; + } + + /** + * return @{class:AphrontFormView} + */ + protected function augmentEditForm( + AphrontFormView $form, + PhabricatorApplicationTransactionValidationException $ex = null) { + + return $form; + } + + /** + * Hook to build up @{class:PhabricatorTransactions}. + * + * return array $transactions + */ + protected function buildTransactions(AphrontRequest $request) { + $transactions = array(); + + $transactions[] = id(new NuanceSourceTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($request->getStr('editPolicy')); + $transactions[] = id(new NuanceSourceTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($request->getStr('viewPolicy')); + $transactions[] = id(new NuanceSourceTransaction()) + ->setTransactionType(NuanceSourceTransaction::TYPE_NAME) + ->setNewvalue($request->getStr('name')); + + return $transactions; + } + + abstract public function renderView(); + + abstract public function renderListView(); +} + diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 94dcd09dbf..3f32681b47 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -4,6 +4,11 @@ final class NuanceItem extends NuanceDAO implements PhabricatorPolicyInterface { + const STATUS_OPEN = 0; + const STATUS_ASSIGNED = 10; + const STATUS_CLOSED = 20; + + protected $status; protected $ownerPHID; protected $requestorPHID; protected $sourcePHID; @@ -12,6 +17,11 @@ final class NuanceItem protected $mailKey; protected $dateNuanced; + public static function initializeNewItem(PhabricatorUser $user) { + return id(new NuanceItem()) + ->setDateNuanced(time()) + ->setStatus(NuanceItem::STATUS_OPEN); + } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -74,7 +84,7 @@ final class NuanceItem public function getPolicy($capability) { // TODO - this should be based on the queues the item currently resides in - return PhabricatorPolicies::POLICY_NOONE; + return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { @@ -92,4 +102,17 @@ final class NuanceItem return null; } + public function toDictionary() { + return array( + 'id' => $this->getID(), + 'phid' => $this->getPHID(), + 'ownerPHID' => $this->getOwnerPHID(), + 'requestorPHID' => $this->getRequestorPHID(), + 'sourcePHID' => $this->getSourcePHID(), + 'sourceLabel' => $this->getSourceLabel(), + 'dateCreated' => $this->getDateCreated(), + 'dateModified' => $this->getDateModified(), + 'dateNuanced' => $this->getDateNuanced(), + ); + } } diff --git a/src/applications/nuance/storage/NuanceItemTransaction.php b/src/applications/nuance/storage/NuanceItemTransaction.php index da889c0c6e..6be69da6e0 100644 --- a/src/applications/nuance/storage/NuanceItemTransaction.php +++ b/src/applications/nuance/storage/NuanceItemTransaction.php @@ -3,6 +3,10 @@ final class NuanceItemTransaction extends NuanceTransaction { + const TYPE_OWNER = 'item-owner'; + const TYPE_REQUESTOR = 'item-requestor'; + const TYPE_SOURCE = 'item-source'; + public function getApplicationTransactionType() { return NuancePHIDTypeItem::TYPECONST; } diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php index 51407a4ec5..e7984b6397 100644 --- a/src/applications/nuance/storage/NuanceSource.php +++ b/src/applications/nuance/storage/NuanceSource.php @@ -36,6 +36,27 @@ final class NuanceSource return '/nuance/source/view/'.$this->getID().'/'; } + public static function initializeNewSource(PhabricatorUser $actor) { + $app = id(new PhabricatorApplicationQuery()) + ->setViewer($actor) + ->withClasses(array('PhabricatorApplicationNuance')) + ->executeOne(); + + $view_policy = $app->getPolicy( + NuanceCapabilitySourceDefaultView::CAPABILITY); + $edit_policy = $app->getPolicy( + NuanceCapabilitySourceDefaultEdit::CAPABILITY); + + $definitions = NuanceSourceDefinition::getAllDefinitions(); + $lucky_definition = head($definitions); + + return id(new NuanceSource()) + ->setViewPolicy($view_policy) + ->setEditPolicy($edit_policy) + ->setType($lucky_definition->getSourceTypeConstant()); + + } + public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, diff --git a/src/applications/nuance/storage/NuanceSourceTransaction.php b/src/applications/nuance/storage/NuanceSourceTransaction.php index 1c06508b40..ba7d8e8f03 100644 --- a/src/applications/nuance/storage/NuanceSourceTransaction.php +++ b/src/applications/nuance/storage/NuanceSourceTransaction.php @@ -3,6 +3,8 @@ final class NuanceSourceTransaction extends NuanceTransaction { + const TYPE_NAME = 'name-source'; + public function getApplicationTransactionType() { return NuancePHIDTypeSource::TYPECONST; } @@ -11,4 +13,27 @@ final class NuanceSourceTransaction return new NuanceSourceTransactionComment(); } + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + $author_phid = $this->getAuthorPHID(); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created this source.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s renamed this source from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + break; + } + + } + } diff --git a/src/applications/owners/controller/PhabricatorOwnersController.php b/src/applications/owners/controller/PhabricatorOwnersController.php index a68060c9fb..ff74fb14fc 100644 --- a/src/applications/owners/controller/PhabricatorOwnersController.php +++ b/src/applications/owners/controller/PhabricatorOwnersController.php @@ -45,9 +45,7 @@ abstract class PhabricatorOwnersController extends PhabricatorController { } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $crumbs->addAction( id(new PHUIListItemView()) diff --git a/src/applications/owners/controller/PhabricatorOwnersDeleteController.php b/src/applications/owners/controller/PhabricatorOwnersDeleteController.php index dd1cf89b59..306658eb6b 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDeleteController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDeleteController.php @@ -29,9 +29,7 @@ final class PhabricatorOwnersDeleteController $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle('Really delete this package?') - ->appendChild(hsprintf( - '

    %s

    ', - $text)) + ->appendChild(phutil_tag('p', array(), $text)) ->addSubmitButton(pht('Delete')) ->addCancelButton('/owners/package/'.$package->getID().'/') ->setSubmitURI($request->getRequestURI()); diff --git a/src/applications/owners/controller/PhabricatorOwnersEditController.php b/src/applications/owners/controller/PhabricatorOwnersEditController.php index 77c3f582b5..b456e3f729 100644 --- a/src/applications/owners/controller/PhabricatorOwnersEditController.php +++ b/src/applications/owners/controller/PhabricatorOwnersEditController.php @@ -112,13 +112,6 @@ final class PhabricatorOwnersEditController } } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Package Errors')); - $error_view->setErrors($errors); - } - $handles = $this->loadViewerHandles($owners); $primary = $package->getPrimaryOwnerPHID(); @@ -252,7 +245,7 @@ final class PhabricatorOwnersEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $nav = $this->buildSideNavView(); diff --git a/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php new file mode 100644 index 0000000000..f2951353ed --- /dev/null +++ b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php @@ -0,0 +1,46 @@ +\d+)' => 'PassphraseCredentialViewController', + '/passphrase/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PassphraseCredentialListController', + 'create/' => 'PassphraseCredentialCreateController', + 'edit/(?:(?P\d+)/)?' => 'PassphraseCredentialEditController', + 'destroy/(?P\d+)/' => 'PassphraseCredentialDestroyController', + 'reveal/(?P\d+)/' => 'PassphraseCredentialRevealController', + )); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseController.php b/src/applications/passphrase/controller/PassphraseController.php new file mode 100644 index 0000000000..77268c1cbe --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseController.php @@ -0,0 +1,40 @@ +getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + if ($for_app) { + $nav->addFilter('create', pht('Create Credential')); + } + + id(new PassphraseCredentialSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationMenu() { + return $this->buildSideNavView(true)->getMenu(); + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Create Credential')) + ->setHref($this->getApplicationURI('create/')) + ->setIcon('create')); + + return $crumbs; + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialCreateController.php b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php new file mode 100644 index 0000000000..56bf4f474f --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php @@ -0,0 +1,70 @@ +getRequest(); + $viewer = $request->getUser(); + + $types = PassphraseCredentialType::getAllCreateableTypes(); + $types = mpull($types, null, 'getCredentialType'); + $types = msort($types, 'getCredentialTypeName'); + + $errors = array(); + $e_type = null; + + if ($request->isFormPost()) { + $type = $request->getStr('type'); + if (empty($types[$type])) { + $errors[] = pht('You must choose a credential type.'); + $e_type = pht('Required'); + } + + if (!$errors) { + $uri = $this->getApplicationURI('edit/?type='.$type); + return id(new AphrontRedirectResponse())->setURI($uri); + } + } + + $types_control = id(new AphrontFormRadioButtonControl()) + ->setName('type') + ->setLabel(pht('Credential Type')) + ->setError($e_type); + + foreach ($types as $type) { + $types_control->addButton( + $type->getCredentialType(), + $type->getCredentialTypeName(), + $type->getCredentialTypeDescription()); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($types_control) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Continue')) + ->addCancelButton($this->getApplicationURI())); + + $title = pht('New Credential'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Create')); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Create New Credential')) + ->setFormErrors($errors) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php new file mode 100644 index 0000000000..30c744e96f --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php @@ -0,0 +1,67 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + $view_uri = '/K'.$credential->getID(); + + if ($request->isFormPost()) { + + $xactions = array(); + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType(PassphraseCredentialTransaction::TYPE_DESTROY) + ->setNewValue(1); + + $editor = id(new PassphraseCredentialTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($credential, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Really destroy credential?')) + ->appendChild( + pht( + 'This credential will be deactivated and the secret will be '. + 'unrecoverably destroyed. Anything relying on this credential will '. + 'cease to function. This operation can not be undone.')) + ->addSubmitButton(pht('Destroy Credential')) + ->addCancelButton($view_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php new file mode 100644 index 0000000000..917b045c6f --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -0,0 +1,312 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + if (!$type->isCreateable()) { + throw new Exception( + pht('Credential has noncreateable type "%s"!', $type)); + } + + $is_new = false; + } else { + $type_const = $request->getStr('type'); + $type = PassphraseCredentialType::getTypeByConstant($type_const); + if (!$type) { + return new Aphront404Response(); + } + + $credential = PassphraseCredential::initializeNewCredential($viewer) + ->setCredentialType($type->getCredentialType()) + ->setProvidesType($type->getProvidesType()); + + $is_new = true; + + // Prefill username if provided. + $credential->setUsername($request->getStr('username')); + } + + $errors = array(); + + $v_name = $credential->getName(); + $e_name = true; + + $v_desc = $credential->getDescription(); + + $v_username = $credential->getUsername(); + $e_username = true; + + $bullet = "\xE2\x80\xA2"; + + $v_secret = $credential->getSecretID() ? str_repeat($bullet, 32) : null; + + $validation_exception = null; + $errors = array(); + $e_password = null; + if ($request->isFormPost()) { + + $v_name = $request->getStr('name'); + $v_desc = $request->getStr('description'); + $v_username = $request->getStr('username'); + $v_view_policy = $request->getStr('viewPolicy'); + $v_edit_policy = $request->getStr('editPolicy'); + + $v_secret = $request->getStr('secret'); + $v_password = $request->getStr('password'); + $v_decrypt = $v_secret; + + $env_secret = new PhutilOpaqueEnvelope($v_secret); + $env_password = new PhutilOpaqueEnvelope($v_password); + + if ($type->requiresPassword($env_secret)) { + if (strlen($v_password)) { + $v_decrypt = $type->decryptSecret($env_secret, $env_password); + if ($v_decrypt === null) { + $e_password = pht('Incorrect'); + $errors[] = pht( + 'This key requires a password, but the password you provided '. + 'is incorrect.'); + } else { + $v_decrypt = $v_decrypt->openEnvelope(); + } + } else { + $e_password = pht('Required'); + $errors[] = pht( + 'This key requires a password. You must provide the password '. + 'for the key.'); + } + } + + if (!$errors) { + $type_name = PassphraseCredentialTransaction::TYPE_NAME; + $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION; + $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; + $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; + $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; + $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; + $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; + + $xactions = array(); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_desc) + ->setNewValue($v_desc); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_username) + ->setNewValue($v_username); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_view_policy) + ->setNewValue($v_view_policy); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_edit_policy) + ->setNewValue($v_edit_policy); + + // Open a transaction in case we're writing a new secret; this limits + // the amount of code which handles secret plaintexts. + $credential->openTransaction(); + + $min_secret = str_replace($bullet, '', trim($v_decrypt)); + if (strlen($min_secret)) { + // If the credential was previously destroyed, restore it when it is + // edited if a secret is provided. + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_destroy) + ->setNewValue(0); + + $new_secret = id(new PassphraseSecret()) + ->setSecretData($v_decrypt) + ->save(); + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_secret_id) + ->setNewValue($new_secret->getID()); + } + + try { + $editor = id(new PassphraseCredentialTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($credential, $xactions); + + $credential->saveTransaction(); + + if ($request->isAjax()) { + return id(new AphrontAjaxResponse())->setContent( + array( + 'phid' => $credential->getPHID(), + 'name' => 'K'.$credential->getID().' '.$credential->getName(), + )); + } else { + return id(new AphrontRedirectResponse()) + ->setURI('/K'.$credential->getID()); + } + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $credential->killTransaction(); + + $validation_exception = $ex; + + $e_name = $ex->getShortMessage($type_name); + $e_username = $ex->getShortMessage($type_username); + + $credential->setViewPolicy($v_view_policy); + $credential->setEditPolicy($v_edit_policy); + } + } + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($credential) + ->execute(); + + $secret_control = $type->newSecretControl(); + + if ($request->isAjax()) { + $form = new PHUIFormLayoutView(); + } else { + $form = id(new AphrontFormView()) + ->setUser($viewer); + } + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('name') + ->setLabel(pht('Name')) + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) + ->setName('description') + ->setLabel(pht('Description')) + ->setValue($v_desc)) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Credential Type')) + ->setValue($type->getCredentialTypeName())) + ->appendChild( + id(new AphrontFormDividerControl())) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($credential) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($credential) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormDividerControl())) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('username') + ->setLabel(pht('Login/Username')) + ->setValue($v_username) + ->setError($e_username)) + ->appendChild( + $secret_control + ->setName('secret') + ->setLabel($type->getSecretLabel()) + ->setValue($v_secret)); + + if ($type->shouldShowPasswordField()) { + $form->appendChild( + id(new AphrontFormPasswordControl()) + ->setName('password') + ->setLabel($type->getPasswordLabel()) + ->setError($e_password)); + } + + $crumbs = $this->buildApplicationCrumbs(); + + if ($is_new) { + $title = pht('Create Credential'); + $header = pht('Create New Credential'); + $crumbs->addTextCrumb(pht('Create')); + } else { + $title = pht('Edit Credential'); + $header = pht('Edit Credential %s', 'K'.$credential->getID()); + $crumbs->addTextCrumb( + 'K'.$credential->getID(), + '/K'.$credential->getID()); + $crumbs->addTextCrumb(pht('Edit')); + } + + if ($request->isAjax()) { + $errors = id(new AphrontErrorView())->setErrors($errors); + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) + ->appendChild($errors) + ->appendChild($form) + ->addSubmitButton(pht('Create Credential')) + ->addCancelButton($this->getApplicationURI()); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $form->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save')) + ->addCancelButton($this->getApplicationURI())); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->setFormErrors($errors) + ->setValidationException($validation_exception) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialListController.php b/src/applications/passphrase/controller/PassphraseCredentialListController.php new file mode 100644 index 0000000000..bc1bac0258 --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialListController.php @@ -0,0 +1,63 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PassphraseCredentialSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function renderResultsList( + array $credentials, + PhabricatorSavedQuery $query) { + assert_instances_of($credentials, 'PassphraseCredential'); + + $viewer = $this->getRequest()->getUser(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($credentials as $credential) { + + $item = id(new PHUIObjectItemView()) + ->setObjectName('K'.$credential->getID()) + ->setHeader($credential->getName()) + ->setHref('/K'.$credential->getID()) + ->setObject($credential); + + $item->addAttribute( + pht('Login: %s', $credential->getUsername())); + + if ($credential->getIsDestroyed()) { + $item->addIcon('disable', pht('Destroyed')); + $item->setDisabled(true); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if ($type) { + $item->addIcon('wrench', $type->getCredentialTypeName()); + } + + $list->addItem($item); + } + + return $list; + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php new file mode 100644 index 0000000000..88de484955 --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -0,0 +1,75 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->needSecrets(true) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $view_uri = '/K'.$credential->getID(); + + if ($request->isFormPost()) { + if ($credential->getSecret()) { + $body = id(new PHUIFormLayoutView()) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Plaintext')) + ->setValue($credential->getSecret()->openEnvelope())); + } else { + $body = pht('This credential has no associated secret.'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Credential Secret')) + ->appendChild($body) + ->addCancelButton($view_uri, pht('Done')); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + if ($is_serious) { + $body = pht( + 'The secret associated with this credential will be shown in plain '. + 'text on your screen.'); + } else { + $body = pht( + 'The secret associated with this credential will be shown in plain '. + 'text on your screen. Before continuing, wrap your arms around your '. + 'monitor to create a human shield, keeping it safe from prying eyes. '. + 'Protect company secrets!'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Really show secret?')) + ->appendChild($body) + ->addSubmitButton(pht('Show Secret')) + ->addCancelButton($view_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php new file mode 100644 index 0000000000..5ff0181bde --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -0,0 +1,179 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + $xactions = id(new PassphraseCredentialTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($credential->getPHID())) + ->execute(); + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer); + + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($credential->getPHID()) + ->setTransactions($xactions); + + $title = pht('%s %s', 'K'.$credential->getID(), $credential->getName()); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb('K'.$credential->getID()); + + $header = $this->buildHeaderView($credential); + $actions = $this->buildActionView($credential); + $properties = $this->buildPropertyView($credential, $type, $actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $timeline, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + + private function buildHeaderView(PassphraseCredential $credential) { + $viewer = $this->getRequest()->getUser(); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($credential->getName()) + ->setPolicyObject($credential); + + if ($credential->getIsDestroyed()) { + $header->setStatus('reject', 'red', pht('Destroyed')); + } + + return $header; + } + + private function buildActionView(PassphraseCredential $credential) { + $viewer = $this->getRequest()->getUser(); + + $id = $credential->getID(); + + $actions = id(new PhabricatorActionListView()) + ->setObjectURI('/K'.$id) + ->setUser($viewer); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $credential, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Credential')) + ->setIcon('edit') + ->setHref($this->getApplicationURI("edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + if (!$credential->getIsDestroyed()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Destroy Credential')) + ->setIcon('delete') + ->setHref($this->getApplicationURI("destroy/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Show Secret')) + ->setIcon('preview') + ->setHref($this->getApplicationURI("reveal/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } + + + return $actions; + } + + private function buildPropertyView( + PassphraseCredential $credential, + PassphraseCredentialType $type, + PhabricatorActionListView $actions) { + $viewer = $this->getRequest()->getUser(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($credential) + ->setActionList($actions); + + $properties->addProperty( + pht('Credential Type'), + $type->getCredentialTypeName()); + + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( + $viewer, + $credential); + + $properties->addProperty( + pht('Editable By'), + $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); + + $properties->addProperty( + pht('Username'), + $credential->getUsername()); + + $used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $credential->getPHID(), + PhabricatorEdgeConfig::TYPE_CREDENTIAL_USED_BY_OBJECT); + + if ($used_by_phids) { + $this->loadHandles($used_by_phids); + $properties->addProperty( + pht('Used By'), + $this->renderHandlesForPHIDs($used_by_phids)); + } + + $description = $credential->getDescription(); + if (strlen($description)) { + $properties->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $properties->addTextContent( + PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff()) + ->setContent($description), + 'default', + $viewer)); + } + + return $properties; + } + +} diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php new file mode 100644 index 0000000000..5d94a58f0d --- /dev/null +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php @@ -0,0 +1,109 @@ +setAncestorClass(__CLASS__) + ->loadObjects(); + return $types; + } + + public static function getAllCreateableTypes() { + $types = self::getAllTypes(); + foreach ($types as $key => $type) { + if (!$type->isCreateable()) { + unset($types[$key]); + } + } + + return $types; + } + + public static function getTypeByConstant($constant) { + $all = self::getAllTypes(); + $all = mpull($all, null, 'getCredentialType'); + return idx($all, $constant); + } + + + /** + * Can users create new credentials of this type? + * + * @return bool True if new credentials of this type can be created. + */ + public function isCreateable() { + return true; + } + + +/* -( Passwords )---------------------------------------------------------- */ + + + /** + * Return true to show an additional "Password" field. This is used by + * SSH credentials to strip passwords off private keys. + * + * @return bool True if a password field should be shown to the user. + * + * @task password + */ + public function shouldShowPasswordField() { + return false; + } + + + /** + * Return the label for the password field, if one is shown. + * + * @return string Human-readable field label. + * + * @task password + */ + public function getPasswordLabel() { + return pht('Password'); + } + + + /** + * Return true if the provided credental requires a password to decrypt. + * + * @param PhutilOpaqueEnvelope Credential secret value. + * @return bool True if the credential needs a password. + * + * @task password + */ + public function requiresPassword(PhutilOpaqueEnvelope $secret) { + return false; + } + + + /** + * Return the decrypted credential secret, or `null` if the password does + * not decrypt the credential. + * + * @param PhutilOpaqueEnvelope Credential secret value. + * @param PhutilOpaqueEnvelope Credential password. + * @return + * @task password + */ + public function decryptSecret( + PhutilOpaqueEnvelope $secret, + PhutilOpaqueEnvelope $password) { + return $secret; + } + +} diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php new file mode 100644 index 0000000000..7f3ef9e6e1 --- /dev/null +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php @@ -0,0 +1,33 @@ +openEnvelope()); + } + + public function decryptSecret( + PhutilOpaqueEnvelope $secret, + PhutilOpaqueEnvelope $password) { + + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $secret->openEnvelope()); + + if (!Filesystem::binaryExists('ssh-keygen')) { + throw new Exception( + pht( + 'Decrypting SSH keys requires the `ssh-keygen` binary, but it '. + 'is not available in PATH. Either make it available or strip the '. + 'password fromt his SSH key manually before uploading it.')); + } + + list($err, $stdout, $stderr) = exec_manual( + 'ssh-keygen -p -P %P -N %s -f %s', + $password, + '', + (string)$tmp); + + if ($err) { + return null; + } else { + return new PhutilOpaqueEnvelope(Filesystem::readFile($tmp)); + } + } + +} diff --git a/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php b/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php new file mode 100644 index 0000000000..126833e0ee --- /dev/null +++ b/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php @@ -0,0 +1,173 @@ +getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + if ($this->getIsNewObject()) { + return null; + } + return $object->getName(); + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + return $object->getDescription(); + case PassphraseCredentialTransaction::TYPE_USERNAME: + return $object->getUsername(); + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + return $object->getSecretID(); + case PassphraseCredentialTransaction::TYPE_DESTROY: + return $object->getIsDestroyed(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + case PassphraseCredentialTransaction::TYPE_USERNAME: + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + case PassphraseCredentialTransaction::TYPE_DESTROY: + return $xaction->getNewValue(); + } + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + $object->setDescription($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_USERNAME: + $object->setUsername($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + $old_id = $object->getSecretID(); + if ($old_id) { + $this->destroySecret($old_id); + } + $object->setSecretID($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_DESTROY: + // When destroying a credential, wipe out its secret. + $is_destroyed = $xaction->getNewValue(); + $object->setIsDestroyed($is_destroyed); + if ($is_destroyed) { + $secret_id = $object->getSecretID(); + if ($secret_id) { + $this->destroySecret($secret_id); + $object->setSecretID(null); + } + } + return; + case PhabricatorTransactions::TYPE_VIEW_POLICY: + $object->setViewPolicy($xaction->getNewValue()); + return; + case PhabricatorTransactions::TYPE_EDIT_POLICY: + $object->setEditPolicy($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + case PassphraseCredentialTransaction::TYPE_USERNAME: + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + case PassphraseCredentialTransaction::TYPE_DESTROY: + case PhabricatorTransactions::TYPE_VIEW_POLICY: + case PhabricatorTransactions::TYPE_EDIT_POLICY: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + private function destroySecret($secret_id) { + $table = new PassphraseSecret(); + queryfx( + $table->establishConnection('w'), + 'DELETE FROM %T WHERE id = %d', + $table->getTableName(), + $secret_id); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PassphraseCredentialTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Credential name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + case PassphraseCredentialTransaction::TYPE_USERNAME: + $missing = $this->validateIsEmptyTextField( + $object->getUsername(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Username is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + +} diff --git a/src/applications/passphrase/keys/PassphraseAbstractKey.php b/src/applications/passphrase/keys/PassphraseAbstractKey.php new file mode 100644 index 0000000000..cf532d3fac --- /dev/null +++ b/src/applications/passphrase/keys/PassphraseAbstractKey.php @@ -0,0 +1,74 @@ +credential) { + throw new Exception(pht("Credential is required!")); + } + return $this->credential; + } + + private function loadCredential( + $phid, + PhabricatorUser $viewer) { + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->needSecrets(true) + ->executeOne(); + + if (!$credential) { + throw new Exception(pht('Failed to load credential "%s"!', $phid)); + } + + return $credential; + } + + private function validateCredential( + PassphraseCredential $credential, + $provides_type) { + + $type = $credential->getCredentialTypeImplementation(); + + if (!$type) { + throw new Exception( + pht( + 'Credential "%s" is of unknown type "%s"!', + 'K'.$credential->getID(), + $credential->getCredentialType())); + } + + if ($type->getProvidesType() !== $provides_type) { + throw new Exception( + pht( + 'Credential "%s" must provide "%s", but provides "%s"!', + 'K'.$credential->getID(), + $provides_type, + $type->getProvidesType())); + } + } + + protected function loadAndValidateFromPHID( + $phid, + PhabricatorUser $viewer, + $type) { + + $credential = $this->loadCredential($phid, $viewer); + + $this->validateCredential($credential, $type); + + $this->credential = $credential; + + return $this; + } + + public function getUsernameEnvelope() { + $credential = $this->requireCredential(); + return new PhutilOpaqueEnvelope($credential->getUsername()); + } + +} diff --git a/src/applications/passphrase/keys/PassphrasePasswordKey.php b/src/applications/passphrase/keys/PassphrasePasswordKey.php new file mode 100644 index 0000000000..df2e683c20 --- /dev/null +++ b/src/applications/passphrase/keys/PassphrasePasswordKey.php @@ -0,0 +1,17 @@ +loadAndValidateFromPHID( + $phid, + $viewer, + PassphraseCredentialTypePassword::PROVIDES_TYPE); + } + + public function getPasswordEnvelope() { + return $this->requireCredential()->getSecret(); + } + +} diff --git a/src/applications/passphrase/keys/PassphraseSSHKey.php b/src/applications/passphrase/keys/PassphraseSSHKey.php new file mode 100644 index 0000000000..4ba5025b98 --- /dev/null +++ b/src/applications/passphrase/keys/PassphraseSSHKey.php @@ -0,0 +1,40 @@ +loadAndValidateFromPHID( + $phid, + $viewer, + PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE); + } + + public function getKeyfileEnvelope() { + $credential = $this->requireCredential(); + + $text_type = PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE; + if ($credential->getCredentialType() == $text_type) { + // If the credential stores key text, write it out to a temporary file + // so we can pass it to `ssh`. + if (!$this->keyFile) { + $temporary_file = new TempFile('passphrase-ssh-key'); + + Filesystem::changePermissions($temporary_file, 0600); + + Filesystem::writeFile( + $temporary_file, + $credential->getSecret()->openEnvelope()); + + $this->keyFile = $temporary_file; + } + + return new PhutilOpaqueEnvelope((string)$this->keyFile); + } + + return $credential->getSecret(); + } + +} diff --git a/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php b/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php new file mode 100644 index 0000000000..ac7fedaa93 --- /dev/null +++ b/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php @@ -0,0 +1,76 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $credential = $objects[$phid]; + $id = $credential->getID(); + $name = $credential->getName(); + + $handle->setName("K{$id}"); + $handle->setFullName("K{$id} {$name}"); + $handle->setURI("/K{$id}"); + + if ($credential->getIsDestroyed()) { + $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); + } + } + } + + public function canLoadNamedObject($name) { + return preg_match('/^K\d*[1-9]\d*$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + $id_map = array(); + foreach ($names as $name) { + $id = (int)substr($name, 1); + $id_map[$id][] = $name; + } + + $objects = id(new PassphraseCredentialQuery()) + ->setViewer($query->getViewer()) + ->withIDs(array_keys($id_map)) + ->execute(); + + $results = array(); + foreach ($objects as $id => $object) { + foreach (idx($id_map, $id, array()) as $name) { + $results[$name] = $object; + } + } + + return $results; + } + +} diff --git a/src/applications/passphrase/query/PassphraseCredentialQuery.php b/src/applications/passphrase/query/PassphraseCredentialQuery.php new file mode 100644 index 0000000000..349a4abee7 --- /dev/null +++ b/src/applications/passphrase/query/PassphraseCredentialQuery.php @@ -0,0 +1,137 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withCredentialTypes(array $credential_types) { + $this->credentialTypes = $credential_types; + return $this; + } + + public function withProvidesTypes(array $provides_types) { + $this->providesTypes = $provides_types; + return $this; + } + + public function withIsDestroyed($destroyed) { + $this->isDestroyed = $destroyed; + return $this; + } + + public function needSecrets($need_secrets) { + $this->needSecrets = $need_secrets; + return $this; + } + + protected function loadPage() { + $table = new PassphraseCredential(); + $conn_r = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($rows); + } + + protected function willFilterPage(array $page) { + if ($this->needSecrets) { + $secret_ids = mpull($page, 'getSecretID'); + $secret_ids = array_filter($secret_ids); + + $secrets = array(); + if ($secret_ids) { + $secret_objects = id(new PassphraseSecret())->loadAllWhere( + 'id IN (%Ld)', + $secret_ids); + foreach ($secret_objects as $secret) { + $secret_data = $secret->getSecretData(); + $secrets[$secret->getID()] = new PhutilOpaqueEnvelope($secret_data); + } + } + + foreach ($page as $key => $credential) { + $secret_id = $credential->getSecretID(); + if (!$secret_id) { + $credential->attachSecret(null); + } else if (isset($secrets[$secret_id])) { + $credential->attachSecret($secrets[$secret_id]); + } else { + unset($page[$key]); + } + } + } + + return $page; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + $where[] = $this->buildPagingClause($conn_r); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->credentialTypes) { + $where[] = qsprintf( + $conn_r, + 'credentialType in (%Ls)', + $this->credentialTypes); + } + + if ($this->providesTypes) { + $where[] = qsprintf( + $conn_r, + 'providesType IN (%Ls)', + $this->providesTypes); + } + + if ($this->isDestroyed !== null) { + $where[] = qsprintf( + $conn_r, + 'isDestroyed = %d', + (int)$this->isDestroyed); + } + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPassphrase'; + } + +} diff --git a/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php b/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php new file mode 100644 index 0000000000..ab2144f213 --- /dev/null +++ b/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php @@ -0,0 +1,73 @@ +setParameter( + 'isDestroyed', + $this->readBoolFromRequest($request, 'isDestroyed')); + + return $saved; + } + + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $query = id(new PassphraseCredentialQuery()); + + $destroyed = $saved->getParameter('isDestroyed'); + if ($destroyed !== null) { + $query->withIsDestroyed($destroyed); + } + + return $query; + } + + public function buildSearchForm( + AphrontFormView $form, + PhabricatorSavedQuery $saved_query) { + + $form->appendChild( + id(new AphrontFormSelectControl()) + ->setName('isDestroyed') + ->setLabel(pht('Status')) + ->setValue($this->getBoolFromQuery($saved_query, 'isDestroyed')) + ->setOptions( + array( + '' => pht('Show All Credentials'), + 'false' => pht('Show Only Active Credentials'), + 'true' => pht('Show Only Destroyed Credentials'), + ))); + + } + + protected function getURI($path) { + return '/passphrase/'.$path; + } + + public function getBuiltinQueryNames() { + $names = array( + 'active' => pht('Active Credentials'), + 'all' => pht('All Credentials'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + case 'active': + return $query->setParameter('isDestroyed', false); + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php b/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php new file mode 100644 index 0000000000..ebc5237091 --- /dev/null +++ b/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php @@ -0,0 +1,10 @@ +setName('') + ->setUsername('') + ->setDescription('') + ->setIsDestroyed(0) + ->setViewPolicy($actor->getPHID()) + ->setEditPolicy($actor->getPHID()); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PassphrasePHIDTypeCredential::TYPECONST); + } + + public function attachSecret(PhutilOpaqueEnvelope $secret = null) { + $this->secret = $secret; + return $this; + } + + public function getSecret() { + return $this->assertAttached($this->secret); + } + + public function getCredentialTypeImplementation() { + $type = $this->getCredentialType(); + return PassphraseCredentialType::getTypeByConstant($type); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php new file mode 100644 index 0000000000..52739a4808 --- /dev/null +++ b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php @@ -0,0 +1,106 @@ +getOldValue(); + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return ($old === null); + case self::TYPE_USERNAME: + return !strlen($old); + } + return parent::shouldHide(); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + $author_phid = $this->getAuthorPHID(); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created this credential.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s renamed this credential from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + break; + case self::TYPE_DESCRIPTION: + return pht( + '%s updated the description for this credential.', + $this->renderHandleLink($author_phid)); + case self::TYPE_USERNAME: + if (strlen($old)) { + return pht( + '%s changed the username for this credential from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } else { + return pht( + '%s set the username for this credential to "%s".', + $this->renderHandleLink($author_phid), + $new); + } + break; + case self::TYPE_SECRET_ID: + return pht( + '%s updated the secret for this credential.', + $this->renderHandleLink($author_phid)); + case self::TYPE_DESTROY: + return pht( + '%s destroyed this credential.', + $this->renderHandleLink($author_phid)); + } + + return parent::getTitle(); + } + + public function hasChangeDetails() { + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return true; + } + return parent::hasChangeDetails(); + } + + public function renderChangeDetails(PhabricatorUser $viewer) { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setUser($viewer) + ->setOldText(json_encode($old)) + ->setNewText(json_encode($new)); + + return $view->render(); + } + + +} diff --git a/src/applications/passphrase/storage/PassphraseDAO.php b/src/applications/passphrase/storage/PassphraseDAO.php new file mode 100644 index 0000000000..ccc1e519ed --- /dev/null +++ b/src/applications/passphrase/storage/PassphraseDAO.php @@ -0,0 +1,9 @@ + false, + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/passphrase/view/PassphraseCredentialControl.php b/src/applications/passphrase/view/PassphraseCredentialControl.php new file mode 100644 index 0000000000..a12cff7a57 --- /dev/null +++ b/src/applications/passphrase/view/PassphraseCredentialControl.php @@ -0,0 +1,101 @@ +allowNull = $allow_null; + return $this; + } + + public function setDefaultUsername($default_username) { + $this->defaultUsername = $default_username; + return $this; + } + + public function setCredentialType($credential_type) { + $this->credentialType = $credential_type; + return $this; + } + + public function getCredentialType() { + return $this->credentialType; + } + + public function setOptions(array $options) { + assert_instances_of($options, 'PassphraseCredential'); + $this->options = $options; + return $this; + } + + protected function getCustomControlClass() { + return 'passphrase-credential-control'; + } + + protected function renderInput() { + + $options_map = array(); + foreach ($this->options as $option) { + $options_map[$option->getPHID()] = pht( + "%s %s", + 'K'.$option->getID(), + $option->getName()); + } + + $disabled = $this->getDisabled(); + if ($this->allowNull) { + $options_map = array('' => pht('(No Credentials)')) + $options_map; + } else { + if (!$options_map) { + $options_map[''] = pht('(No Existing Credentials)'); + $disabled = true; + } + } + + Javelin::initBehavior('passphrase-credential-control'); + + $options = AphrontFormSelectControl::renderSelectTag( + $this->getValue(), + $options_map, + array( + 'id' => $this->getControlID(), + 'name' => $this->getName(), + 'disabled' => $disabled ? 'disabled' : null, + 'sigil' => 'passphrase-credential-select', + )); + + if ($this->credentialType) { + $button = javelin_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button grey', + 'sigil' => 'passphrase-credential-add', + 'mustcapture' => true, + ), + pht('Add Credential')); + } else { + $button = null; + } + + return javelin_tag( + 'div', + array( + 'sigil' => 'passphrase-credential-control', + 'meta' => array( + 'type' => $this->getCredentialType(), + 'username' => $this->defaultUsername, + 'allowNull' => $this->allowNull, + ), + ), + array( + $options, + $button, + )); + } + +} diff --git a/src/applications/paste/application/PhabricatorApplicationPaste.php b/src/applications/paste/application/PhabricatorApplicationPaste.php index 846dc86cae..b3d10bbf7e 100644 --- a/src/applications/paste/application/PhabricatorApplicationPaste.php +++ b/src/applications/paste/application/PhabricatorApplicationPaste.php @@ -18,10 +18,6 @@ final class PhabricatorApplicationPaste extends PhabricatorApplication { return self::GROUP_UTILITIES; } - public function getQuickCreateURI() { - return $this->getBaseURI().'create/'; - } - public function getRemarkupRules() { return array( new PhabricatorPasteRemarkupRule(), @@ -50,4 +46,16 @@ final class PhabricatorApplicationPaste extends PhabricatorApplication { ); } + public function getQuickCreateItems(PhabricatorUser $viewer) { + $items = array(); + + $item = id(new PHUIListItemView()) + ->setName(pht('Paste')) + ->setAppIcon('paste-dark') + ->setHref($this->getBaseURI().'create/'); + $items[] = $item; + + return $items; + } + } diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php index 185b4b0337..394d62eae1 100644 --- a/src/applications/paste/controller/PhabricatorPasteEditController.php +++ b/src/applications/paste/controller/PhabricatorPasteEditController.php @@ -123,13 +123,6 @@ final class PhabricatorPasteEditController extends PhabricatorPasteController { } } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('A Fatal Omission!')) - ->setErrors($errors); - } - $form = new AphrontFormView(); $langs = array( @@ -207,18 +200,14 @@ final class PhabricatorPasteEditController extends PhabricatorPasteController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); if (!$is_create) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('P'.$paste->getID()) - ->setHref('/P'.$paste->getID())); + $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView())->setName($short)); + $crumbs->addTextCrumb($short); return $this->buildApplicationPage( array( diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index c74937f6db..be756de590 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -88,10 +88,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) ->setActionList($actions) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('P'.$paste->getID()) - ->setHref('/P'.$paste->getID())); + ->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); $xactions = id(new PhabricatorPasteTransactionQuery()) ->setViewer($request->getUser()) @@ -117,11 +114,9 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $add_comment_header = id(new PHUIHeaderView()) - ->setHeader( - $is_serious - ? pht('Add Comment') - : pht('Debate Paste Accuracy')); + $add_comment_header = $is_serious + ? pht('Add Comment') + : pht('Debate Paste Accuracy'); $submit_button_name = $is_serious ? pht('Add Comment') @@ -133,21 +128,17 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { ->setUser($user) ->setObjectPHID($paste->getPHID()) ->setDraft($draft) + ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$paste->getID().'/')) ->setSubmitButtonName($submit_button_name); - $comment_box = id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($add_comment_header) - ->appendChild($add_comment_form); - return $this->buildApplicationPage( array( $crumbs, $object_box, $source_code, $timeline, - $comment_box, + $add_comment_form, ), array( 'title' => $paste->getFullName(), @@ -157,8 +148,10 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { } private function buildHeaderView(PhabricatorPaste $paste) { + $title = (nonempty($paste->getTitle())) ? + $paste->getTitle() : pht('(An Untitled Masterwork)'); $header = id(new PHUIHeaderView()) - ->setHeader($paste->getTitle()) + ->setHeader($title) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($paste); diff --git a/src/applications/people/application/PhabricatorApplicationPeople.php b/src/applications/people/application/PhabricatorApplicationPeople.php index cdbd347a11..8ea84fd95d 100644 --- a/src/applications/people/application/PhabricatorApplicationPeople.php +++ b/src/applications/people/application/PhabricatorApplicationPeople.php @@ -41,6 +41,8 @@ final class PhabricatorApplicationPeople extends PhabricatorApplication { '/people/' => array( '(query/(?P[^/]+)/)?' => 'PhabricatorPeopleListController', 'logs/' => 'PhabricatorPeopleLogsController', + 'approve/(?P[1-9]\d*)/' => 'PhabricatorPeopleApproveController', + 'disable/(?P[1-9]\d*)/' => 'PhabricatorPeopleDisableController', 'edit/(?:(?P[1-9]\d*)/(?:(?P\w+)/)?)?' => 'PhabricatorPeopleEditController', 'ldap/' => 'PhabricatorPeopleLdapController', @@ -60,19 +62,54 @@ final class PhabricatorApplicationPeople extends PhabricatorApplication { ); } + + protected function getCustomCapabilities() { + return array( + PeopleCapabilityBrowseUserDirectory::CAPABILITY => array( + ), + ); + } + + public function loadStatus(PhabricatorUser $user) { + if (!$user->getIsAdmin()) { + return array(); + } + + $need_approval = id(new PhabricatorPeopleQuery()) + ->setViewer($user) + ->withIsApproved(false) + ->execute(); + + if (!$need_approval) { + return array(); + } + + $status = array(); + + $count = count($need_approval); + $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; + $status[] = id(new PhabricatorApplicationStatusView()) + ->setType($type) + ->setText(pht('%d User(s) Need Approval', $count)) + ->setCount($count); + + return $status; + } + public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { $items = array(); - if ($user->isLoggedIn()) { + if ($user->isLoggedIn() && $user->isUserActivated()) { $image = $user->loadProfileImageURI(); - $item = new PHUIListItemView(); - $item->setName($user->getUsername()); - $item->setHref('/p/'.$user->getUsername().'/'); - $item->addClass('core-menu-item'); + $item = id(new PHUIListItemView()) + ->setName($user->getUsername()) + ->setHref('/p/'.$user->getUsername().'/') + ->addClass('core-menu-item') + ->setOrder(100); $classes = array( 'phabricator-core-menu-icon', diff --git a/src/applications/people/capability/PeopleCapabilityBrowseUserDirectory.php b/src/applications/people/capability/PeopleCapabilityBrowseUserDirectory.php new file mode 100644 index 0000000000..1f4fee9ce1 --- /dev/null +++ b/src/applications/people/capability/PeopleCapabilityBrowseUserDirectory.php @@ -0,0 +1,25 @@ +getIsApproved()) { + $roles[] = 'approved'; + } + + if ($user->isUserActivated()) { + $roles[] = 'activated'; + } + $return = array( 'phid' => $user->getPHID(), 'userName' => $user->getUserName(), diff --git a/src/applications/people/conduit/ConduitAPI_user_find_Method.php b/src/applications/people/conduit/ConduitAPI_user_find_Method.php index ab2dadf102..b5e1b06966 100644 --- a/src/applications/people/conduit/ConduitAPI_user_find_Method.php +++ b/src/applications/people/conduit/ConduitAPI_user_find_Method.php @@ -1,19 +1,23 @@ 'required nonempty list' + 'aliases' => 'required list' ); } @@ -27,9 +31,10 @@ final class ConduitAPI_user_find_Method } protected function execute(ConduitAPIRequest $request) { - $users = id(new PhabricatorUser())->loadAllWhere( - 'username in (%Ls)', - $request->getValue('aliases')); + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($request->getUser()) + ->withUsernames($request->getValue('aliases', array())) + ->execute(); return mpull($users, 'getPHID', 'getUsername'); } diff --git a/src/applications/people/controller/PhabricatorPeopleApproveController.php b/src/applications/people/controller/PhabricatorPeopleApproveController.php new file mode 100644 index 0000000000..23d43da28d --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleApproveController.php @@ -0,0 +1,66 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + + $request = $this->getRequest(); + $admin = $request->getUser(); + + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($admin) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$user) { + return new Aphront404Response(); + } + + $done_uri = $this->getApplicationURI('query/approval/'); + + if ($request->isFormPost()) { + id(new PhabricatorUserEditor()) + ->setActor($admin) + ->approveUser($user, true); + + $title = pht( + 'Phabricator Account "%s" Approved', + $user->getUsername(), + $admin->getUsername()); + + $body = pht( + "Your Phabricator account (%s) has been approved by %s. You can ". + "login here:\n\n %s\n\n", + $user->getUsername(), + $admin->getUsername(), + PhabricatorEnv::getProductionURI('/')); + + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(array($user->getPHID())) + ->addCCs(array($admin->getPHID())) + ->setSubject('[Phabricator] '.$title) + ->setBody($body) + ->saveAndSend(); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($admin) + ->setTitle(pht('Confirm Approval')) + ->appendChild( + pht( + 'Allow %s to access this Phabricator install?', + phutil_tag('strong', array(), $user->getUsername()))) + ->addCancelButton($done_uri) + ->addSubmitButton(pht('Approve Account')); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } +} diff --git a/src/applications/people/controller/PhabricatorPeopleDisableController.php b/src/applications/people/controller/PhabricatorPeopleDisableController.php new file mode 100644 index 0000000000..16fab2e10c --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleDisableController.php @@ -0,0 +1,48 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + + $request = $this->getRequest(); + $admin = $request->getUser(); + + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($admin) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$user) { + return new Aphront404Response(); + } + + $done_uri = $this->getApplicationURI('query/approval/'); + + if ($request->isFormPost()) { + id(new PhabricatorUserEditor()) + ->setActor($admin) + ->disableUser($user, true); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($admin) + ->setTitle(pht('Confirm Disable')) + ->appendChild( + pht( + 'Disable %s? They will no longer be able to access Phabricator or '. + 'receive email.', + phutil_tag('strong', array(), $user->getUsername()))) + ->addCancelButton($done_uri) + ->addSubmitButton(pht('Disable Account')); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } +} diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php index f952899e89..39aa5564da 100644 --- a/src/applications/people/controller/PhabricatorPeopleEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleEditController.php @@ -23,21 +23,12 @@ final class PhabricatorPeopleEditController return new Aphront404Response(); } $base_uri = '/people/edit/'.$user->getID().'/'; - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit User')) - ->setHref('/people/edit/')); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getFullName()) - ->setHref($base_uri)); + $crumbs->addTextCrumb(pht('Edit User'), '/people/edit/'); + $crumbs->addTextCrumb($user->getFullName(), $base_uri); } else { $user = new PhabricatorUser(); $base_uri = '/people/edit/'; - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create New User')) - ->setHref($base_uri)); + $crumbs->addTextCrumb(pht('Create New User'), $base_uri); } $nav = new AphrontSideNavFilterView(); @@ -182,6 +173,9 @@ final class PhabricatorPeopleEditController ->setAddress($new_email) ->setIsVerified(0); + // Automatically approve the user, since an admin is creating them. + $user->setIsApproved(1); + id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email); @@ -220,13 +214,6 @@ final class PhabricatorPeopleEditController } } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $form = new AphrontFormView(); $form->setUser($admin); if ($user->getID()) { @@ -325,7 +312,9 @@ final class PhabricatorPeopleEditController if ($user->getIsDisabled()) { $roles[] = pht('Disabled'); } - + if (!$user->getIsApproved()) { + $roles[] = pht('Not Approved'); + } if (!$roles) { $roles[] = pht('Normal User'); } @@ -351,7 +340,7 @@ final class PhabricatorPeopleEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return array($form_box); @@ -367,9 +356,9 @@ final class PhabricatorPeopleEditController if ($request->isFormPost()) { - $log_template = PhabricatorUserLog::newLog( + $log_template = PhabricatorUserLog::initializeNewLog( $admin, - $user, + $user->getPHID(), null); $logs = array(); @@ -400,22 +389,14 @@ final class PhabricatorPeopleEditController } } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - - $form = id(new AphrontFormView()) ->setUser($admin) ->setAction($request->getRequestURI()->alter('saved', null)); if ($is_self) { $inst = pht('NOTE: You can not edit your own role.'); - $form->appendChild(hsprintf( - '

    %s

    ', $inst)); + $form->appendChild( + phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); } $form @@ -456,7 +437,7 @@ final class PhabricatorPeopleEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return array($form_box); @@ -473,8 +454,8 @@ final class PhabricatorPeopleEditController $form ->setUser($admin) ->setAction($request->getRequestURI()) - ->appendChild(hsprintf( - '

    %s

    ', $inst)); + ->appendChild( + phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); if ($user->getIsSystemAgent()) { $form @@ -541,14 +522,6 @@ final class PhabricatorPeopleEditController } } - if ($errors) { - $errors = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } else { - $errors = null; - } - $inst1 = pht('Be careful when renaming users!'); $inst2 = pht('The old username will no longer be tied to the user, so '. 'anything which uses it (like old commit messages) will no longer '. @@ -594,7 +567,7 @@ final class PhabricatorPeopleEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Change Username')) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); return array($form_box); @@ -640,14 +613,6 @@ final class PhabricatorPeopleEditController } } - if ($errors) { - $errors = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } else { - $errors = null; - } - $str1 = pht('Be careful when deleting users!'); $str2 = pht('If this user interacted with anything, it is generally '. 'better to disable them, not delete them. If you delete them, it will '. @@ -687,7 +652,7 @@ final class PhabricatorPeopleEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Delete User')) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); return array($form_box); @@ -703,11 +668,10 @@ final class PhabricatorPeopleEditController ), pht('User Guide: Account Roles')); - $inst = pht('For a detailed explanation of account roles, see %s.', - $roles_link); - return hsprintf( - '

    %s

    ', - $inst); + return phutil_tag( + 'p', + array('class' => 'aphront-form-instructions'), + pht('For a detailed explanation of account roles, see %s.', $roles_link)); } private function processSetAccountPicture(PhabricatorUser $user) { diff --git a/src/applications/people/controller/PhabricatorPeopleLdapController.php b/src/applications/people/controller/PhabricatorPeopleLdapController.php index ee6e2cd8d1..280971bf06 100644 --- a/src/applications/people/controller/PhabricatorPeopleLdapController.php +++ b/src/applications/people/controller/PhabricatorPeopleLdapController.php @@ -38,10 +38,9 @@ final class PhabricatorPeopleLdapController ->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Import Ldap Users')) - ->setHref($this->getApplicationURI('/ldap/'))); + $crumbs->addTextCrumb( + pht('Import Ldap Users'), + $this->getApplicationURI('/ldap/')); $nav = $this->buildSideNavView(); $nav->setCrumbs($crumbs); diff --git a/src/applications/people/controller/PhabricatorPeopleListController.php b/src/applications/people/controller/PhabricatorPeopleListController.php index 878ca311ca..d5d204f91a 100644 --- a/src/applications/people/controller/PhabricatorPeopleListController.php +++ b/src/applications/people/controller/PhabricatorPeopleListController.php @@ -19,6 +19,11 @@ final class PhabricatorPeopleListController extends PhabricatorPeopleController public function processRequest() { $request = $this->getRequest(); + $viewer = $request->getUser(); + + $this->requireApplicationCapability( + PeopleCapabilityBrowseUserDirectory::CAPABILITY); + $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->key) ->setSearchEngine(new PhabricatorPeopleSearchEngine()) @@ -38,6 +43,8 @@ final class PhabricatorPeopleListController extends PhabricatorPeopleController $list = new PHUIObjectItemListView(); + $is_approval = ($query->getQueryKey() == 'approval'); + foreach ($users as $user) { $primary_email = $user->loadPrimaryEmail(); if ($primary_email && $primary_email->getIsVerified()) { @@ -61,6 +68,12 @@ final class PhabricatorPeopleListController extends PhabricatorPeopleController $item->addIcon('disable', pht('Disabled')); } + if (!$is_approval) { + if (!$user->getIsApproved()) { + $item->addIcon('perflab-grey', pht('Needs Approval')); + } + } + if ($user->getIsAdmin()) { $item->addIcon('highlight', pht('Admin')); } @@ -70,11 +83,26 @@ final class PhabricatorPeopleListController extends PhabricatorPeopleController } if ($viewer->getIsAdmin()) { - $uid = $user->getID(); - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('edit') - ->setHref($this->getApplicationURI('edit/'.$uid.'/'))); + $user_id = $user->getID(); + if ($is_approval) { + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('disable') + ->setName(pht('Disable')) + ->setWorkflow(true) + ->setHref($this->getApplicationURI('disable/'.$user_id.'/'))); + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('like') + ->setName(pht('Approve')) + ->setWorkflow(true) + ->setHref($this->getApplicationURI('approve/'.$user_id.'/'))); + } else { + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('edit') + ->setHref($this->getApplicationURI('edit/'.$user_id.'/'))); + } } $list->addItem($item); diff --git a/src/applications/people/controller/PhabricatorPeopleLogsController.php b/src/applications/people/controller/PhabricatorPeopleLogsController.php index f43be11c23..8cd7ad57a0 100644 --- a/src/applications/people/controller/PhabricatorPeopleLogsController.php +++ b/src/applications/people/controller/PhabricatorPeopleLogsController.php @@ -207,10 +207,7 @@ final class PhabricatorPeopleLogsController $filter = new AphrontListFilterView(); $filter->appendChild($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Activity Logs')) - ->setHref('/people/logs/')); + $crumbs->addTextCrumb(pht('Activity Logs'), '/people/logs/'); $nav = $this->buildSideNavView(); $nav->selectFilter('logs'); diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 5098438f50..9bb4225e5e 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -71,9 +71,7 @@ final class PhabricatorPeopleProfileController $properties = $this->buildPropertyView($user, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getUsername())); + $crumbs->addTextCrumb($user->getUsername()); $feed = $this->renderUserFeed($user); $object_box = id(new PHUIObjectBoxView()) @@ -127,10 +125,8 @@ final class PhabricatorPeopleProfileController $builder->setShowHovercards(true); $view = $builder->buildView(); - return hsprintf( - '
    - %s -
    ', + return phutil_tag_div( + 'profile-feed', $view->render()); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileEditController.php b/src/applications/people/controller/PhabricatorPeopleProfileEditController.php index f341f1db78..5c5ff58912 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileEditController.php @@ -61,13 +61,8 @@ final class PhabricatorPeopleProfileEditController $title = pht('Edit Profile'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getUsername()) - ->setHref($profile_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($user->getUsername(), $profile_uri); + $crumbs->addTextCrumb($title); $form = id(new AphrontFormView()) ->setUser($viewer); diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php index 169b90911a..07e12639f0 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php @@ -91,13 +91,8 @@ final class PhabricatorPeopleProfilePictureController $title = pht('Edit Profile Picture'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getUsername()) - ->setHref($profile_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($user->getUsername(), $profile_uri); + $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); @@ -263,7 +258,7 @@ final class PhabricatorPeopleProfilePictureController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); $upload_form = id(new AphrontFormView()) @@ -281,15 +276,6 @@ final class PhabricatorPeopleProfilePictureController ->addCancelButton($profile_uri) ->setValue(pht('Upload Picture'))); - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setFormError($errors) - ->setForm($form); - $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); diff --git a/src/applications/people/customfield/PhabricatorUserRolesField.php b/src/applications/people/customfield/PhabricatorUserRolesField.php index dca0b3aaf3..add52385a2 100644 --- a/src/applications/people/customfield/PhabricatorUserRolesField.php +++ b/src/applications/people/customfield/PhabricatorUserRolesField.php @@ -31,6 +31,9 @@ final class PhabricatorUserRolesField if ($user->getIsDisabled()) { $roles[] = pht('Disabled'); } + if (!$user->getIsApproved()) { + $roles[] = pht('Not Approved'); + } if ($user->getIsSystemAgent()) { $roles[] = pht('Bot'); } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 768c618917..ff253b4fc8 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -41,6 +41,12 @@ final class PhabricatorUserEditor extends PhabricatorEditor { // Always set a new user's email address to primary. $email->setIsPrimary(1); + // If the primary address is already verified, also set the verified flag + // on the user themselves. + if ($email->getIsVerified()) { + $user->setIsEmailVerified(1); + } + $this->willAddEmail($email); $user->openTransaction(); @@ -58,9 +64,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw $ex; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_CREATE); $log->setNewValue($email->getAddress()); $log->save(); @@ -87,9 +93,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $email->save(); } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EDIT); $log->save(); @@ -117,9 +123,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->setPassword($envelope, $blender_import); $user->save(); - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_PASSWORD); $log->save(); @@ -156,9 +162,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw $ex; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_USERNAME); $log->setOldValue($old_username); $log->setNewValue($username); @@ -193,14 +199,14 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_ADMIN); $log->setOldValue($user->getIsAdmin()); $log->setNewValue($admin); - $user->setIsAdmin($admin); + $user->setIsAdmin((int)$admin); $user->save(); $log->save(); @@ -231,9 +237,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_SYSTEM_AGENT); $log->setOldValue($user->getIsSystemAgent()); $log->setNewValue($system_agent); @@ -270,14 +276,14 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_DISABLE); $log->setOldValue($user->getIsDisabled()); $log->setNewValue($disable); - $user->setIsDisabled($disable); + $user->setIsDisabled((int)$disable); $user->save(); $log->save(); @@ -289,6 +295,44 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } + /** + * @task role + */ + public function approveUser(PhabricatorUser $user, $approve) { + $actor = $this->requireActor(); + + if (!$user->getID()) { + throw new Exception("User has not been created yet!"); + } + + $user->openTransaction(); + $user->beginWriteLocking(); + + $user->reload(); + if ($user->getIsApproved() == $approve) { + $user->endWriteLocking(); + $user->killTransaction(); + return $this; + } + + $log = PhabricatorUserLog::initializeNewLog( + $actor, + $user->getPHID(), + PhabricatorUserLog::ACTION_APPROVE); + $log->setOldValue($user->getIsApproved()); + $log->setNewValue($approve); + + $user->setIsApproved($approve); + $user->save(); + + $log->save(); + + $user->endWriteLocking(); + $user->saveTransaction(); + + return $this; + } + /** * @task role */ @@ -339,9 +383,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $email->delete(); } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_DELETE); $log->save(); @@ -392,9 +436,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw $ex; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_ADD); $log->setNewValue($email->getAddress()); $log->save(); @@ -437,9 +481,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $email->delete(); - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_REMOVE); $log->setOldValue($email->getAddress()); $log->save(); @@ -493,9 +537,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $email->setIsPrimary(1); $email->save(); - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_PRIMARY); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); diff --git a/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php b/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php index 31ebf778c2..380e446b64 100644 --- a/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php +++ b/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php @@ -35,6 +35,8 @@ final class PhabricatorPeopleHovercardEventListener if ($user->getIsDisabled()) { $hovercard->addField(pht('Account'), pht('Disabled')); + } else if (!$user->isUserActivated()) { + $hovercard->addField(pht('Account'), pht('Not Activated')); } else { $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( array($user->getPHID())); diff --git a/src/applications/people/phid/PhabricatorPeoplePHIDTypeExternal.php b/src/applications/people/phid/PhabricatorPeoplePHIDTypeExternal.php index d4e8241c19..f84d9f3a1a 100644 --- a/src/applications/people/phid/PhabricatorPeoplePHIDTypeExternal.php +++ b/src/applications/people/phid/PhabricatorPeoplePHIDTypeExternal.php @@ -37,8 +37,4 @@ final class PhabricatorPeoplePHIDTypeExternal extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/people/phid/PhabricatorPeoplePHIDTypeUser.php b/src/applications/people/phid/PhabricatorPeoplePHIDTypeUser.php index a0bce6ea47..a6df8adb15 100644 --- a/src/applications/people/phid/PhabricatorPeoplePHIDTypeUser.php +++ b/src/applications/people/phid/PhabricatorPeoplePHIDTypeUser.php @@ -38,7 +38,7 @@ final class PhabricatorPeoplePHIDTypeUser extends PhabricatorPHIDType { $handle->setFullName( $user->getUsername().' ('.$user->getRealName().')'); $handle->setImageURI($user->loadProfileImageURI()); - $handle->setDisabled($user->getIsDisabled()); + $handle->setDisabled(!$user->isUserActivated()); if ($user->hasStatus()) { $status = $user->getStatus(); $handle->setStatus($status->getTextStatus()); diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index f2c0b312bd..133e764f97 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -13,6 +13,7 @@ final class PhabricatorPeopleQuery private $isAdmin; private $isSystemAgent; private $isDisabled; + private $isApproved; private $nameLike; private $needPrimaryEmail; @@ -70,6 +71,11 @@ final class PhabricatorPeopleQuery return $this; } + public function withIsApproved($approved) { + $this->isApproved = $approved; + return $this; + } + public function withNameLike($like) { $this->nameLike = $like; return $this; @@ -249,10 +255,18 @@ final class PhabricatorPeopleQuery 'user.isAdmin = 1'); } - if ($this->isDisabled) { + if ($this->isDisabled !== null) { $where[] = qsprintf( $conn_r, - 'user.isDisabled = 1'); + 'user.isDisabled = %d', + (int)$this->isDisabled); + } + + if ($this->isApproved !== null) { + $where[] = qsprintf( + $conn_r, + 'user.isApproved = %d', + (int)$this->isApproved); } if ($this->isSystemAgent) { diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index 2c9074957a..deaaeab0d9 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -15,6 +15,7 @@ final class PhabricatorPeopleSearchEngine $saved->setParameter('isAdmin', $request->getStr('isAdmin')); $saved->setParameter('isDisabled', $request->getStr('isDisabled')); $saved->setParameter('isSystemAgent', $request->getStr('isSystemAgent')); + $saved->setParameter('needsApproval', $request->getStr('needsApproval')); $saved->setParameter('createdStart', $request->getStr('createdStart')); $saved->setParameter('createdEnd', $request->getStr('createdEnd')); @@ -40,6 +41,8 @@ final class PhabricatorPeopleSearchEngine $is_admin = $saved->getParameter('isAdmin'); $is_disabled = $saved->getParameter('isDisabled'); $is_system_agent = $saved->getParameter('isSystemAgent'); + $needs_approval = $saved->getParameter('needsApproval'); + $no_disabled = $saved->getParameter('noDisabled'); if ($is_admin) { $query->withIsAdmin(true); @@ -47,12 +50,18 @@ final class PhabricatorPeopleSearchEngine if ($is_disabled) { $query->withIsDisabled(true); + } else if ($no_disabled) { + $query->withIsDisabled(false); } if ($is_system_agent) { $query->withIsSystemAgent(true); } + if ($needs_approval) { + $query->withIsApproved(false); + } + $start = $this->parseDateTime($saved->getParameter('createdStart')); $end = $this->parseDateTime($saved->getParameter('createdEnd')); @@ -79,6 +88,7 @@ final class PhabricatorPeopleSearchEngine $is_admin = $saved->getParameter('isAdmin'); $is_disabled = $saved->getParameter('isDisabled'); $is_system_agent = $saved->getParameter('isSystemAgent'); + $needs_approval = $saved->getParameter('needsApproval'); $form ->appendChild( @@ -108,7 +118,12 @@ final class PhabricatorPeopleSearchEngine 'isSystemAgent', 1, pht('Show only System Agents.'), - $is_system_agent)); + $is_system_agent) + ->addCheckbox( + 'needsApproval', + 1, + pht('Show only users who need approval.'), + $needs_approval)); $this->appendCustomFieldsToForm($form, $saved); @@ -130,6 +145,11 @@ final class PhabricatorPeopleSearchEngine 'all' => pht('All'), ); + $viewer = $this->requireViewer(); + if ($viewer->getIsAdmin()) { + $names['approval'] = pht('Approval Queue'); + } + return $names; } @@ -140,6 +160,10 @@ final class PhabricatorPeopleSearchEngine switch ($query_key) { case 'all': return $query; + case 'approval': + return $query + ->setParameter('needsApproval', true) + ->setParameter('noDisabled', true); } return parent::buildSavedQueryFromBuiltin($query_key); diff --git a/src/applications/people/remarkup/PhabricatorRemarkupRuleMention.php b/src/applications/people/remarkup/PhabricatorRemarkupRuleMention.php index 31d2f4a2cf..ada30a05fd 100644 --- a/src/applications/people/remarkup/PhabricatorRemarkupRuleMention.php +++ b/src/applications/people/remarkup/PhabricatorRemarkupRuleMention.php @@ -101,22 +101,22 @@ final class PhabricatorRemarkupRuleMention $user = $actual_users[$username]; Javelin::initBehavior('phabricator-hovercards'); - $tag = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_PERSON) + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_PERSON) ->setPHID($user->getPHID()) ->setName('@'.$user->getUserName()) ->setHref('/p/'.$user->getUserName().'/'); - if ($user->getIsDisabled()) { - $tag->setDotColor(PhabricatorTagView::COLOR_GREY); + if (!$user->isUserActivated()) { + $tag->setDotColor(PHUITagView::COLOR_GREY); } else { $status = idx($user_statuses, $user->getPHID()); if ($status) { $status = $status->getStatus(); if ($status == PhabricatorUserStatus::STATUS_AWAY) { - $tag->setDotColor(PhabricatorTagView::COLOR_RED); + $tag->setDotColor(PHUITagView::COLOR_RED); } else if ($status == PhabricatorUserStatus::STATUS_AWAY) { - $tag->setDotColor(PhabricatorTagView::COLOR_ORANGE); + $tag->setDotColor(PHUITagView::COLOR_ORANGE); } } } diff --git a/src/applications/people/search/PhabricatorUserSearchIndexer.php b/src/applications/people/search/PhabricatorUserSearchIndexer.php index 8e7cd6984d..9a8c069610 100644 --- a/src/applications/people/search/PhabricatorUserSearchIndexer.php +++ b/src/applications/people/search/PhabricatorUserSearchIndexer.php @@ -20,7 +20,7 @@ final class PhabricatorUserSearchIndexer // TODO: Index the blurbs from their profile or something? Probably not // actually useful... - if (!$user->getIsDisabled()) { + if ($user->isUserActivated()) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_OPEN, $user->getPHID(), diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 79c195f43b..aa85462527 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -29,6 +29,8 @@ final class PhabricatorUser protected $isSystemAgent = 0; protected $isAdmin = 0; protected $isDisabled = 0; + protected $isEmailVerified = 0; + protected $isApproved = 0; private $profileImage = null; private $profile = null; @@ -37,6 +39,8 @@ final class PhabricatorUser private $omnipotent = false; private $customFields = self::ATTACHABLE; + private $alternateCSRFString = self::ATTACHABLE; + protected function readField($field) { switch ($field) { case 'timezoneIdentifier': @@ -51,11 +55,41 @@ final class PhabricatorUser return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; + case 'isEmailVerified': + return (bool)$this->isEmailVerified; + case 'isApproved': + return (bool)$this->isApproved; default: return parent::readField($field); } } + + /** + * Is this a live account which has passed required approvals? Returns true + * if this is an enabled, verified (if required), approved (if required) + * account, and false otherwise. + * + * @return bool True if this is a standard, usable account. + */ + public function isUserActivated() { + if ($this->getIsDisabled()) { + return false; + } + + if (!$this->getIsApproved()) { + return false; + } + + if (PhabricatorUserEmail::isEmailVerificationRequired()) { + if (!$this->getIsEmailVerified()) { + return false; + } + } + + return true; + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -190,7 +224,7 @@ final class PhabricatorUser $this->updateNameTokens(); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($this->getPHID()); + ->queueDocumentForIndexing($this->getPHID()); return $result; } @@ -265,12 +299,7 @@ final class PhabricatorUser } public function validateCSRFToken($token) { - if (!$this->getPHID()) { - return true; - } - $salt = null; - $version = 'plain'; // This is a BREACH-mitigating token. See T3684. @@ -335,175 +364,18 @@ final class PhabricatorUser } private function generateToken($epoch, $frequency, $key, $len) { + if ($this->getPHID()) { + $vec = $this->getPHID().$this->getPasswordHash(); + } else { + $vec = $this->getAlternateCSRFString(); + } + $time_block = floor($epoch / $frequency); - $vec = $this->getPHID().$this->getPasswordHash().$key.$time_block; + $vec = $vec.$key.$time_block; + return substr(PhabricatorHash::digest($vec), 0, $len); } - /** - * Issue a new session key to this user. Phabricator supports different - * types of sessions (like "web" and "conduit") and each session type may - * have multiple concurrent sessions (this allows a user to be logged in on - * multiple browsers at the same time, for instance). - * - * Note that this method is transport-agnostic and does not set cookies or - * issue other types of tokens, it ONLY generates a new session key. - * - * You can configure the maximum number of concurrent sessions for various - * session types in the Phabricator configuration. - * - * @param string Session type, like "web". - * @return string Newly generated session key. - */ - public function establishSession($session_type) { - $conn_w = $this->establishConnection('w'); - - if (strpos($session_type, '-') !== false) { - throw new Exception("Session type must not contain hyphen ('-')!"); - } - - // We allow multiple sessions of the same type, so when a caller requests - // a new session of type "web", we give them the first available session in - // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none - // of these sessions is available, we overwrite the oldest session and - // reissue a new one in its place. - - $session_limit = 1; - switch ($session_type) { - case 'web': - $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); - break; - case 'conduit': - $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); - break; - default: - throw new Exception("Unknown session type '{$session_type}'!"); - } - - $session_limit = (int)$session_limit; - if ($session_limit <= 0) { - throw new Exception( - "Session limit for '{$session_type}' must be at least 1!"); - } - - // NOTE: Session establishment is sensitive to race conditions, as when - // piping `arc` to `arc`: - // - // arc export ... | arc paste ... - // - // To avoid this, we overwrite an old session only if it hasn't been - // re-established since we read it. - - // Consume entropy to generate a new session key, forestalling the eventual - // heat death of the universe. - $session_key = Filesystem::readRandomCharacters(40); - - // Load all the currently active sessions. - $sessions = queryfx_all( - $conn_w, - 'SELECT type, sessionKey, sessionStart FROM %T - WHERE userPHID = %s AND type LIKE %>', - PhabricatorUser::SESSION_TABLE, - $this->getPHID(), - $session_type.'-'); - $sessions = ipull($sessions, null, 'type'); - $sessions = isort($sessions, 'sessionStart'); - - $existing_sessions = array_keys($sessions); - - // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet. - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - $retries = 0; - while (true) { - - - // Choose which 'type' we'll actually establish, i.e. what number we're - // going to append to the basic session type. To do this, just check all - // the numbers sequentially until we find an available session. - $establish_type = null; - for ($ii = 1; $ii <= $session_limit; $ii++) { - $try_type = $session_type.'-'.$ii; - if (!in_array($try_type, $existing_sessions)) { - $establish_type = $try_type; - $expect_key = PhabricatorHash::digest($session_key); - $existing_sessions[] = $try_type; - - // Ensure the row exists so we can issue an update below. We don't - // care if we race here or not. - queryfx( - $conn_w, - 'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart) - VALUES (%s, %s, %s, 0)', - self::SESSION_TABLE, - $this->getPHID(), - $establish_type, - PhabricatorHash::digest($session_key)); - break; - } - } - - // If we didn't find an available session, choose the oldest session and - // overwrite it. - if (!$establish_type) { - $oldest = reset($sessions); - $establish_type = $oldest['type']; - $expect_key = $oldest['sessionKey']; - } - - // This is so that we'll only overwrite the session if it hasn't been - // refreshed since we read it. If it has, the session key will be - // different and we know we're racing other processes. Whichever one - // won gets the session, we go back and try again. - - queryfx( - $conn_w, - 'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP() - WHERE userPHID = %s AND type = %s AND sessionKey = %s', - self::SESSION_TABLE, - PhabricatorHash::digest($session_key), - $this->getPHID(), - $establish_type, - $expect_key); - - if ($conn_w->getAffectedRows()) { - // The update worked, so the session is valid. - break; - } else { - // We know this just got grabbed, so don't try it again. - unset($sessions[$establish_type]); - } - - if (++$retries > $session_limit) { - throw new Exception("Failed to establish a session!"); - } - } - - $log = PhabricatorUserLog::newLog( - $this, - $this, - PhabricatorUserLog::ACTION_LOGIN); - $log->setDetails( - array( - 'session_type' => $session_type, - 'session_issued' => $establish_type, - )); - $log->setSession($session_key); - $log->save(); - - return $session_key; - } - - public function destroySession($session_key) { - $conn_w = $this->establishConnection('w'); - queryfx( - $conn_w, - 'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s', - self::SESSION_TABLE, - $this->getPHID(), - PhabricatorHash::digest($session_key)); - } - private function generateEmailToken( PhabricatorUserEmail $email, $offset = 0) { @@ -543,8 +415,18 @@ final class PhabricatorUser } } $token = $this->generateEmailToken($email); - $uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/'); + + $uri = '/login/etoken/'.$token.'/'; + try { + $uri = PhabricatorEnv::getProductionURI($uri); + } catch (Exception $ex) { + // If a user runs `bin/auth recover` before configuring the base URI, + // just show the path. We don't have any way to figure out the domain. + // See T4132. + } + $uri = new PhutilURI($uri); + return $uri->alter('email', $email->getAddress()); } @@ -641,6 +523,15 @@ final class PhabricatorUser } } + public function getAlternateCSRFString() { + return $this->assertAttached($this->alternateCSRFString); + } + + public function attachAlternateCSRFString($string) { + $this->alternateCSRFString = $string; + return $this; + } + private static function tokenizeName($name) { if (function_exists('mb_strtolower')) { $name = mb_strtolower($name, 'UTF-8'); diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index 20c35bbdec..b82799ae6f 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -13,6 +13,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO { const ACTION_ADMIN = 'admin'; const ACTION_SYSTEM_AGENT = 'system-agent'; const ACTION_DISABLE = 'disable'; + const ACTION_APPROVE = 'approve'; const ACTION_DELETE = 'delete'; const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert'; @@ -34,9 +35,9 @@ final class PhabricatorUserLog extends PhabricatorUserDAO { protected $remoteAddr; protected $session; - public static function newLog( + public static function initializeNewLog( PhabricatorUser $actor = null, - PhabricatorUser $user = null, + $object_phid, $action) { $log = new PhabricatorUserLog(); @@ -45,15 +46,8 @@ final class PhabricatorUserLog extends PhabricatorUserDAO { $log->setActorPHID($actor->getPHID()); } - if ($user) { - $log->setUserPHID($user->getPHID()); - } else { - $log->setUserPHID(''); - } - - if ($action) { - $log->setAction($action); - } + $log->setUserPHID((string)$object_phid); + $log->setAction($action); return $log; } @@ -72,7 +66,10 @@ final class PhabricatorUserLog extends PhabricatorUserDAO { $this->remoteAddr = idx($_SERVER, 'REMOTE_ADDR', ''); } if (!$this->session) { - $this->setSession(idx($_COOKIE, 'phsid')); + // TODO: This is not correct if there's a cookie prefix. This object + // should take an AphrontRequest. + // TODO: Maybe record session kind, or drop this for anonymous sessions? + $this->setSession(idx($_COOKIE, PhabricatorCookies::COOKIE_SESSION)); } $this->details['host'] = php_uname('n'); $this->details['user_agent'] = AphrontRequest::getHTTPHeader('User-Agent'); diff --git a/src/applications/phame/celerity/PhameCelerityResources.php b/src/applications/phame/celerity/PhameCelerityResources.php new file mode 100644 index 0000000000..d7951dc731 --- /dev/null +++ b/src/applications/phame/celerity/PhameCelerityResources.php @@ -0,0 +1,28 @@ +skin = $skin; + return $this; + } + + public function getSkin() { + return $this->skin; + } + + public function getName() { + return 'phame:'.$this->getSkin()->getName(); + } + + public function getResourceData($name) { + $resource_path = $this->skin->getRootDirectory().DIRECTORY_SEPARATOR.$name; + return Filesystem::readFile($resource_path); + } + +} diff --git a/src/applications/phame/controller/PhameResourceController.php b/src/applications/phame/controller/PhameResourceController.php index dd22d3066c..f63c3d765c 100644 --- a/src/applications/phame/controller/PhameResourceController.php +++ b/src/applications/phame/controller/PhameResourceController.php @@ -9,9 +9,10 @@ final class PhameResourceController extends CelerityResourceController { private $hash; private $name; private $root; + private $celerityResourceMap; - protected function getRootDirectory() { - return $this->root; + public function getCelerityResourceMap() { + return $this->celerityResourceMap; } public function willProcessRequest(array $data) { @@ -26,9 +27,13 @@ final class PhameResourceController extends CelerityResourceController { // We require a visible blog associated with a given skin to serve // resources, so you can't go fishing around where you shouldn't be. + // However, since these resources may be served off a CDN domain, we're + // bypassing the actual policy check. The blog needs to exist, but you + // don't necessarily need to be able to see it in order to see static + // resources on it. $blog = id(new PhameBlogQuery()) - ->setViewer($user) + ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($this->id)) ->executeOne(); if (!$blog) { @@ -38,8 +43,13 @@ final class PhameResourceController extends CelerityResourceController { $skin = $blog->getSkinRenderer($request); $spec = $skin->getSpecification(); - $this->root = $spec->getRootDirectory().DIRECTORY_SEPARATOR; - return $this->serveResource($this->name, $package_hash = null); + $resources = new PhameCelerityResources(); + $resources->setSkin($spec); + + $this->root = $spec->getRootDirectory(); + $this->celerityResourceMap = new CelerityResourceMap($resources); + + return $this->serveResource($this->name); } protected function buildResourceTransformer() { diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index 1b9aac97d6..b8d4f87ccd 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -162,24 +162,13 @@ final class PhameBlogEditController ->addCancelButton($cancel_uri) ->setValue($submit_button)); - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } else { - $error_view = null; - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($page_title) - ->setHref($this->getApplicationURI('blog/new'))); + $crumbs->addTextCrumb($page_title, $this->getApplicationURI('blog/new')); $nav = $this->renderSideNavFilterView(); $nav->selectFilter($this->id ? null : 'blog/new'); diff --git a/src/applications/phame/controller/blog/PhameBlogListController.php b/src/applications/phame/controller/blog/PhameBlogListController.php index bd11cf2947..ccba6c8609 100644 --- a/src/applications/phame/controller/blog/PhameBlogListController.php +++ b/src/applications/phame/controller/blog/PhameBlogListController.php @@ -48,10 +48,7 @@ final class PhameBlogListController extends PhameController { $blog_list->setPager($pager); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); $nav->appendChild( array( diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index 7efb21b3bc..5bd9fabbbd 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -58,10 +58,7 @@ final class PhameBlogViewController extends PhameController { $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($blog->getName()) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($blog->getName(), $this->getApplicationURI()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php index 494bc4f4db..37356b80b3 100644 --- a/src/applications/phame/controller/post/PhamePostEditController.php +++ b/src/applications/phame/controller/post/PhamePostEditController.php @@ -143,17 +143,14 @@ final class PhamePostEditController ->addCancelButton($cancel_uri) ->setValue($submit_button)); - $preview_panel = hsprintf( - '
    -
    - Post Preview -
    -
    -
    - Loading preview... -
    -
    -
    '); + $loading = phutil_tag_div( + 'aphront-panel-preview-loading-text', + pht('Loading preview...')); + + $preview_panel = phutil_tag_div('aphront-panel-preview', array( + phutil_tag_div('phame-post-preview-header', pht('Post Preview')), + phutil_tag('div', array('id' => 'post-preview'), $loading), + )); require_celerity_resource('phame-css'); Javelin::initBehavior( @@ -166,24 +163,15 @@ final class PhamePostEditController 'uri' => '/phame/post/preview/', )); - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Errors saving post.')) - ->setErrors($errors); - } else { - $error_view = null; - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($page_title) - ->setHref($this->getApplicationURI('/post/view/'.$this->id.'/'))); + $crumbs->addTextCrumb( + $page_title, + $this->getApplicationURI('/post/view/'.$this->id.'/')); $nav = $this->renderSideNavFilterView(null); $nav->appendChild( diff --git a/src/applications/phame/controller/post/PhamePostListController.php b/src/applications/phame/controller/post/PhamePostListController.php index 5153c1dd33..de2418c301 100644 --- a/src/applications/phame/controller/post/PhamePostListController.php +++ b/src/applications/phame/controller/post/PhamePostListController.php @@ -78,10 +78,7 @@ final class PhamePostListController extends PhameController { ->appendChild($post_list); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); $nav->appendChild( array( diff --git a/src/applications/phame/controller/post/PhamePostNewController.php b/src/applications/phame/controller/post/PhamePostNewController.php index 7667aa5fc4..b2839c880b 100644 --- a/src/applications/phame/controller/post/PhamePostNewController.php +++ b/src/applications/phame/controller/post/PhamePostNewController.php @@ -69,10 +69,7 @@ final class PhamePostNewController extends PhameController { $nav->selectFilter('post/new'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($view_uri)); + $crumbs->addTextCrumb($title, $view_uri); $nav->appendChild($crumbs); if (!$blogs) { diff --git a/src/applications/phame/controller/post/PhamePostPreviewController.php b/src/applications/phame/controller/post/PhamePostPreviewController.php index 06fe3de4da..8353b98d72 100644 --- a/src/applications/phame/controller/post/PhamePostPreviewController.php +++ b/src/applications/phame/controller/post/PhamePostPreviewController.php @@ -23,7 +23,7 @@ extends PhameController { PhamePost::MARKUP_FIELD_BODY, $user); - $content = hsprintf('
    %s
    ', $content); + $content = phutil_tag_div('phabricator-remarkup', $content); return id(new AphrontAjaxResponse())->setContent($content); } diff --git a/src/applications/phame/controller/post/PhamePostPublishController.php b/src/applications/phame/controller/post/PhamePostPublishController.php index ce336b263a..6986c4e288 100644 --- a/src/applications/phame/controller/post/PhamePostPublishController.php +++ b/src/applications/phame/controller/post/PhamePostPublishController.php @@ -51,10 +51,7 @@ final class PhamePostPublishController extends PhameController { ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Preview')) - ->setHref($view_uri)); + $crumbs->addTextCrumb(pht('Preview'), $view_uri); $nav = $this->renderSideNavFilterView(null); $nav->appendChild( diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index 1fb600544d..bac0a370d3 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -36,10 +36,9 @@ final class PhamePostViewController extends PhameController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($post->getTitle()) - ->setHref($this->getApplicationURI('post/view/'.$post->getID().'/'))); + $crumbs->addTextCrumb( + $post->getTitle(), + $this->getApplicationURI('post/view/'.$post->getID().'/')); $nav->appendChild($crumbs); diff --git a/src/applications/phame/skins/PhameBasicBlogSkin.php b/src/applications/phame/skins/PhameBasicBlogSkin.php index 850a5f0a92..b248bc6b92 100644 --- a/src/applications/phame/skins/PhameBasicBlogSkin.php +++ b/src/applications/phame/skins/PhameBasicBlogSkin.php @@ -123,7 +123,7 @@ abstract class PhameBasicBlogSkin extends PhameBlogSkin { } protected function render404Page() { - return hsprintf('

    404 Not Found

    '); + return phutil_tag('h2', array(), pht('404 Not Found')); } final public function getResourceURI($resource) { diff --git a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php index 7c49d2ae1e..a6851b2f9a 100644 --- a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php +++ b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php @@ -29,15 +29,16 @@ final class PhameBasicTemplateBlogSkin extends PhameBasicBlogSkin { } } - $map = CelerityResourceMap::getInstance(); - $symbol_info = $map->lookupSymbolInformation('syntax-highlighting-css'); + $map = CelerityResourceMap::getNamedInstance('phabricator'); + $resource_symbol = 'syntax-highlighting-css'; + $resource_uri = $map->getURIForSymbol($resource_symbol); $this->cssResources[] = phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', - 'href' => PhabricatorEnv::getCDNURI($symbol_info['uri']), + 'href' => PhabricatorEnv::getCDNURI($resource_uri), )); $this->cssResources = phutil_implode_html("\n", $this->cssResources); diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php index 4acb6d7562..ca53e365b9 100644 --- a/src/applications/phid/PhabricatorPHIDConstants.php +++ b/src/applications/phid/PhabricatorPHIDConstants.php @@ -7,8 +7,6 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_MAGIC = '!!!!'; const PHID_TYPE_STRY = 'STRY'; const PHID_TYPE_ACMT = 'ACMT'; - const PHID_TYPE_DRYR = 'DRYR'; - const PHID_TYPE_DRYL = 'DRYL'; const PHID_TYPE_OASC = 'OASC'; const PHID_TYPE_OASA = 'OASA'; const PHID_TYPE_TOBJ = 'TOBJ'; diff --git a/src/applications/phlux/controller/PhluxEditController.php b/src/applications/phlux/controller/PhluxEditController.php index 1e847490b3..45347ede9a 100644 --- a/src/applications/phlux/controller/PhluxEditController.php +++ b/src/applications/phlux/controller/PhluxEditController.php @@ -109,11 +109,6 @@ final class PhluxEditController extends PhluxController { } } - if ($errors) { - $errors = id(new AphrontErrorView()) - ->setErrors($errors); - } - $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($var) @@ -164,21 +159,15 @@ final class PhluxEditController extends PhluxController { if ($is_new) { $title = pht('Create Variable'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); } else { $title = pht('Edit %s', $this->key); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/phlux/controller/PhluxListController.php b/src/applications/phlux/controller/PhluxListController.php index badc28f9c0..6c4480bf50 100644 --- a/src/applications/phlux/controller/PhluxListController.php +++ b/src/applications/phlux/controller/PhluxListController.php @@ -31,10 +31,7 @@ final class PhluxListController extends PhluxController { $title = pht('Variable List'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/phlux/controller/PhluxViewController.php b/src/applications/phlux/controller/PhluxViewController.php index c492ba3660..1525ef1e1a 100644 --- a/src/applications/phlux/controller/PhluxViewController.php +++ b/src/applications/phlux/controller/PhluxViewController.php @@ -25,10 +25,7 @@ final class PhluxViewController extends PhluxController { $title = $var->getVariableKey(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader($title) diff --git a/src/applications/phlux/editor/PhluxVariableEditor.php b/src/applications/phlux/editor/PhluxVariableEditor.php index 14c289ffbf..0fc9f0319b 100644 --- a/src/applications/phlux/editor/PhluxVariableEditor.php +++ b/src/applications/phlux/editor/PhluxVariableEditor.php @@ -62,6 +62,8 @@ final class PhluxVariableEditor switch ($xaction->getTransactionType()) { case PhluxTransaction::TYPE_EDIT_KEY: case PhluxTransaction::TYPE_EDIT_VALUE: + case PhabricatorTransactions::TYPE_VIEW_POLICY: + case PhabricatorTransactions::TYPE_EDIT_POLICY: return; } return parent::applyCustomExternalTransaction($object, $xaction); diff --git a/src/applications/phlux/phid/PhluxPHIDTypeVariable.php b/src/applications/phlux/phid/PhluxPHIDTypeVariable.php index 5be0c23778..165bc6d6e7 100644 --- a/src/applications/phlux/phid/PhluxPHIDTypeVariable.php +++ b/src/applications/phlux/phid/PhluxPHIDTypeVariable.php @@ -40,8 +40,4 @@ final class PhluxPHIDTypeVariable extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/pholio/application/PhabricatorApplicationPholio.php b/src/applications/pholio/application/PhabricatorApplicationPholio.php index 138bbf4cf7..49a85f7baa 100644 --- a/src/applications/pholio/application/PhabricatorApplicationPholio.php +++ b/src/applications/pholio/application/PhabricatorApplicationPholio.php @@ -70,4 +70,23 @@ final class PhabricatorApplicationPholio extends PhabricatorApplication { ); } + public function getQuickCreateItems(PhabricatorUser $viewer) { + $items = array(); + + $item = id(new PHUIListItemView()) + ->setName(pht('Pholio Mock')) + ->setAppIcon('pholio-dark') + ->setHref($this->getBaseURI().'new/'); + $items[] = $item; + + return $items; + } + + protected function getCustomCapabilities() { + return array( + PholioCapabilityDefaultView::CAPABILITY => array( + ), + ); + } + } diff --git a/src/applications/pholio/capability/PholioCapabilityDefaultView.php b/src/applications/pholio/capability/PholioCapabilityDefaultView.php new file mode 100644 index 0000000000..77289fad18 --- /dev/null +++ b/src/applications/pholio/capability/PholioCapabilityDefaultView.php @@ -0,0 +1,20 @@ +buildApplicationCrumbs(); $crumbs - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('M'.$mock->getID()) - ->setHref('/M'.$mock->getID())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('Image History') - ->setHref($request->getRequestURI())); + ->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()) + ->addTextCrumb('Image History', $request->getRequestURI()); $content = array( $crumbs, diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index a3dde9a4b8..e475e1d32f 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -38,10 +38,7 @@ final class PholioMockEditController extends PholioController { $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { - $mock = id(new PholioMock()) - ->setAuthorPHID($user->getPHID()) - ->attachImages(array()) - ->setViewPolicy(PhabricatorPolicies::POLICY_USER); + $mock = PholioMock::initializeNewMock($user); $title = pht('Create Mock'); @@ -199,14 +196,6 @@ final class PholioMockEditController extends PholioController { } } - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } else { - $error_view = null; - } - if ($this->id) { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton('/M'.$this->id) @@ -322,14 +311,11 @@ final class PholioMockEditController extends PholioController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); $content = array( $crumbs, diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index 2d6871b6f4..e91376830c 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -98,10 +98,7 @@ final class PholioMockViewController extends PholioController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('M'.$mock->getID()) - ->setHref('/M'.$mock->getID())); + $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) @@ -239,9 +236,6 @@ final class PholioMockViewController extends PholioController { ? pht('Add Comment') : pht('History Beckons'); - $header = id(new PHUIHeaderView()) - ->setHeader($title); - $button_name = $is_serious ? pht('Add Comment') : pht('Answer The Call'); @@ -251,14 +245,12 @@ final class PholioMockViewController extends PholioController { ->setObjectPHID($mock->getPHID()) ->setFormID($comment_form_id) ->setDraft($draft) + ->setHeaderText($title) ->setSubmitButtonName($button_name) ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); - return id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($header) - ->appendChild($form); + return $form; } } diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php index 8cf5784cc4..a55cd62595 100644 --- a/src/applications/pholio/editor/PholioMockEditor.php +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -287,9 +287,12 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor { case PholioTransactionType::TYPE_DESCRIPTION: return $v; case PholioTransactionType::TYPE_IMAGE_REPLACE: - if ($u->getNewValue() == $v->getOldValue()) { + $u_img = $u->getNewValue(); + $v_img = $v->getNewValue(); + if ($u_img->getReplacesImagePHID() == $v_img->getReplacesImagePHID()) { return $v; } + break; case PholioTransactionType::TYPE_IMAGE_FILE: return $this->mergePHIDOrEdgeTransactions($u, $v); case PholioTransactionType::TYPE_IMAGE_NAME: diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 9cc8571525..740ac71a9e 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -1,8 +1,5 @@ setViewer($actor) + ->withClasses(array('PhabricatorApplicationPholio')) + ->executeOne(); + + $view_policy = $app->getPolicy(PholioCapabilityDefaultView::CAPABILITY); + + return id(new PholioMock()) + ->setAuthorPHID($actor->getPHID()) + ->attachImages(array()) + ->setViewPolicy($view_policy); + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index a98a01317b..d8875b937e 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -24,10 +24,7 @@ final class PhortuneAccountViewController extends PhortuneController { $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Account')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb(pht('Account'), $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader($title); diff --git a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php index 28059f72f0..4a82a219c5 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php @@ -159,14 +159,8 @@ final class PhortunePaymentMethodEditController ->setHeader($title); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Account')) - ->setHref($account_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Payment Methods')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb(pht('Account'), $account_uri); + $crumbs->addTextCrumb(pht('Payment Methods'), $request->getRequestURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/phortune/controller/PhortuneProductEditController.php b/src/applications/phortune/controller/PhortuneProductEditController.php index 0e93ce29e2..f62817eca0 100644 --- a/src/applications/phortune/controller/PhortuneProductEditController.php +++ b/src/applications/phortune/controller/PhortuneProductEditController.php @@ -135,14 +135,12 @@ final class PhortuneProductEditController extends PhabricatorController { $title = pht('Edit Product'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Products')) - ->setHref($this->getApplicationURI('product/'))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($is_create ? pht('Create') : pht('Edit')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb( + pht('Products'), + $this->getApplicationURI('product/')); + $crumbs->addTextCrumb( + $is_create ? pht('Create') : pht('Edit'), + $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Product')); diff --git a/src/applications/phortune/controller/PhortuneProductListController.php b/src/applications/phortune/controller/PhortuneProductListController.php index fab506ff34..9a7c22bd1e 100644 --- a/src/applications/phortune/controller/PhortuneProductListController.php +++ b/src/applications/phortune/controller/PhortuneProductListController.php @@ -17,10 +17,7 @@ final class PhortuneProductListController extends PhabricatorController { $title = pht('Product List'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('Products') - ->setHref($this->getApplicationURI('product/'))); + $crumbs->addTextCrumb('Products', $this->getApplicationURI('product/')); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Product')) diff --git a/src/applications/phortune/controller/PhortuneProductViewController.php b/src/applications/phortune/controller/PhortuneProductViewController.php index 29f8ccae98..f3c8fcf627 100644 --- a/src/applications/phortune/controller/PhortuneProductViewController.php +++ b/src/applications/phortune/controller/PhortuneProductViewController.php @@ -50,14 +50,12 @@ final class PhortuneProductViewController extends PhortuneController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Products')) - ->setHref($this->getApplicationURI('product/'))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('#%d', $product->getID())) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb( + pht('Products'), + $this->getApplicationURI('product/')); + $crumbs->addTextCrumb( + pht('#%d', $product->getID()), + $request->getRequestURI()); $properties = id(new PHUIPropertyListView()) ->setUser($user) diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 6fbb752d13..8eff081370 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -46,7 +46,12 @@ final class PhortuneAccount extends PhortuneDAO } public function getPolicy($capability) { - return PhabricatorPolicies::POLICY_NOONE; + if ($this->getPHID() === null) { + // Allow a user to create an account for themselves. + return PhabricatorPolicies::POLICY_USER; + } else { + return PhabricatorPolicies::POLICY_NOONE; + } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php index bec14d9a21..2ca3b17e96 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php @@ -14,15 +14,14 @@ final class PhabricatorXHPASTViewFramesetController $response = new AphrontWebpageResponse(); $response->setFrameable(true); - $response->setContent(hsprintf( - ''. - ''. - ''. - ''. - '', - $id, - $id, - $id)); + $response->setContent(phutil_tag( + 'frameset', + array('cols' => '33%, 34%, 33%'), + array( + phutil_tag('frame', array('src' => "/xhpast/input/{$id}/")), + phutil_tag('frame', array('src' => "/xhpast/tree/{$id}/")), + phutil_tag('frame', array('src' => "/xhpast/stream/{$id}/")), + ))); return $response; } diff --git a/src/applications/phragment/application/PhabricatorApplicationPhragment.php b/src/applications/phragment/application/PhabricatorApplicationPhragment.php new file mode 100644 index 0000000000..aaa8f3c4bc --- /dev/null +++ b/src/applications/phragment/application/PhabricatorApplicationPhragment.php @@ -0,0 +1,68 @@ + array( + '' => 'PhragmentBrowseController', + 'browse/(?P.*)' => 'PhragmentBrowseController', + 'create/(?P.*)' => 'PhragmentCreateController', + 'update/(?P.*)' => 'PhragmentUpdateController', + 'policy/(?P.*)' => 'PhragmentPolicyController', + 'history/(?P.*)' => 'PhragmentHistoryController', + 'zip/(?P.*)' => 'PhragmentZIPController', + 'zip@(?P[^/]+)/(?P.*)' => 'PhragmentZIPController', + 'version/(?P[0-9]*)/' => 'PhragmentVersionController', + 'patch/(?P[0-9x]*)/(?P[0-9]*)/' => 'PhragmentPatchController', + 'revert/(?P[0-9]*)/(?P.*)' => 'PhragmentRevertController', + 'snapshot/' => array( + 'create/(?P.*)' => 'PhragmentSnapshotCreateController', + 'view/(?P[0-9]*)/' => 'PhragmentSnapshotViewController', + 'delete/(?P[0-9]*)/' => 'PhragmentSnapshotDeleteController', + 'promote/' => array( + 'latest/(?P.*)' => 'PhragmentSnapshotPromoteController', + '(?P[0-9]*)/' => 'PhragmentSnapshotPromoteController', + ), + ), + ), + ); + } + + protected function getCustomCapabilities() { + return array( + PhragmentCapabilityCanCreate::CAPABILITY => array( + ), + ); + } + +} + diff --git a/src/applications/phragment/capability/PhragmentCapabilityCanCreate.php b/src/applications/phragment/capability/PhragmentCapabilityCanCreate.php new file mode 100644 index 0000000000..860ece8f25 --- /dev/null +++ b/src/applications/phragment/capability/PhragmentCapabilityCanCreate.php @@ -0,0 +1,20 @@ + 'required string', + 'state' => 'required dict', + ); + } + + public function defineReturnType() { + return 'nonempty dict'; + } + + public function defineErrorTypes() { + return array( + 'ERR_BAD_FRAGMENT' => 'No such fragment exists', + ); + } + + protected function execute(ConduitAPIRequest $request) { + $path = $request->getValue('path'); + $state = $request->getValue('state'); + // The state is an array mapping file paths to hashes. + + $patches = array(); + + // We need to get all of the mappings (like phragment.getstate) first + // so that we can detect deletions and creations of files. + $fragment = id(new PhragmentFragmentQuery()) + ->setViewer($request->getUser()) + ->withPaths(array($path)) + ->executeOne(); + if ($fragment === null) { + throw new ConduitException('ERR_BAD_FRAGMENT'); + } + + $mappings = $fragment->getFragmentMappings( + $request->getUser(), + $fragment->getPath()); + + $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); + $files = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + // Scan all of the files that the caller currently has and iterate + // over that. + foreach ($state as $path => $hash) { + // If $mappings[$path] exists, then the user has the file and it's + // also a fragment. + if (array_key_exists($path, $mappings)) { + $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); + if ($file_phid !== null) { + // If the file PHID is present, then we need to check the + // hashes to see if they are the same. + $hash_caller = strtolower($state[$path]); + $hash_current = $files[$file_phid]->getContentHash(); + if ($hash_caller === $hash_current) { + // The user's version is identical to our version, so + // there is no update needed. + } else { + // The hash differs, and the user needs to update. + $patches[] = array( + 'path' => $path, + 'fileOld' => null, + 'fileNew' => $files[$file_phid], + 'hashOld' => $hash_caller, + 'hashNew' => $hash_current, + 'patchURI' => null); + } + } else { + // We have a record of this as a file, but there is no file + // attached to the latest version, so we consider this to be + // a deletion. + $patches[] = array( + 'path' => $path, + 'fileOld' => null, + 'fileNew' => null, + 'hashOld' => $hash_caller, + 'hashNew' => PhragmentPatchUtil::EMPTY_HASH, + 'patchURI' => null); + } + } else { + // If $mappings[$path] does not exist, then the user has a file, + // and we have absolutely no record of it what-so-ever (we haven't + // even recorded a deletion). Assuming most applications will store + // some form of data near their own files, this is probably a data + // file relevant for the application that is not versioned, so we + // don't tell the client to do anything with it. + } + } + + // Check the remaining files that we know about but the caller has + // not reported. + foreach ($mappings as $path => $child) { + if (array_key_exists($path, $state)) { + // We have already evaluated this above. + } else { + $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); + if ($file_phid !== null) { + // If the file PHID is present, then this is a new file that + // we know about, but the caller does not. We need to tell + // the caller to create the file. + $hash_current = $files[$file_phid]->getContentHash(); + $patches[] = array( + 'path' => $path, + 'fileOld' => null, + 'fileNew' => $files[$file_phid], + 'hashOld' => PhragmentPatchUtil::EMPTY_HASH, + 'hashNew' => $hash_current, + 'patchURI' => null); + } else { + // We have a record of deleting this file, and the caller hasn't + // reported it, so they've probably deleted it in a previous + // update. + } + } + } + + // Before we can calculate patches, we need to resolve the old versions + // of files so we can draw diffs on them. + $hashes = array(); + foreach ($patches as $patch) { + if ($patch["hashOld"] !== PhragmentPatchUtil::EMPTY_HASH) { + $hashes[] = $patch["hashOld"]; + } + } + $old_files = array(); + if (count($hashes) !== 0) { + $old_files = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withContentHashes($hashes) + ->execute(); + } + $old_files = mpull($old_files, null, 'getContentHash'); + foreach ($patches as $key => $patch) { + if ($patch["hashOld"] !== PhragmentPatchUtil::EMPTY_HASH) { + if (array_key_exists($patch['hashOld'], $old_files)) { + $patches[$key]['fileOld'] = $old_files[$patch['hashOld']]; + } else { + // We either can't see or can't read the old file. + $patches[$key]['hashOld'] = PhragmentPatchUtil::EMPTY_HASH; + $patches[$key]['fileOld'] = null; + } + } + } + + // Now run through all of the patch entries, calculate the patches + // and return the results. + foreach ($patches as $key => $patch) { + $data = PhragmentPatchUtil::calculatePatch( + $patches[$key]['fileOld'], + $patches[$key]['fileNew']); + unset($patches[$key]['fileOld']); + unset($patches[$key]['fileNew']); + + $file = PhabricatorFile::buildFromFileDataOrHash( + $data, + array( + 'name' => 'patch.dmp', + 'ttl' => time() + 60 * 60 * 24, + )); + $patches[$key]['patchURI'] = $file->getDownloadURI(); + } + + return $patches; + } + +} diff --git a/src/applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php b/src/applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php new file mode 100644 index 0000000000..d08c6c42ec --- /dev/null +++ b/src/applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php @@ -0,0 +1,83 @@ + 'required list', + ); + } + + public function defineReturnType() { + return 'nonempty dict'; + } + + public function defineErrorTypes() { + return array( + 'ERR_BAD_FRAGMENT' => 'No such fragment exists', + ); + } + + protected function execute(ConduitAPIRequest $request) { + $paths = $request->getValue('paths'); + + $fragments = id(new PhragmentFragmentQuery()) + ->setViewer($request->getUser()) + ->withPaths($paths) + ->execute(); + $fragments = mpull($fragments, null, 'getPath'); + foreach ($paths as $path) { + if (!array_key_exists($path, $fragments)) { + throw new ConduitException('ERR_BAD_FRAGMENT'); + } + } + + $results = array(); + foreach ($fragments as $path => $fragment) { + $mappings = $fragment->getFragmentMappings( + $request->getUser(), + $fragment->getPath()); + + $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); + $files = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $result = array(); + foreach ($mappings as $cpath => $child) { + $file_phid = $child->getLatestVersion()->getFilePHID(); + if (!isset($files[$file_phid])) { + // Skip any files we don't have permission to access. + continue; + } + + $file = $files[$file_phid]; + $cpath = substr($child->getPath(), strlen($fragment->getPath()) + 1); + $result[] = array( + 'phid' => $child->getPHID(), + 'phidVersion' => $child->getLatestVersionPHID(), + 'path' => $cpath, + 'hash' => $file->getContentHash(), + 'version' => $child->getLatestVersion()->getSequence(), + 'uri' => $file->getViewURI()); + } + $results[$path] = $result; + } + return $results; + } + +} diff --git a/src/applications/phragment/controller/PhragmentBrowseController.php b/src/applications/phragment/controller/PhragmentBrowseController.php new file mode 100644 index 0000000000..8031dd9095 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentBrowseController.php @@ -0,0 +1,97 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $current = nonempty(last($parents), null); + + $path = ''; + if ($current !== null) { + $path = $current->getPath(); + } + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + if ($this->hasApplicationCapability( + PhragmentCapabilityCanCreate::CAPABILITY)) { + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Create Fragment')) + ->setHref($this->getApplicationURI('/create/'.$path)) + ->setIcon('create')); + } + + $current_box = $this->createCurrentFragmentView($current, false); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + $fragments = null; + if ($current === null) { + // Find all root fragments. + $fragments = id(new PhragmentFragmentQuery()) + ->setViewer($this->getRequest()->getUser()) + ->needLatestVersion(true) + ->withDepths(array(1)) + ->execute(); + } else { + // Find all child fragments. + $fragments = id(new PhragmentFragmentQuery()) + ->setViewer($this->getRequest()->getUser()) + ->needLatestVersion(true) + ->withLeadingPath($current->getPath().'/') + ->withDepths(array($current->getDepth() + 1)) + ->execute(); + } + + foreach ($fragments as $fragment) { + $item = id(new PHUIObjectItemView()); + $item->setHeader($fragment->getName()); + $item->setHref($fragment->getURI()); + if (!$fragment->isDirectory()) { + $item->addAttribute(pht( + 'Last Updated %s', + phabricator_datetime( + $fragment->getLatestVersion()->getDateCreated(), + $viewer))); + $item->addAttribute(pht( + 'Latest Version %s', + $fragment->getLatestVersion()->getSequence())); + if ($fragment->isDeleted()) { + $item->setDisabled(true); + $item->addAttribute(pht('Deleted')); + } + } else { + $item->addAttribute('Directory'); + } + $list->addItem($item); + } + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $current_box, + $list), + array( + 'title' => pht('Browse Fragments'), + 'device' => true)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentController.php b/src/applications/phragment/controller/PhragmentController.php new file mode 100644 index 0000000000..912b729f71 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentController.php @@ -0,0 +1,233 @@ +setViewer($this->getRequest()->getUser()) + ->needLatestVersion(true) + ->withPaths($combinations) + ->execute(); + foreach ($combinations as $combination) { + $found = false; + foreach ($results as $fragment) { + if ($fragment->getPath() === $combination) { + $fragments[] = $fragment; + $found = true; + break; + } + } + if (!$found) { + return null; + } + } + return $fragments; + } + + protected function buildApplicationCrumbsWithPath(array $fragments) { + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb('/', '/phragment/'); + foreach ($fragments as $parent) { + $crumbs->addTextCrumb( + $parent->getName(), + '/phragment/browse/'.$parent->getPath()); + } + return $crumbs; + } + + protected function createCurrentFragmentView($fragment, $is_history_view) { + if ($fragment === null) { + return null; + } + + $viewer = $this->getRequest()->getUser(); + + $phids = array(); + $phids[] = $fragment->getLatestVersionPHID(); + + $snapshot_phids = array(); + $snapshots = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array($fragment->getPHID())) + ->execute(); + foreach ($snapshots as $snapshot) { + $phids[] = $snapshot->getPHID(); + $snapshot_phids[] = $snapshot->getPHID(); + } + + $this->loadHandles($phids); + + $file = null; + $file_uri = null; + if (!$fragment->isDirectory()) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($fragment->getLatestVersion()->getFilePHID())) + ->executeOne(); + if ($file !== null) { + $file_uri = $file->getDownloadURI(); + } + } + + $header = id(new PHUIHeaderView()) + ->setHeader($fragment->getName()) + ->setPolicyObject($fragment) + ->setUser($viewer); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $fragment, + PhabricatorPolicyCapability::CAN_EDIT); + + $zip_uri = $this->getApplicationURI("zip/".$fragment->getPath()); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($fragment) + ->setObjectURI($fragment->getURI()); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Download Fragment')) + ->setHref($this->isCorrectlyConfigured() ? $file_uri : null) + ->setDisabled($file === null || !$this->isCorrectlyConfigured()) + ->setIcon('download')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Download Contents as ZIP')) + ->setHref($this->isCorrectlyConfigured() ? $zip_uri : null) + ->setDisabled(!$this->isCorrectlyConfigured()) + ->setIcon('zip')); + if (!$fragment->isDirectory()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Update Fragment')) + ->setHref($this->getApplicationURI("update/".$fragment->getPath())) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon('edit')); + } else { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Convert to File')) + ->setHref($this->getApplicationURI("update/".$fragment->getPath())) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon('edit')); + } + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Set Fragment Policies')) + ->setHref($this->getApplicationURI("policy/".$fragment->getPath())) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon('edit')); + if ($is_history_view) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Child Fragments')) + ->setHref($this->getApplicationURI("browse/".$fragment->getPath())) + ->setIcon('browse')); + } else { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View History')) + ->setHref($this->getApplicationURI("history/".$fragment->getPath())) + ->setIcon('history')); + } + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Create Snapshot')) + ->setHref($this->getApplicationURI( + "snapshot/create/".$fragment->getPath())) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon('snapshot')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Promote Snapshot to Here')) + ->setHref($this->getApplicationURI( + "snapshot/promote/latest/".$fragment->getPath())) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setIcon('promote')); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($fragment) + ->setActionList($actions); + + if (!$fragment->isDirectory()) { + if ($fragment->isDeleted()) { + $properties->addProperty( + pht('Type'), + pht('File (Deleted)')); + } else { + $properties->addProperty( + pht('Type'), + pht('File')); + } + $properties->addProperty( + pht('Latest Version'), + $this->renderHandlesForPHIDs(array($fragment->getLatestVersionPHID()))); + } else { + $properties->addProperty( + pht('Type'), + pht('Directory')); + } + + if (count($snapshot_phids) > 0) { + $properties->addProperty( + pht('Snapshots'), + $this->renderHandlesForPHIDs($snapshot_phids)); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + } + + function renderConfigurationWarningIfRequired() { + $alt = PhabricatorEnv::getEnvConfig("security.alternate-file-domain"); + if ($alt === null) { + return id(new AphrontErrorView()) + ->setTitle(pht('security.alternate-file-domain must be configured!')) + ->setSeverity(AphrontErrorView::SEVERITY_ERROR) + ->appendChild(phutil_tag('p', array(), pht( + 'Because Phragment generates files (such as ZIP archives and '. + 'patches) as they are requested, it requires that you configure '. + 'the `security.alterate-file-domain` option. This option on it\'s '. + 'own will also provide additional security when serving files '. + 'across Phabricator.'))); + } + return null; + } + + /** + * We use this to disable the download links if the alternate domain is + * not configured correctly. Although the download links will mostly work + * for logged in users without an alternate domain, the behaviour is + * reasonably non-consistent and will deny public users, even if policies + * are configured otherwise (because the Files app does not support showing + * the info page to viewers who are not logged in). + */ + function isCorrectlyConfigured() { + $alt = PhabricatorEnv::getEnvConfig("security.alternate-file-domain"); + return $alt !== null; + } + +} diff --git a/src/applications/phragment/controller/PhragmentCreateController.php b/src/applications/phragment/controller/PhragmentCreateController.php new file mode 100644 index 0000000000..5640913374 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentCreateController.php @@ -0,0 +1,131 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parent = null; + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + if (count($parents) !== 0) { + $parent = idx($parents, count($parents) - 1, null); + } + + $parent_path = ''; + if ($parent !== null) { + $parent_path = $parent->getPath(); + } + $parent_path = trim($parent_path, '/'); + + $fragment = id(new PhragmentFragment()); + + $error_view = null; + + if ($request->isFormPost()) { + $errors = array(); + + $v_name = $request->getStr('name'); + $v_fileid = $request->getInt('fileID'); + $v_viewpolicy = $request->getStr('viewPolicy'); + $v_editpolicy = $request->getStr('editPolicy'); + + if (strpos($v_name, '/') !== false) { + $errors[] = pht('The fragment name can not contain \'/\'.'); + } + + $file = id(new PhabricatorFile())->load($v_fileid); + if ($file === null) { + $errors[] = pht('The specified file doesn\'t exist.'); + } + + if (!count($errors)) { + $depth = 1; + if ($parent !== null) { + $depth = $parent->getDepth() + 1; + } + + PhragmentFragment::createFromFile( + $viewer, + $file, + trim($parent_path.'/'.$v_name, '/'), + $v_viewpolicy, + $v_editpolicy); + + return id(new AphrontRedirectResponse()) + ->setURI('/phragment/browse/'.trim($parent_path.'/'.$v_name, '/')); + } else { + $error_view = id(new AphrontErrorView()) + ->setErrors($errors) + ->setTitle(pht('Errors while creating fragment')); + } + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($fragment) + ->execute(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Parent Path')) + ->setDisabled(true) + ->setValue('/'.trim($parent_path.'/', '/'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name')) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('File ID')) + ->setName('fileID')) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setName('viewPolicy') + ->setPolicyObject($fragment) + ->setPolicies($policies) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setName('editPolicy') + ->setPolicyObject($fragment) + ->setPolicies($policies) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Create Fragment')) + ->addCancelButton( + $this->getApplicationURI('browse/'.$parent_path))); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addTextCrumb(pht('Create Fragment')); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Create Fragment') + ->setValidationException(null) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $box), + array( + 'title' => pht('Create Fragment'), + 'device' => true)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentHistoryController.php b/src/applications/phragment/controller/PhragmentHistoryController.php new file mode 100644 index 0000000000..63a731c6a1 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentHistoryController.php @@ -0,0 +1,111 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $current = idx($parents, count($parents) - 1, null); + + $path = $current->getPath(); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + if ($this->hasApplicationCapability( + PhragmentCapabilityCanCreate::CAPABILITY)) { + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Create Fragment')) + ->setHref($this->getApplicationURI('/create/'.$path)) + ->setIcon('create')); + } + + $current_box = $this->createCurrentFragmentView($current, true); + + $versions = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($current->getPHID())) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + $file_phids = mpull($versions, 'getFilePHID'); + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $current, + PhabricatorPolicyCapability::CAN_EDIT); + + $first = true; + foreach ($versions as $version) { + $item = id(new PHUIObjectItemView()); + $item->setHeader('Version '.$version->getSequence()); + $item->setHref($version->getURI()); + $item->addAttribute(phabricator_datetime( + $version->getDateCreated(), + $viewer)); + + if ($version->getFilePHID() === null) { + $item->setDisabled(true); + $item->addAttribute('Deletion'); + } + + if (!$first && $can_edit) { + $item->addAction(id(new PHUIListItemView()) + ->setIcon('undo') + ->setRenderNameAsTooltip(true) + ->setWorkflow(true) + ->setName(pht("Revert to Here")) + ->setHref($this->getApplicationURI( + "revert/".$version->getID()."/".$current->getPath()))); + } + + $disabled = !isset($files[$version->getFilePHID()]); + $action = id(new PHUIListItemView()) + ->setIcon('download') + ->setDisabled($disabled || !$this->isCorrectlyConfigured()) + ->setRenderNameAsTooltip(true) + ->setName(pht("Download")); + if (!$disabled && $this->isCorrectlyConfigured()) { + $action->setHref($files[$version->getFilePHID()] + ->getDownloadURI($version->getURI())); + } + $item->addAction($action); + + $list->addItem($item); + + $first = false; + } + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $current_box, + $list), + array( + 'title' => pht('Fragment History'), + 'device' => true)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentPatchController.php b/src/applications/phragment/controller/PhragmentPatchController.php new file mode 100644 index 0000000000..b3f6b6bfa7 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentPatchController.php @@ -0,0 +1,104 @@ +aid = idx($data, "aid", 0); + $this->bid = idx($data, "bid", 0); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + // If "aid" is "x", then it means the user wants to generate + // a patch of an empty file to the version specified by "bid". + + $ids = array($this->aid, $this->bid); + if ($this->aid === "x") { + $ids = array($this->bid); + } + + $versions = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + $version_a = null; + if ($this->aid !== "x") { + $version_a = idx($versions, $this->aid, null); + if ($version_a === null) { + return new Aphront404Response(); + } + } + + $version_b = idx($versions, $this->bid, null); + if ($version_b === null) { + return new Aphront404Response(); + } + + $file_phids = array(); + if ($version_a !== null) { + $file_phids[] = $version_a->getFilePHID(); + } + $file_phids[] = $version_b->getFilePHID(); + + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $file_a = null; + if ($version_a != null) { + $file_a = idx($files, $version_a->getFilePHID(), null); + } + $file_b = idx($files, $version_b->getFilePHID(), null); + + $patch = PhragmentPatchUtil::calculatePatch($file_a, $file_b); + + if ($patch === null) { + // There are no differences between the two files, so we output + // an empty patch. + $patch = ''; + } + + $a_sequence = 'x'; + if ($version_a !== null) { + $a_sequence = $version_a->getSequence(); + } + + $name = + $version_b->getFragment()->getName().'.'. + $a_sequence.'.'. + $version_b->getSequence().'.patch'; + + $return = $version_b->getURI(); + if ($request->getExists('return')) { + $return = $request->getStr('return'); + } + + $result = PhabricatorFile::buildFromFileDataOrHash( + $patch, + array( + 'name' => $name, + 'mime-type' => 'text/plain', + 'ttl' => time() + 60 * 60 * 24, + )); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $result->attachToObject($viewer, $version_b->getFragmentPHID()); + unset($unguarded); + + return id(new AphrontRedirectResponse()) + ->setURI($result->getDownloadURI($return)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentPolicyController.php b/src/applications/phragment/controller/PhragmentPolicyController.php new file mode 100644 index 0000000000..b135e23f5d --- /dev/null +++ b/src/applications/phragment/controller/PhragmentPolicyController.php @@ -0,0 +1,108 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $fragment = idx($parents, count($parents) - 1, null); + + $error_view = null; + + if ($request->isFormPost()) { + $errors = array(); + + $v_view_policy = $request->getStr('viewPolicy'); + $v_edit_policy = $request->getStr('editPolicy'); + $v_replace_children = $request->getBool('replacePoliciesOnChildren'); + + $fragment->setViewPolicy($v_view_policy); + $fragment->setEditPolicy($v_edit_policy); + + $fragment->save(); + + if ($v_replace_children) { + // If you can edit a fragment, you can forcibly set the policies + // on child fragments, regardless of whether you can see them or not. + $children = id(new PhragmentFragmentQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withLeadingPath($fragment->getPath().'/') + ->execute(); + $children_phids = mpull($children, 'getPHID'); + + $fragment->openTransaction(); + foreach ($children as $child) { + $child->setViewPolicy($v_view_policy); + $child->setEditPolicy($v_edit_policy); + $child->save(); + } + $fragment->saveTransaction(); + } + + return id(new AphrontRedirectResponse()) + ->setURI('/phragment/browse/'.$fragment->getPath()); + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($fragment) + ->execute(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($fragment) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($fragment) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'replacePoliciesOnChildren', + 'true', + pht( + 'Replace policies on child fragments with '. + 'the policies above.'))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save Fragment Policies')) + ->addCancelButton( + $this->getApplicationURI('browse/'.$fragment->getPath()))); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addTextCrumb(pht('Edit Fragment Policies')); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Edit Fragment Policies: %s', $fragment->getPath())) + ->setValidationException(null) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $box), + array( + 'title' => pht('Edit Fragment Policies'), + 'device' => true)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentRevertController.php b/src/applications/phragment/controller/PhragmentRevertController.php new file mode 100644 index 0000000000..b9943a6a6e --- /dev/null +++ b/src/applications/phragment/controller/PhragmentRevertController.php @@ -0,0 +1,87 @@ +dblob = $data['dblob']; + $this->id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $fragment = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->withPaths(array($this->dblob)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if ($fragment === null) { + return new Aphront404Response(); + } + + $version = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($fragment->getPHID())) + ->withIDs(array($this->id)) + ->executeOne(); + if ($version === null) { + return new Aphront404Response(); + } + + if ($request->isDialogFormPost()) { + $file_phid = $version->getFilePHID(); + + $file = null; + if ($file_phid !== null) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if ($file === null) { + throw new Exception( + "The file associated with this version was not found."); + } + } + + if ($file === null) { + $fragment->deleteFile($viewer); + } else { + $fragment->updateFromFile($viewer, $file); + } + + return id(new AphrontRedirectResponse()) + ->setURI($this->getApplicationURI('/history/'.$this->dblob)); + } + + return $this->createDialog($fragment, $version); + } + + function createDialog( + PhragmentFragment $fragment, + PhragmentFragmentVersion $version) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $dialog = id(new AphrontDialogView()) + ->setTitle(pht('Really revert this fragment?')) + ->setUser($request->getUser()) + ->addSubmitButton(pht('Revert')) + ->addCancelButton(pht('Cancel')) + ->appendParagraph(pht( + "Reverting this fragment to version %d will create a new version of ". + "the fragment. It will not delete any version history.", + $version->getSequence(), + $version->getSequence())); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php new file mode 100644 index 0000000000..a689b9951b --- /dev/null +++ b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php @@ -0,0 +1,168 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $fragment = nonempty(last($parents), null); + if ($fragment === null) { + return new Aphront404Response(); + } + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $fragment, + PhabricatorPolicyCapability::CAN_EDIT); + + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($fragment->getPath().'/') + ->execute(); + + $errors = array(); + if ($request->isFormPost()) { + + $v_name = $request->getStr('name'); + if (strlen($v_name) === 0) { + $errors[] = pht('You must specify a name.'); + } + if (strpos($v_name, '/') !== false) { + $errors[] = pht('Snapshot names can not contain "/".'); + } + + if (!count($errors)) { + $snapshot = null; + + try { + // Create the snapshot. + $snapshot = id(new PhragmentSnapshot()) + ->setPrimaryFragmentPHID($fragment->getPHID()) + ->setName($v_name) + ->save(); + } catch (AphrontQueryDuplicateKeyException $e) { + $errors[] = pht('A snapshot with this name already exists.'); + } + + if (!count($errors)) { + // Add the primary fragment. + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($fragment->getPHID()) + ->setFragmentVersionPHID($fragment->getLatestVersionPHID()) + ->save(); + + // Add all of the child fragments. + foreach ($children as $child) { + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($child->getPHID()) + ->setFragmentVersionPHID($child->getLatestVersionPHID()) + ->save(); + } + + return id(new AphrontRedirectResponse()) + ->setURI('/phragment/snapshot/view/'.$snapshot->getID()); + } + } + } + + $fragment_sequence = "-"; + if ($fragment->getLatestVersion() !== null) { + $fragment_sequence = $fragment->getLatestVersion()->getSequence(); + } + + $rows = array(); + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('th', array(), 'Fragment'), + phutil_tag('th', array(), 'Version'))); + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('td', array(), $fragment->getPath()), + phutil_tag('td', array(), $fragment_sequence))); + foreach ($children as $child) { + $sequence = "-"; + if ($child->getLatestVersion() !== null) { + $sequence = $child->getLatestVersion()->getSequence(); + } + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('td', array(), $child->getPath()), + phutil_tag('td', array(), $sequence))); + } + + $table = phutil_tag( + 'table', + array('class' => 'remarkup-table'), + $rows); + + $container = phutil_tag( + 'div', + array('class' => 'phabricator-remarkup'), + array( + phutil_tag( + 'p', + array(), + pht( + "The snapshot will contain the following fragments at ". + "the specified versions: ")), + $table)); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Fragment Path')) + ->setDisabled(true) + ->setValue('/'.$fragment->getPath())) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Snapshot Name')) + ->setName('name')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Create Snapshot')) + ->addCancelButton( + $this->getApplicationURI('browse/'.$fragment->getPath()))) + ->appendChild( + id(new PHUIFormDividerControl())) + ->appendInstructions($container); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addTextCrumb(pht('Create Snapshot')); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Create Snapshot of %s', $fragment->getName())) + ->setFormErrors($errors) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $box), + array( + 'title' => pht('Create Fragment'), + 'device' => true)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php b/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php new file mode 100644 index 0000000000..902e51e1d0 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php @@ -0,0 +1,53 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->requireCapabilities(array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT)) + ->withIDs(array($this->id)) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + if ($request->isDialogFormPost()) { + $fragment_uri = $snapshot->getPrimaryFragment()->getURI(); + + $snapshot->delete(); + + return id(new AphrontRedirectResponse()) + ->setURI($fragment_uri); + } + + return $this->createDialog(); + } + + function createDialog() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $dialog = id(new AphrontDialogView()) + ->setTitle(pht('Really delete this snapshot?')) + ->setUser($request->getUser()) + ->addSubmitButton(pht('Delete')) + ->addCancelButton(pht('Cancel')) + ->appendParagraph(pht( + "Deleting this snapshot is a permanent operation. You can not ". + "recover the state of the snapshot.")); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php b/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php new file mode 100644 index 0000000000..9876c69e35 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php @@ -0,0 +1,192 @@ +dblob = idx($data, 'dblob', null); + $this->id = idx($data, 'id', null); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + // When the user is promoting a snapshot to the latest version, the + // identifier is a fragment path. + if ($this->dblob !== null) { + $this->targetFragment = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->requireCapabilities(array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT)) + ->withPaths(array($this->dblob)) + ->executeOne(); + if ($this->targetFragment === null) { + return new Aphront404Response(); + } + + $this->snapshots = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array($this->targetFragment->getPHID())) + ->execute(); + } + + // When the user is promoting a snapshot to another snapshot, the + // identifier is another snapshot ID. + if ($this->id !== null) { + $this->targetSnapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->requireCapabilities(array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT)) + ->withIDs(array($this->id)) + ->executeOne(); + if ($this->targetSnapshot === null) { + return new Aphront404Response(); + } + + $this->snapshots = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array( + $this->targetSnapshot->getPrimaryFragmentPHID())) + ->execute(); + } + + // If there's no identifier, just 404. + if ($this->snapshots === null) { + return new Aphront404Response(); + } + + // Work out what options the user has. + $this->options = mpull( + $this->snapshots, + 'getName', + 'getID'); + if ($this->id !== null) { + unset($this->options[$this->id]); + } + + // If there's no options, show a dialog telling the + // user there are no snapshots to promote. + if (count($this->options) === 0) { + return id(new AphrontDialogResponse())->setDialog( + id(new AphrontDialogView()) + ->setTitle(pht('No snapshots to promote')) + ->appendParagraph(pht( + "There are no snapshots available to promote.")) + ->setUser($request->getUser()) + ->addCancelButton(pht('Cancel'))); + } + + // Handle snapshot promotion. + if ($request->isDialogFormPost()) { + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getStr('snapshot'))) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + $snapshot->openTransaction(); + // Delete all existing child entries. + $children = id(new PhragmentSnapshotChildQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSnapshotPHIDs(array($snapshot->getPHID())) + ->execute(); + foreach ($children as $child) { + $child->delete(); + } + + if ($this->id === null) { + // The user is promoting the snapshot to the latest version. + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($this->targetFragment->getPath().'/') + ->execute(); + + // Add the primary fragment. + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($this->targetFragment->getPHID()) + ->setFragmentVersionPHID( + $this->targetFragment->getLatestVersionPHID()) + ->save(); + + // Add all of the child fragments. + foreach ($children as $child) { + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($child->getPHID()) + ->setFragmentVersionPHID($child->getLatestVersionPHID()) + ->save(); + } + } else { + // The user is promoting the snapshot to another snapshot. We just + // copy the other snapshot's child entries and change the snapshot + // PHID to make it identical. + $children = id(new PhragmentSnapshotChildQuery()) + ->setViewer($viewer) + ->withSnapshotPHIDs(array($this->targetSnapshot->getPHID())) + ->execute(); + foreach ($children as $child) { + id(new PhragmentSnapshotChild()) + ->setSnapshotPHID($snapshot->getPHID()) + ->setFragmentPHID($child->getFragmentPHID()) + ->setFragmentVersionPHID($child->getFragmentVersionPHID()) + ->save(); + } + } + $snapshot->saveTransaction(); + + if ($this->id === null) { + return id(new AphrontRedirectResponse()) + ->setURI($this->targetFragment->getURI()); + } else { + return id(new AphrontRedirectResponse()) + ->setURI($this->targetSnapshot->getURI()); + } + } + + return $this->createDialog(); + } + + function createDialog() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $dialog = id(new AphrontDialogView()) + ->setTitle(pht('Promote which snapshot?')) + ->setUser($request->getUser()) + ->addSubmitButton(pht('Promote')) + ->addCancelButton(pht('Cancel')); + + if ($this->id === null) { + // The user is promoting a snapshot to the latest version. + $dialog->appendParagraph(pht( + "Select the snapshot you want to promote to the latest version:")); + } else { + // The user is promoting a snapshot to another snapshot. + $dialog->appendParagraph(pht( + "Select the snapshot you want to promote to '%s':", + $this->targetSnapshot->getName())); + } + + $dialog->appendChild( + id(new AphrontFormSelectControl()) + ->setUser($viewer) + ->setName('snapshot') + ->setOptions($this->options)); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/phragment/controller/PhragmentSnapshotViewController.php b/src/applications/phragment/controller/PhragmentSnapshotViewController.php new file mode 100644 index 0000000000..47663e6331 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentSnapshotViewController.php @@ -0,0 +1,154 @@ +id = idx($data, "id", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + $box = $this->createSnapshotView($snapshot); + + $fragment = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->withPHIDs(array($snapshot->getPrimaryFragmentPHID())) + ->executeOne(); + if ($fragment === null) { + return new Aphront404Response(); + } + + $parents = $this->loadParentFragments($fragment->getPath()); + if ($parents === null) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addTextCrumb(pht('"%s" Snapshot', $snapshot->getName())); + + $children = id(new PhragmentSnapshotChildQuery()) + ->setViewer($viewer) + ->needFragments(true) + ->needFragmentVersions(true) + ->withSnapshotPHIDs(array($snapshot->getPHID())) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($children as $child) { + $item = id(new PHUIObjectItemView()) + ->setHeader($child->getFragment()->getPath()); + + if ($child->getFragmentVersion() !== null) { + $item + ->setHref($child->getFragmentVersion()->getURI()) + ->addAttribute(pht( + 'Version %s', + $child->getFragmentVersion()->getSequence())); + } else { + $item + ->setHref($child->getFragment()->getURI()) + ->addAttribute(pht('Directory')); + } + + $list->addItem($item); + } + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $box, + $list), + array( + 'title' => pht('View Snapshot'), + 'device' => true)); + } + + protected function createSnapshotView($snapshot) { + if ($snapshot === null) { + return null; + } + + $viewer = $this->getRequest()->getUser(); + + $phids = array(); + $phids[] = $snapshot->getPrimaryFragmentPHID(); + + $this->loadHandles($phids); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('"%s" Snapshot', $snapshot->getName())) + ->setPolicyObject($snapshot) + ->setUser($viewer); + + $zip_uri = $this->getApplicationURI( + "zip@".$snapshot->getName(). + "/".$snapshot->getPrimaryFragment()->getPath()); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $snapshot, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($snapshot) + ->setObjectURI($snapshot->getURI()); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Download Snapshot as ZIP')) + ->setHref($this->isCorrectlyConfigured() ? $zip_uri : null) + ->setDisabled(!$this->isCorrectlyConfigured()) + ->setIcon('zip')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Delete Snapshot')) + ->setHref($this->getApplicationURI( + "snapshot/delete/".$snapshot->getID()."/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setIcon('delete')); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Promote Another Snapshot to Here')) + ->setHref($this->getApplicationURI( + "snapshot/promote/".$snapshot->getID()."/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setIcon('promote')); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($snapshot) + ->setActionList($actions); + + $properties->addProperty( + pht('Name'), + $snapshot->getName()); + $properties->addProperty( + pht('Fragment'), + $this->renderHandlesForPHIDs(array($snapshot->getPrimaryFragmentPHID()))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + } +} diff --git a/src/applications/phragment/controller/PhragmentUpdateController.php b/src/applications/phragment/controller/PhragmentUpdateController.php new file mode 100644 index 0000000000..f14d16e2fa --- /dev/null +++ b/src/applications/phragment/controller/PhragmentUpdateController.php @@ -0,0 +1,82 @@ +dblob = idx($data, "dblob", ""); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $fragment = idx($parents, count($parents) - 1, null); + + $error_view = null; + + if ($request->isFormPost()) { + $errors = array(); + + $v_fileid = $request->getInt('fileID'); + + $file = id(new PhabricatorFile())->load($v_fileid); + if ($file === null) { + $errors[] = pht('The specified file doesn\'t exist.'); + } + + if (!count($errors)) { + // If the file is a ZIP archive (has application/zip mimetype) + // then we extract the zip and apply versions for each of the + // individual fragments, creating and deleting files as needed. + if ($file->getMimeType() === "application/zip") { + $fragment->updateFromZIP($viewer, $file); + } else { + $fragment->updateFromFile($viewer, $file); + } + + return id(new AphrontRedirectResponse()) + ->setURI('/phragment/browse/'.$fragment->getPath()); + } else { + $error_view = id(new AphrontErrorView()) + ->setErrors($errors) + ->setTitle(pht('Errors while updating fragment')); + } + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('File ID')) + ->setName('fileID')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Update Fragment')) + ->addCancelButton( + $this->getApplicationURI('browse/'.$fragment->getPath()))); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addTextCrumb(pht('Update Fragment')); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Update Fragment: %s', $fragment->getPath())) + ->setValidationException(null) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $box), + array( + 'title' => pht('Update Fragment'), + 'device' => true)); + } + +} diff --git a/src/applications/phragment/controller/PhragmentVersionController.php b/src/applications/phragment/controller/PhragmentVersionController.php new file mode 100644 index 0000000000..803d0afb56 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentVersionController.php @@ -0,0 +1,136 @@ +id = idx($data, "id", 0); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $version = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if ($version === null) { + return new Aphront404Response(); + } + + $parents = $this->loadParentFragments($version->getFragment()->getPath()); + if ($parents === null) { + return new Aphront404Response(); + } + $current = idx($parents, count($parents) - 1, null); + + $crumbs = $this->buildApplicationCrumbsWithPath($parents); + $crumbs->addTextCrumb(pht('View Version %d', $version->getSequence())); + + $phids = array(); + $phids[] = $version->getFilePHID(); + + $this->loadHandles($phids); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($version->getFilePHID())) + ->executeOne(); + if ($file !== null) { + $file_uri = $file->getDownloadURI(); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht( + "%s at version %d", + $version->getFragment()->getName(), + $version->getSequence())) + ->setPolicyObject($version) + ->setUser($viewer); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($version) + ->setObjectURI($version->getURI()); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Download Version')) + ->setDisabled($file === null || !$this->isCorrectlyConfigured()) + ->setHref($this->isCorrectlyConfigured() ? $file_uri : null) + ->setIcon('download')); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($version) + ->setActionList($actions); + $properties->addProperty( + pht('File'), + $this->renderHandlesForPHIDs(array($version->getFilePHID()))); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $this->renderConfigurationWarningIfRequired(), + $box, + $this->renderPreviousVersionList($version)), + array( + 'title' => pht('View Version'), + 'device' => true)); + } + + private function renderPreviousVersionList( + PhragmentFragmentVersion $version) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $previous_versions = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($version->getFragmentPHID())) + ->withSequenceBefore($version->getSequence()) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($previous_versions as $previous_version) { + $item = id(new PHUIObjectItemView()); + $item->setHeader('Version '.$previous_version->getSequence()); + $item->setHref($previous_version->getURI()); + $item->addAttribute(phabricator_datetime( + $previous_version->getDateCreated(), + $viewer)); + $patch_uri = $this->getApplicationURI( + 'patch/'.$previous_version->getID().'/'.$version->getID()); + $item->addAction(id(new PHUIListItemView()) + ->setIcon('patch') + ->setName(pht("Get Patch")) + ->setHref($this->isCorrectlyConfigured() ? $patch_uri : null) + ->setDisabled(!$this->isCorrectlyConfigured())); + $list->addItem($item); + } + + $item = id(new PHUIObjectItemView()); + $item->setHeader('Prior to Version 0'); + $item->addAttribute('Prior to any content (empty file)'); + $item->addAction(id(new PHUIListItemView()) + ->setIcon('patch') + ->setName(pht("Get Patch")) + ->setHref($this->getApplicationURI( + 'patch/x/'.$version->getID()))); + $list->addItem($item); + + return $list; + } + +} diff --git a/src/applications/phragment/controller/PhragmentZIPController.php b/src/applications/phragment/controller/PhragmentZIPController.php new file mode 100644 index 0000000000..59fc37c831 --- /dev/null +++ b/src/applications/phragment/controller/PhragmentZIPController.php @@ -0,0 +1,152 @@ +dblob = idx($data, "dblob", ""); + $this->snapshot = idx($data, "snapshot", null); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $parents = $this->loadParentFragments($this->dblob); + if ($parents === null) { + return new Aphront404Response(); + } + $fragment = idx($parents, count($parents) - 1, null); + + if ($this->snapshot !== null) { + $snapshot = id(new PhragmentSnapshotQuery()) + ->setViewer($viewer) + ->withPrimaryFragmentPHIDs(array($fragment->getPHID())) + ->withNames(array($this->snapshot)) + ->executeOne(); + if ($snapshot === null) { + return new Aphront404Response(); + } + + $cache = id(new PhragmentSnapshotChildQuery()) + ->setViewer($viewer) + ->needFragmentVersions(true) + ->withSnapshotPHIDs(array($snapshot->getPHID())) + ->execute(); + $this->snapshotCache = mpull( + $cache, + 'getFragmentVersion', + 'getFragmentPHID'); + } + + $temp = new TempFile(); + + $zip = null; + try { + $zip = new ZipArchive(); + } catch (Exception $e) { + $dialog = new AphrontDialogView(); + $dialog->setUser($viewer); + + $inst = pht( + 'This system does not have the ZIP PHP extension installed. This '. + 'is required to download ZIPs from Phragment.'); + + $dialog->setTitle(pht('ZIP Extension Not Installed')); + $dialog->appendParagraph($inst); + + $dialog->addCancelButton('/phragment/browse/'.$this->dblob); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + if (!$zip->open((string)$temp, ZipArchive::CREATE)) { + throw new Exception("Unable to create ZIP archive!"); + } + + $mappings = $this->getFragmentMappings($fragment, $fragment->getPath()); + + $phids = array(); + foreach ($mappings as $path => $file_phid) { + $phids[] = $file_phid; + } + + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + foreach ($mappings as $path => $file_phid) { + if (!isset($files[$file_phid])) { + unset($mappings[$path]); + } + $mappings[$path] = $files[$file_phid]; + } + + foreach ($mappings as $path => $file) { + if ($file !== null) { + $zip->addFromString($path, $file->loadFileData()); + } + } + $zip->close(); + + $zip_name = $fragment->getName(); + if (substr($zip_name, -4) !== '.zip') { + $zip_name .= '.zip'; + } + + $data = Filesystem::readFile((string)$temp); + $file = PhabricatorFile::buildFromFileDataOrHash( + $data, + array( + 'name' => $zip_name, + 'ttl' => time() + 60 * 60 * 24, + )); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file->attachToObject($viewer, $fragment->getPHID()); + unset($unguarded); + + $return = $fragment->getURI(); + if ($request->getExists('return')) { + $return = $request->getStr('return'); + } + + return id(new AphrontRedirectResponse()) + ->setURI($file->getDownloadURI($return)); + } + + /** + * Returns a list of mappings like array('some/path.txt' => 'file PHID'); + */ + private function getFragmentMappings(PhragmentFragment $current, $base_path) { + $mappings = $current->getFragmentMappings( + $this->getRequest()->getUser(), + $base_path); + + $result = array(); + foreach ($mappings as $path => $fragment) { + $version = $this->getVersion($fragment); + if ($version !== null) { + $result[$path] = $version->getFilePHID(); + } + } + return $result; + } + + private function getVersion($fragment) { + if ($this->snapshot === null) { + return $fragment->getLatestVersion(); + } else { + return idx($this->snapshotCache, $fragment->getPHID(), null); + } + } + +} diff --git a/src/applications/phragment/phid/PhragmentPHIDTypeFragment.php b/src/applications/phragment/phid/PhragmentPHIDTypeFragment.php new file mode 100644 index 0000000000..df7fdfec1e --- /dev/null +++ b/src/applications/phragment/phid/PhragmentPHIDTypeFragment.php @@ -0,0 +1,45 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + $viewer = $query->getViewer(); + foreach ($handles as $phid => $handle) { + $fragment = $objects[$phid]; + + $handle->setName(pht( + "Fragment %s: %s", + $fragment->getID(), + $fragment->getName())); + $handle->setURI($fragment->getURI()); + } + } + +} diff --git a/src/applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php b/src/applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php new file mode 100644 index 0000000000..640c4aaf61 --- /dev/null +++ b/src/applications/phragment/phid/PhragmentPHIDTypeFragmentVersion.php @@ -0,0 +1,45 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + $viewer = $query->getViewer(); + foreach ($handles as $phid => $handle) { + $version = $objects[$phid]; + + $handle->setName(pht( + "Fragment Version %d: %s", + $version->getSequence(), + $version->getFragment()->getName())); + $handle->setURI($version->getURI()); + } + } + +} diff --git a/src/applications/phragment/phid/PhragmentPHIDTypeSnapshot.php b/src/applications/phragment/phid/PhragmentPHIDTypeSnapshot.php new file mode 100644 index 0000000000..29e4c70884 --- /dev/null +++ b/src/applications/phragment/phid/PhragmentPHIDTypeSnapshot.php @@ -0,0 +1,44 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + $viewer = $query->getViewer(); + foreach ($handles as $phid => $handle) { + $snapshot = $objects[$phid]; + + $handle->setName(pht( + 'Snapshot: %s', + $snapshot->getName())); + $handle->setURI($snapshot->getURI()); + } + } + +} diff --git a/src/applications/phragment/query/PhragmentFragmentQuery.php b/src/applications/phragment/query/PhragmentFragmentQuery.php new file mode 100644 index 0000000000..5301b2f5df --- /dev/null +++ b/src/applications/phragment/query/PhragmentFragmentQuery.php @@ -0,0 +1,130 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withPaths(array $paths) { + $this->paths = $paths; + return $this; + } + + public function withLeadingPath($path) { + $this->leadingPath = $path; + return $this; + } + + public function withDepths($depths) { + $this->depths = $depths; + return $this; + } + + public function needLatestVersion($need_latest_version) { + $this->needLatestVersion = $need_latest_version; + return $this; + } + + public function loadPage() { + $table = new PhragmentFragment(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->paths) { + $where[] = qsprintf( + $conn_r, + 'path IN (%Ls)', + $this->paths); + } + + if ($this->leadingPath) { + $where[] = qsprintf( + $conn_r, + 'path LIKE %>', + $this->leadingPath); + } + + if ($this->depths) { + $where[] = qsprintf( + $conn_r, + 'depth IN (%Ld)', + $this->depths); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function didFilterPage(array $page) { + if ($this->needLatestVersion) { + $versions = array(); + + $version_phids = array_filter(mpull($page, 'getLatestVersionPHID')); + if ($version_phids) { + $versions = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($version_phids) + ->setParentQuery($this) + ->execute(); + $versions = mpull($versions, null, 'getPHID'); + } + + foreach ($page as $key => $fragment) { + $version_phid = $fragment->getLatestVersionPHID(); + if (empty($versions[$version_phid])) { + continue; + } + $fragment->attachLatestVersion($versions[$version_phid]); + } + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhragment'; + } +} diff --git a/src/applications/phragment/query/PhragmentFragmentVersionQuery.php b/src/applications/phragment/query/PhragmentFragmentVersionQuery.php new file mode 100644 index 0000000000..264a89cf89 --- /dev/null +++ b/src/applications/phragment/query/PhragmentFragmentVersionQuery.php @@ -0,0 +1,123 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withFragmentPHIDs(array $fragment_phids) { + $this->fragmentPHIDs = $fragment_phids; + return $this; + } + + public function withSequences(array $sequences) { + $this->sequences = $sequences; + return $this; + } + + public function withSequenceBefore($current) { + $this->sequenceBefore = $current; + return $this; + } + + public function loadPage() { + $table = new PhragmentFragmentVersion(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->fragmentPHIDs) { + $where[] = qsprintf( + $conn_r, + 'fragmentPHID IN (%Ls)', + $this->fragmentPHIDs); + } + + if ($this->sequences) { + $where[] = qsprintf( + $conn_r, + 'sequence IN (%Ld)', + $this->sequences); + } + + if ($this->sequenceBefore !== null) { + $where[] = qsprintf( + $conn_r, + 'sequence < %d', + $this->sequenceBefore); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function willFilterPage(array $page) { + $fragments = array(); + + $fragment_phids = array_filter(mpull($page, 'getFragmentPHID')); + if ($fragment_phids) { + $fragments = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_phids) + ->setParentQuery($this) + ->execute(); + $fragments = mpull($fragments, null, 'getPHID'); + } + + foreach ($page as $key => $version) { + $fragment_phid = $version->getFragmentPHID(); + if (empty($fragments[$fragment_phid])) { + unset($page[$key]); + continue; + } + $version->attachFragment($fragments[$fragment_phid]); + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhragment'; + } +} diff --git a/src/applications/phragment/query/PhragmentSnapshotChildQuery.php b/src/applications/phragment/query/PhragmentSnapshotChildQuery.php new file mode 100644 index 0000000000..276510eaae --- /dev/null +++ b/src/applications/phragment/query/PhragmentSnapshotChildQuery.php @@ -0,0 +1,174 @@ +ids = $ids; + return $this; + } + + public function withSnapshotPHIDs(array $snapshot_phids) { + $this->snapshotPHIDs = $snapshot_phids; + return $this; + } + + public function withFragmentPHIDs(array $fragment_phids) { + $this->fragmentPHIDs = $fragment_phids; + return $this; + } + + public function withFragmentVersionPHIDs(array $fragment_version_phids) { + $this->fragmentVersionPHIDs = $fragment_version_phids; + return $this; + } + + public function needFragments($need_fragments) { + $this->needFragments = $need_fragments; + return $this; + } + + public function needFragmentVersions($need_fragment_versions) { + $this->needFragmentVersions = $need_fragment_versions; + return $this; + } + + public function loadPage() { + $table = new PhragmentSnapshotChild(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->snapshotPHIDs) { + $where[] = qsprintf( + $conn_r, + 'snapshotPHID IN (%Ls)', + $this->snapshotPHIDs); + } + + if ($this->fragmentPHIDs) { + $where[] = qsprintf( + $conn_r, + 'fragmentPHID IN (%Ls)', + $this->fragmentPHIDs); + } + + if ($this->fragmentVersionPHIDs) { + $where[] = qsprintf( + $conn_r, + 'fragmentVersionPHID IN (%Ls)', + $this->fragmentVersionPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function willFilterPage(array $page) { + $snapshots = array(); + + $snapshot_phids = array_filter(mpull($page, 'getSnapshotPHID')); + if ($snapshot_phids) { + $snapshots = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($snapshot_phids) + ->setParentQuery($this) + ->execute(); + $snapshots = mpull($snapshots, null, 'getPHID'); + } + + foreach ($page as $key => $child) { + $snapshot_phid = $child->getSnapshotPHID(); + if (empty($snapshots[$snapshot_phid])) { + unset($page[$key]); + continue; + } + $child->attachSnapshot($snapshots[$snapshot_phid]); + } + + return $page; + } + + protected function didFilterPage(array $page) { + if ($this->needFragments) { + $fragments = array(); + + $fragment_phids = array_filter(mpull($page, 'getFragmentPHID')); + if ($fragment_phids) { + $fragments = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_phids) + ->setParentQuery($this) + ->execute(); + $fragments = mpull($fragments, null, 'getPHID'); + } + + foreach ($page as $key => $child) { + $fragment_phid = $child->getFragmentPHID(); + if (empty($fragments[$fragment_phid])) { + unset($page[$key]); + continue; + } + $child->attachFragment($fragments[$fragment_phid]); + } + } + + if ($this->needFragmentVersions) { + $fragment_versions = array(); + + $fragment_version_phids = array_filter(mpull( + $page, + 'getFragmentVersionPHID')); + if ($fragment_version_phids) { + $fragment_versions = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_version_phids) + ->setParentQuery($this) + ->execute(); + $fragment_versions = mpull($fragment_versions, null, 'getPHID'); + } + + foreach ($page as $key => $child) { + $fragment_version_phid = $child->getFragmentVersionPHID(); + if (empty($fragment_versions[$fragment_version_phid])) { + continue; + } + $child->attachFragmentVersion( + $fragment_versions[$fragment_version_phid]); + } + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhragment'; + } +} diff --git a/src/applications/phragment/query/PhragmentSnapshotQuery.php b/src/applications/phragment/query/PhragmentSnapshotQuery.php new file mode 100644 index 0000000000..4a6e47c8ab --- /dev/null +++ b/src/applications/phragment/query/PhragmentSnapshotQuery.php @@ -0,0 +1,110 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withPrimaryFragmentPHIDs(array $primary_fragment_phids) { + $this->primaryFragmentPHIDs = $primary_fragment_phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function loadPage() { + $table = new PhragmentSnapshot(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->primaryFragmentPHIDs) { + $where[] = qsprintf( + $conn_r, + 'primaryFragmentPHID IN (%Ls)', + $this->primaryFragmentPHIDs); + } + + if ($this->names) { + $where[] = qsprintf( + $conn_r, + 'name IN (%Ls)', + $this->names); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function willFilterPage(array $page) { + $fragments = array(); + + $fragment_phids = array_filter(mpull($page, 'getPrimaryFragmentPHID')); + if ($fragment_phids) { + $fragments = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($fragment_phids) + ->setParentQuery($this) + ->execute(); + $fragments = mpull($fragments, null, 'getPHID'); + } + + foreach ($page as $key => $snapshot) { + $fragment_phid = $snapshot->getPrimaryFragmentPHID(); + if (empty($fragments[$fragment_phid])) { + unset($page[$key]); + continue; + } + $snapshot->attachPrimaryFragment($fragments[$fragment_phid]); + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhragment'; + } +} diff --git a/src/applications/phragment/storage/PhragmentDAO.php b/src/applications/phragment/storage/PhragmentDAO.php new file mode 100644 index 0000000000..5b3a9a20d7 --- /dev/null +++ b/src/applications/phragment/storage/PhragmentDAO.php @@ -0,0 +1,9 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhragmentPHIDTypeFragment::TYPECONST); + } + + public function getURI() { + return '/phragment/browse/'.$this->getPath(); + } + + public function getName() { + return basename($this->path); + } + + public function getFile() { + return $this->assertAttached($this->file); + } + + public function attachFile(PhabricatorFile $file) { + return $this->file = $file; + } + + public function isDirectory() { + return $this->latestVersionPHID === null; + } + + public function isDeleted() { + return $this->getLatestVersion()->getFilePHID() === null; + } + + public function getLatestVersion() { + if ($this->latestVersionPHID === null) { + return null; + } + return $this->assertAttached($this->latestVersion); + } + + public function attachLatestVersion(PhragmentFragmentVersion $version) { + return $this->latestVersion = $version; + } + + +/* -( Updating ) --------------------------------------------------------- */ + + + /** + * Create a new fragment from a file. + */ + public static function createFromFile( + PhabricatorUser $viewer, + PhabricatorFile $file = null, + $path, + $view_policy, + $edit_policy) { + + $fragment = id(new PhragmentFragment()); + $fragment->setPath($path); + $fragment->setDepth(count(explode('/', $path))); + $fragment->setLatestVersionPHID(null); + $fragment->setViewPolicy($view_policy); + $fragment->setEditPolicy($edit_policy); + $fragment->save(); + + // Directory fragments have no versions associated with them, so we + // just return the fragment at this point. + if ($file === null) { + return $fragment; + } + + if ($file->getMimeType() === "application/zip") { + $fragment->updateFromZIP($viewer, $file); + } else { + $fragment->updateFromFile($viewer, $file); + } + + return $fragment; + } + + + /** + * Set the specified file as the next version for the fragment. + */ + public function updateFromFile( + PhabricatorUser $viewer, + PhabricatorFile $file) { + + $existing = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($this->getPHID())) + ->execute(); + $sequence = count($existing); + + $this->openTransaction(); + $version = id(new PhragmentFragmentVersion()); + $version->setSequence($sequence); + $version->setFragmentPHID($this->getPHID()); + $version->setFilePHID($file->getPHID()); + $version->save(); + + $this->setLatestVersionPHID($version->getPHID()); + $this->save(); + $this->saveTransaction(); + + $file->attachToObject($viewer, $version->getPHID()); + } + + /** + * Apply the specified ZIP archive onto the fragment, removing + * and creating fragments as needed. + */ + public function updateFromZIP( + PhabricatorUser $viewer, + PhabricatorFile $file) { + + if ($file->getMimeType() !== "application/zip") { + throw new Exception("File must have mimetype 'application/zip'"); + } + + // First apply the ZIP as normal. + $this->updateFromFile($viewer, $file); + + // Ensure we have ZIP support. + $zip = null; + try { + $zip = new ZipArchive(); + } catch (Exception $e) { + // The server doesn't have php5-zip, so we can't do recursive updates. + return; + } + + $temp = new TempFile(); + Filesystem::writeFile($temp, $file->loadFileData()); + if (!$zip->open($temp)) { + throw new Exception("Unable to open ZIP"); + } + + // Get all of the paths and their data from the ZIP. + $mappings = array(); + for ($i = 0; $i < $zip->numFiles; $i++) { + $path = trim($zip->getNameIndex($i), '/'); + $stream = $zip->getStream($path); + $data = null; + // If the stream is false, then it is a directory entry. We leave + // $data set to null for directories so we know not to create a + // version entry for them. + if ($stream !== false) { + $data = stream_get_contents($stream); + fclose($stream); + } + $mappings[$path] = $data; + } + + // We need to detect any directories that are in the ZIP folder that + // aren't explicitly noted in the ZIP. This can happen if the file + // entries in the ZIP look like: + // + // * something/blah.png + // * something/other.png + // * test.png + // + // Where there is no explicit "something/" entry. + foreach ($mappings as $path_key => $data) { + if ($data === null) { + continue; + } + $directory = dirname($path_key); + while ($directory !== ".") { + if (!array_key_exists($directory, $mappings)) { + $mappings[$directory] = null; + } + if (dirname($directory) === $directory) { + // dirname() will not reduce this directory any further; to + // prevent infinite loop we just break out here. + break; + } + $directory = dirname($directory); + } + } + + // Adjust the paths relative to this fragment so we can look existing + // fragments up in the DB. + $base_path = $this->getPath(); + $paths = array(); + foreach ($mappings as $p => $data) { + $paths[] = $base_path.'/'.$p; + } + + // FIXME: What happens when a child exists, but the current user + // can't see it. We're going to create a new child with the exact + // same path and then bad things will happen. + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($this->getPath().'/') + ->execute(); + $children = mpull($children, null, 'getPath'); + + // Iterate over the existing fragments. + foreach ($children as $full_path => $child) { + $path = substr($full_path, strlen($base_path) + 1); + if (array_key_exists($path, $mappings)) { + if ($child->isDirectory() && $mappings[$path] === null) { + // Don't create a version entry for a directory + // (unless it's been converted into a file). + continue; + } + + // The file is being updated. + $file = PhabricatorFile::newFromFileData( + $mappings[$path], + array('name' => basename($path))); + $child->updateFromFile($viewer, $file); + } else { + // The file is being deleted. + $child->deleteFile($viewer); + } + } + + // Iterate over the mappings to find new files. + foreach ($mappings as $path => $data) { + if (!array_key_exists($base_path.'/'.$path, $children)) { + // The file is being created. If the data is null, + // then this is explicitly a directory being created. + $file = null; + if ($mappings[$path] !== null) { + $file = PhabricatorFile::newFromFileData( + $mappings[$path], + array('name' => basename($path))); + } + PhragmentFragment::createFromFile( + $viewer, + $file, + $base_path.'/'.$path, + $this->getViewPolicy(), + $this->getEditPolicy()); + } + } + } + + /** + * Delete the contents of the specified fragment. + */ + public function deleteFile(PhabricatorUser $viewer) { + $existing = id(new PhragmentFragmentVersionQuery()) + ->setViewer($viewer) + ->withFragmentPHIDs(array($this->getPHID())) + ->execute(); + $sequence = count($existing); + + $this->openTransaction(); + $version = id(new PhragmentFragmentVersion()); + $version->setSequence($sequence); + $version->setFragmentPHID($this->getPHID()); + $version->setFilePHID(null); + $version->save(); + + $this->setLatestVersionPHID($version->getPHID()); + $this->save(); + $this->saveTransaction(); + } + + +/* -( Utility ) ---------------------------------------------------------- */ + + + public function getFragmentMappings( + PhabricatorUser $viewer, + $base_path) { + + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($this->getPath().'/') + ->withDepths(array($this->getDepth() + 1)) + ->execute(); + + if (count($children) === 0) { + $path = substr($this->getPath(), strlen($base_path) + 1); + return array($path => $this); + } else { + $mappings = array(); + foreach ($children as $child) { + $child_mappings = $child->getFragmentMappings( + $viewer, + $base_path); + foreach ($child_mappings as $key => $value) { + $mappings[$key] = $value; + } + } + return $mappings; + } + } + + +/* -( Policy Interface )--------------------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/phragment/storage/PhragmentFragmentVersion.php b/src/applications/phragment/storage/PhragmentFragmentVersion.php new file mode 100644 index 0000000000..ff848e3270 --- /dev/null +++ b/src/applications/phragment/storage/PhragmentFragmentVersion.php @@ -0,0 +1,62 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhragmentPHIDTypeFragmentVersion::TYPECONST); + } + + public function getURI() { + return '/phragment/version/'.$this->getID().'/'; + } + + public function getFragment() { + return $this->assertAttached($this->fragment); + } + + public function attachFragment(PhragmentFragment $fragment) { + return $this->fragment = $fragment; + } + + public function getFile() { + return $this->assertAttached($this->file); + } + + public function attachFile(PhabricatorFile $file) { + return $this->file = $file; + } + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW + ); + } + + public function getPolicy($capability) { + return $this->getFragment()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getFragment()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return $this->getFragment()->describeAutomaticCapability($capability); + } + +} diff --git a/src/applications/phragment/storage/PhragmentSnapshot.php b/src/applications/phragment/storage/PhragmentSnapshot.php new file mode 100644 index 0000000000..59d0a04b26 --- /dev/null +++ b/src/applications/phragment/storage/PhragmentSnapshot.php @@ -0,0 +1,67 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhragmentPHIDTypeSnapshot::TYPECONST); + } + + public function getURI() { + return '/phragment/snapshot/view/'.$this->getID().'/'; + } + + public function getPrimaryFragment() { + return $this->assertAttached($this->primaryFragment); + } + + public function attachPrimaryFragment(PhragmentFragment $fragment) { + return $this->primaryFragment = $fragment; + } + + public function delete() { + $children = id(new PhragmentSnapshotChild()) + ->loadAllWhere('snapshotPHID = %s', $this->getPHID()); + $this->openTransaction(); + foreach ($children as $child) { + $child->delete(); + } + $result = parent::delete(); + $this->saveTransaction(); + return $result; + } + + +/* -( Policy Interface )--------------------------------------------------- */ + + + public function getCapabilities() { + return $this->getPrimaryFragment()->getCapabilities(); + } + + public function getPolicy($capability) { + return $this->getPrimaryFragment()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getPrimaryFragment() + ->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return $this->getPrimaryFragment() + ->describeAutomaticCapability($capability); + } +} diff --git a/src/applications/phragment/storage/PhragmentSnapshotChild.php b/src/applications/phragment/storage/PhragmentSnapshotChild.php new file mode 100644 index 0000000000..06d33adfba --- /dev/null +++ b/src/applications/phragment/storage/PhragmentSnapshotChild.php @@ -0,0 +1,64 @@ +assertAttached($this->snapshot); + } + + public function attachSnapshot(PhragmentSnapshot $snapshot) { + return $this->snapshot = $snapshot; + } + + public function getFragment() { + return $this->assertAttached($this->fragment); + } + + public function attachFragment(PhragmentFragment $fragment) { + return $this->fragment = $fragment; + } + + public function getFragmentVersion() { + if ($this->fragmentVersionPHID === null) { + return null; + } + return $this->assertAttached($this->fragmentVersion); + } + + public function attachFragmentVersion(PhragmentFragmentVersion $version) { + return $this->fragmentVersion = $version; + } + + +/* -( Policy Interface )--------------------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW + ); + } + + public function getPolicy($capability) { + return $this->getSnapshot()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getSnapshot() + ->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return $this->getSnapshot() + ->describeAutomaticCapability($capability); + } +} diff --git a/src/applications/phragment/util/PhragmentPatchUtil.php b/src/applications/phragment/util/PhragmentPatchUtil.php new file mode 100644 index 0000000000..c395a14e35 --- /dev/null +++ b/src/applications/phragment/util/PhragmentPatchUtil.php @@ -0,0 +1,53 @@ +getContentHash(); + } + if ($new !== null) { + $new_hash = $new->getContentHash(); + } + + $old_content = ""; + $new_content = ""; + + if ($old_hash === $new_hash) { + return null; + } + + if ($old_hash !== self::EMPTY_HASH) { + $old_content = $old->loadFileData(); + } else { + $old_content = ""; + } + + if ($new_hash !== self::EMPTY_HASH) { + $new_content = $new->loadFileData(); + } else { + $new_content = ""; + } + + $dmp = new diff_match_patch(); + $dmp_patches = $dmp->patch_make($old_content, $new_content); + return $dmp->patch_toText($dmp_patches); + } + +} diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php index 6db052d98f..70e49a229f 100644 --- a/src/applications/phriction/controller/PhrictionDiffController.php +++ b/src/applications/phriction/controller/PhrictionDiffController.php @@ -107,21 +107,16 @@ final class PhrictionDiffController $crumbs->addCrumb($view); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History')) - ->setHref(PhrictionDocument::getSlugURI($slug, 'history'))); - + $crumbs->addTextCrumb( + pht('History'), + PhrictionDocument::getSlugURI($slug, 'history')); $title = pht("Version %s vs %s", $l, $r); $header = id(new PHUIHeaderView()) ->setHeader($title); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $comparison_table = $this->renderComparisonTable( @@ -174,15 +169,13 @@ final class PhrictionDiffController pht('Most Recent Change')); } - $navigation_table = hsprintf( - ' - - - - -
    ', - $link_l, - $link_r); + $navigation_table = phutil_tag( + 'table', + array('class' => 'phriction-history-nav-table'), + phutil_tag('tr', array(), array( + phutil_tag('td', array('class' => 'nav-prev'), $link_l), + phutil_tag('td', array('class' => 'nav-next'), $link_r), + ))); } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index b3b1f1f064..6f150fdd0f 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -40,9 +40,11 @@ final class PhrictionDocumentController $document = new PhrictionDocument(); if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProject())->loadOneWhere( - 'phrictionSlug = %s', - PhrictionDocument::getProjectSlugIdentifier($slug)); + $project = id(new PhabricatorProjectQuery()) + ->setViewer($user) + ->withPhrictionSlugs(array( + PhrictionDocument::getProjectSlugIdentifier($slug))) + ->executeOne(); if (!$project) { return new Aphront404Response(); } @@ -214,9 +216,11 @@ final class PhrictionDocumentController $project_phid = null; if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProject())->loadOneWhere( - 'phrictionSlug = %s', - PhrictionDocument::getProjectSlugIdentifier($slug)); + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPhrictionSlugs(array( + PhrictionDocument::getProjectSlugIdentifier($slug))) + ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index c6203a03a9..e9df079fdb 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -51,9 +51,11 @@ final class PhrictionEditController $content = id(new PhrictionContent())->load($document->getContentID()); } else { if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProject())->loadOneWhere( - 'phrictionSlug = %s', - PhrictionDocument::getProjectSlugIdentifier($slug)); + $project = id(new PhabricatorProjectQuery()) + ->setViewer($user) + ->withPhrictionSlugs(array( + PhrictionDocument::getProjectSlugIdentifier($slug))) + ->executeOne(); if (!$project) { return new Aphront404Response(); } @@ -147,13 +149,6 @@ final class PhrictionEditController } } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - if ($document->getID()) { $panel_header = pht('Edit Phriction Document'); $submit_button = pht('Save Changes'); @@ -228,7 +223,7 @@ final class PhrictionEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Document')) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) @@ -239,17 +234,12 @@ final class PhrictionEditController $crumbs = $this->buildApplicationCrumbs(); if ($document->getID()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($content->getTitle()) - ->setHref(PhrictionDocument::getSlugURI($document->getSlug()))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb( + $content->getTitle(), + PhrictionDocument::getSlugURI($document->getSlug())); + $crumbs->addTextCrumb(pht('Edit')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create'))); + $crumbs->addTextCrumb(pht('Create')); } return $this->buildApplicationPage( diff --git a/src/applications/phriction/controller/PhrictionHistoryController.php b/src/applications/phriction/controller/PhrictionHistoryController.php index 53add915d2..d44d8f8f4b 100644 --- a/src/applications/phriction/controller/PhrictionHistoryController.php +++ b/src/applications/phriction/controller/PhrictionHistoryController.php @@ -138,11 +138,9 @@ final class PhrictionHistoryController foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History')) - ->setHref( - PhrictionDocument::getSlugURI($document->getSlug(), 'history'))); + $crumbs->addTextCrumb( + pht('History'), + PhrictionDocument::getSlugURI($document->getSlug(), 'history')); $header = new PHUIHeaderView(); $header->setHeader(pht('Document History for %s', diff --git a/src/applications/phriction/controller/PhrictionMoveController.php b/src/applications/phriction/controller/PhrictionMoveController.php index b870c48d33..39ebe028d7 100644 --- a/src/applications/phriction/controller/PhrictionMoveController.php +++ b/src/applications/phriction/controller/PhrictionMoveController.php @@ -111,7 +111,6 @@ final class PhrictionMoveController if ($errors) { $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) ->setErrors($errors); } diff --git a/src/applications/phriction/controller/PhrictionNewController.php b/src/applications/phriction/controller/PhrictionNewController.php index 0628406696..609907955c 100644 --- a/src/applications/phriction/controller/PhrictionNewController.php +++ b/src/applications/phriction/controller/PhrictionNewController.php @@ -26,14 +26,20 @@ final class PhrictionNewController extends PhrictionController { ->setUser($user) ->appendChild(pht( 'The document %s already exists. Do you want to edit it instead?', - hsprintf('%s', $slug))) + phutil_tag('tt', array(), $slug))) ->addHiddenInput('slug', $slug) ->addHiddenInput('prompt', 'yes') ->addCancelButton('/w/') ->addSubmitButton(pht('Edit Document')); return id(new AphrontDialogResponse())->setDialog($dialog); - } elseif (substr($slug, 0, 9) == 'projects/') { + } else if (PhrictionDocument::isProjectSlug($slug)) { + $project = id(new PhabricatorProjectQuery()) + ->setViewer($user) + ->withPhrictionSlugs(array( + PhrictionDocument::getProjectSlugIdentifier($slug))) + ->executeOne(); + if (!$project) { $dialog = new AphrontDialogView(); $dialog->setSubmitURI('/w/') ->setTitle(pht('Oops!')) @@ -44,12 +50,12 @@ final class PhrictionNewController extends PhrictionController { Create a new project with this name first.')) ->addCancelButton('/w/', 'Okay'); return id(new AphrontDialogResponse())->setDialog($dialog); - - } else { - $uri = '/phriction/edit/?slug='.$slug; - return id(new AphrontRedirectResponse()) - ->setURI($uri); + } } + + $uri = '/phriction/edit/?slug='.$slug; + return id(new AphrontRedirectResponse()) + ->setURI($uri); } if ($slug == '/') { diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php index effeda2203..d0ca728e89 100644 --- a/src/applications/phriction/editor/PhrictionDocumentEditor.php +++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php @@ -213,7 +213,7 @@ final class PhrictionDocumentEditor extends PhabricatorEditor { $document->attachContent($new_content); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($document->getPHID()); + ->queueDocumentForIndexing($document->getPHID()); // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($document->getSlug()); @@ -240,9 +240,11 @@ final class PhrictionDocumentEditor extends PhabricatorEditor { $project_phid = null; $slug = $document->getSlug(); if (PhrictionDocument::isProjectSlug($slug)) { - $project = id(new PhabricatorProject())->loadOneWhere( - 'phrictionSlug = %s', - PhrictionDocument::getProjectSlugIdentifier($slug)); + $project = id(new PhabricatorProjectQuery()) + ->setViewer($this->requireActor()) + ->withPhrictionSlugs(array( + PhrictionDocument::getProjectSlugIdentifier($slug))) + ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } diff --git a/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php b/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php index e0ebcd92a2..b992b0c157 100644 --- a/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php +++ b/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php @@ -46,8 +46,4 @@ final class PhrictionPHIDTypeDocument extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php index ebc252cba8..d40aec3c05 100644 --- a/src/applications/phriction/storage/PhrictionContent.php +++ b/src/applications/phriction/storage/PhrictionContent.php @@ -75,19 +75,15 @@ final class PhrictionContent extends PhrictionDAO $engine); if ($toc) { - $toc = hsprintf( - '
    '. - '
    %s
    '. - '%s'. - '
    ', - pht('Table of Contents'), - $toc); + $toc = phutil_tag_div('phabricator-remarkup-toc', array( + phutil_tag_div( + 'phabricator-remarkup-toc-header', + pht('Table of Contents')), + $toc, + )); } - return hsprintf( - '
    %s%s
    ', - $toc, - $output); + return phutil_tag_div('phabricator-remarkup', array($toc, $output)); } diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php index edd41d9c6b..860ca52f69 100644 --- a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php +++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php @@ -227,6 +227,35 @@ final class PhabricatorPolicyTestCase extends PhabricatorTestCase { } } + public function testMultipleCapabilities() { + $object = new PhabricatorPolicyTestObject(); + $object->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + $object->setPolicies( + array( + PhabricatorPolicyCapability::CAN_VIEW + => PhabricatorPolicies::POLICY_USER, + PhabricatorPolicyCapability::CAN_EDIT + => PhabricatorPolicies::POLICY_NOONE, + )); + + $filter = new PhabricatorPolicyFilter(); + $filter->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + $filter->setViewer($this->buildUser('user')); + + $result = $filter->apply(array($object)); + + $this->assertEqual(array(), $result); + } + + /** * Test an object for visibility across multiple user specifications. */ diff --git a/src/applications/policy/constants/PhabricatorPolicyType.php b/src/applications/policy/constants/PhabricatorPolicyType.php index 4f107db876..51e8cb4c4d 100644 --- a/src/applications/policy/constants/PhabricatorPolicyType.php +++ b/src/applications/policy/constants/PhabricatorPolicyType.php @@ -3,6 +3,7 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { const TYPE_GLOBAL = 'global'; + const TYPE_USER = 'user'; const TYPE_CUSTOM = 'custom'; const TYPE_PROJECT = 'project'; const TYPE_MASKED = 'masked'; @@ -10,8 +11,9 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { public static function getPolicyTypeOrder($type) { static $map = array( self::TYPE_GLOBAL => 0, - self::TYPE_CUSTOM => 1, - self::TYPE_PROJECT => 2, + self::TYPE_USER => 1, + self::TYPE_CUSTOM => 2, + self::TYPE_PROJECT => 3, self::TYPE_MASKED => 9, ); return idx($map, $type, 9); @@ -21,6 +23,8 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { switch ($type) { case self::TYPE_GLOBAL: return pht('Basic Policies'); + case self::TYPE_USER: + return pht('User Policies'); case self::TYPE_CUSTOM: return pht('Advanced'); case self::TYPE_PROJECT: diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index cdaf8af816..f3cac68765 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -156,10 +156,10 @@ final class PhabricatorPolicyFilter { // If we're missing any capability, move on to the next object. continue 2; } - - // If we make it here, we have all of the required capabilities. - $filtered[$key] = $object; } + + // If we make it here, we have all of the required capabilities. + $filtered[$key] = $object; } return $filtered; @@ -259,6 +259,15 @@ final class PhabricatorPolicyFilter { return; } + if ($this->viewer->isOmnipotent()) { + // Never raise policy exceptions for the omnipotent viewer. Although we + // will never normally issue a policy rejection for the omnipotent + // viewer, we can end up here when queries blanket reject objects that + // have failed to load, without distinguishing between nonexistent and + // nonvisible objects. + return; + } + $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); $rejection = null; if ($capobj) { diff --git a/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php index b710eed768..be832aa128 100644 --- a/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php +++ b/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php @@ -20,7 +20,7 @@ final class PhabricatorPolicyManagementShowWorkflow public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $obj_names = $args->getArg('objects'); if (!$obj_names) { diff --git a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php index f3906eee13..7d0768801f 100644 --- a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php +++ b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php @@ -22,7 +22,7 @@ final class PhabricatorPolicyManagementUnlockWorkflow public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $obj_names = $args->getArg('objects'); if (!$obj_names) { diff --git a/src/applications/policy/management/PhabricatorPolicyManagementWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementWorkflow.php index 85d1fe7b3e..4ecac63843 100644 --- a/src/applications/policy/management/PhabricatorPolicyManagementWorkflow.php +++ b/src/applications/policy/management/PhabricatorPolicyManagementWorkflow.php @@ -1,10 +1,6 @@ getPHID(); } + + // Include the "current viewer" policy. This improves consistency, but + // is also useful for creating private instances of normally-shared object + // types, like repositories. + $phids[] = $viewer->getPHID(); } $capabilities = $this->object->getCapabilities(); diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleLegalpadSignature.php b/src/applications/policy/rule/PhabricatorPolicyRuleLegalpadSignature.php new file mode 100644 index 0000000000..8941d53b28 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleLegalpadSignature.php @@ -0,0 +1,69 @@ +setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($values) + ->withSignerPHIDs(array($viewer->getPHID())) + ->execute(); + $this->signatures = mpull($documents, 'getPHID', 'getPHID'); + } + + public function applyRule(PhabricatorUser $viewer, $value) { + foreach ($value as $document_phid) { + if (!isset($this->signatures[$document_phid])) { + return false; + } + } + return true; + } + + public function getValueControlType() { + return self::CONTROL_TYPE_TOKENIZER; + } + + public function getValueControlTemplate() { + return array( + 'markup' => new AphrontTokenizerTemplateView(), + 'uri' => '/typeahead/common/legalpaddocuments/', + 'placeholder' => pht('Type a document title...'), + ); + } + + public function getRuleOrder() { + return 900; + } + + public function getValueForStorage($value) { + PhutilTypeSpec::newFromString('list')->check($value); + return array_values($value); + } + + public function getValueForDisplay(PhabricatorUser $viewer, $value) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($value) + ->execute(); + + return mpull($handles, 'getFullName', 'getPHID'); + } + + public function ruleHasEffect($value) { + return (bool)$value; + } + +} diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index d124a10e5e..f52ab4ecbc 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -66,6 +66,10 @@ final class PhabricatorPolicy $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); $policy->setName($handle->getName()); break; + case PhabricatorPeoplePHIDTypeUser::TYPECONST: + $policy->setType(PhabricatorPolicyType::TYPE_USER); + $policy->setName($handle->getFullName()); + break; case PhabricatorPolicyPHIDTypePolicy::TYPECONST: // TODO: This creates a weird handle-based version of a rule policy. // It behaves correctly, but can't be applied since it doesn't have @@ -138,17 +142,15 @@ final class PhabricatorPolicy PhabricatorPolicies::POLICY_NOONE => 'policy-noone', ); return idx($map, $this->getPHID(), 'policy-unknown'); - break; + case PhabricatorPolicyType::TYPE_USER: + return 'policy-user'; case PhabricatorPolicyType::TYPE_PROJECT: return 'policy-project'; - break; case PhabricatorPolicyType::TYPE_CUSTOM: case PhabricatorPolicyType::TYPE_MASKED: return 'policy-custom'; - break; default: return 'policy-unknown'; - break; } } diff --git a/src/applications/ponder/constants/PonderQuestionStatus.php b/src/applications/ponder/constants/PonderQuestionStatus.php index bdba816145..4480b8f48e 100644 --- a/src/applications/ponder/constants/PonderQuestionStatus.php +++ b/src/applications/ponder/constants/PonderQuestionStatus.php @@ -25,7 +25,7 @@ final class PonderQuestionStatus extends PonderConstants { public static function getQuestionStatusTagColor($status) { $map = array( - self::STATUS_CLOSED => PhabricatorTagView::COLOR_BLACK, + self::STATUS_CLOSED => PHUITagView::COLOR_BLACK, ); return idx($map, $status); diff --git a/src/applications/ponder/controller/PonderAnswerEditController.php b/src/applications/ponder/controller/PonderAnswerEditController.php index 75db3a1c23..ac045149a3 100644 --- a/src/applications/ponder/controller/PonderAnswerEditController.php +++ b/src/applications/ponder/controller/PonderAnswerEditController.php @@ -61,10 +61,6 @@ final class PonderAnswerEditController extends PonderController { } } - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } - $answer_content_id = celerity_generate_unique_node_id(); $form = id(new AphrontFormView()) @@ -86,17 +82,12 @@ final class PonderAnswerEditController extends PonderController { ->addCancelButton($answer_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$qid}") - ->setHref($answer_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Answer'))); + $crumbs->addTextCrumb("Q{$qid}", $answer_uri); + $crumbs->addTextCrumb(pht('Edit Answer')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Answer')) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) diff --git a/src/applications/ponder/controller/PonderAnswerHistoryController.php b/src/applications/ponder/controller/PonderAnswerHistoryController.php index 324809ec27..b15e228889 100644 --- a/src/applications/ponder/controller/PonderAnswerHistoryController.php +++ b/src/applications/ponder/controller/PonderAnswerHistoryController.php @@ -46,17 +46,9 @@ final class PonderAnswerHistoryController extends PonderController { $aid = $answer->getID(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$qid}") - ->setHref("/Q{$qid}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("A{$aid}") - ->setHref("/Q{$qid}#{$aid}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb("Q{$qid}", "/Q{$qid}"); + $crumbs->addTextCrumb("A{$aid}", "/Q{$qid}#{$aid}"); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php index bcfc722ee0..42cf045718 100644 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -76,13 +76,6 @@ final class PonderQuestionEditController extends PonderController { } } - $error_view = null; - if ($errors) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Form Errors')) - ->setErrors($errors); - } - $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( @@ -110,24 +103,17 @@ final class PonderQuestionEditController extends PonderController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Ask New Question')) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $id = $question->getID(); if ($id) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$id}") - ->setHref("/Q{$id}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb("Q{$id}", "/Q{$id}"); + $crumbs->addTextCrumb(pht('Edit')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Ask Question'))); + $crumbs->addTextCrumb(pht('Ask Question')); } return $this->buildApplicationPage( diff --git a/src/applications/ponder/controller/PonderQuestionHistoryController.php b/src/applications/ponder/controller/PonderQuestionHistoryController.php index 4c8c382f8e..09658c14ed 100644 --- a/src/applications/ponder/controller/PonderQuestionHistoryController.php +++ b/src/applications/ponder/controller/PonderQuestionHistoryController.php @@ -45,13 +45,8 @@ final class PonderQuestionHistoryController extends PonderController { $qid = $question->getID(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$qid}") - ->setHref("/Q{$qid}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb("Q{$qid}", "/Q{$qid}"); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index f6d60f13fd..28d51492a3 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -56,10 +56,7 @@ final class PonderQuestionViewController extends PonderController { $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('Q'.$this->questionID) - ->setHref('/Q'.$this->questionID)); + $crumbs->addTextCrumb('Q'.$this->questionID, '/Q'.$this->questionID); return $this->buildApplicationPage( array( @@ -72,7 +69,9 @@ final class PonderQuestionViewController extends PonderController { array( 'device' => true, 'title' => 'Q'.$question->getID().' '.$question->getTitle(), - 'pageObjects' => array($question->getPHID()), + 'pageObjects' => array_merge( + array($question->getPHID()), + mpull($question->getAnswers(), 'getPHID')), )); } @@ -210,19 +209,15 @@ final class PonderQuestionViewController extends PonderController { ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) + ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); - $object_box = id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeaderText(pht('Question Comment')) - ->appendChild($add_comment); - return $this->wrapComments( count($xactions), array( $timeline, - $object_box, + $add_comment, )); } @@ -286,15 +281,11 @@ final class PonderQuestionViewController extends PonderController { ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setShowPreview(false) + ->setHeaderText(pht('Answer Comment')) ->setAction($this->getApplicationURI("/answer/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); - $comment_box = id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeaderText(pht('Answer Comment')) - ->appendChild($form); - - $details[] = $comment_box; + $details[] = $form; $out[] = $this->wrapComments( count($xactions), diff --git a/src/applications/ponder/storage/PonderQuestionTransaction.php b/src/applications/ponder/storage/PonderQuestionTransaction.php index 6c1da4be8d..3218a01f8a 100644 --- a/src/applications/ponder/storage/PonderQuestionTransaction.php +++ b/src/applications/ponder/storage/PonderQuestionTransaction.php @@ -258,13 +258,16 @@ final class PonderQuestionTransaction return phutil_escape_html_newlines( phutil_utf8_shorten($question->getContent(), 128)); } + break; case self::TYPE_ANSWERS: $answer = $this->getNewAnswerObject($story); if ($answer) { return phutil_escape_html_newlines( phutil_utf8_shorten($answer->getContent(), 128)); } + break; } + return parent::getBodyForFeed($story); } diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php index ba7590a56c..9a1f22c9b9 100644 --- a/src/applications/project/application/PhabricatorApplicationProject.php +++ b/src/applications/project/application/PhabricatorApplicationProject.php @@ -46,6 +46,9 @@ final class PhabricatorApplicationProject extends PhabricatorApplication { 'PhabricatorProjectProfilePictureController', 'create/' => 'PhabricatorProjectCreateController', 'board/(?P[1-9]\d*)/' => 'PhabricatorProjectBoardController', + 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', + 'board/(?P[1-9]\d*)/edit/(?:(?P\d+)/)?' + => 'PhabricatorProjectBoardEditController', 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', 'history/(?P[1-9]\d*)/' => 'PhabricatorProjectHistoryController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index 937d23cf2b..4473ee70aa 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -30,28 +30,21 @@ final class PhabricatorProjectBoardController ->withProjectPHIDs(array($project->getPHID())) ->execute(); - // TODO: Completely making this part up. - $columns[] = id(new PhabricatorProjectColumn()) - ->setName('Backlog') - ->setPHID(0) - ->setSequence(0); + $columns = mpull($columns, null, 'getSequence'); - $columns[] = id(new PhabricatorProjectColumn()) - ->setName('Assigned') - ->setPHID(1) - ->setSequence(1); + // If there's no default column, create one now. + if (empty($columns[0])) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $column = PhabricatorProjectColumn::initializeNewColumn($viewer) + ->setSequence(0) + ->setProjectPHID($project->getPHID()) + ->save(); + $column->attachProject($project); + $columns[0] = $column; + unset($unguarded); + } - $columns[] = id(new PhabricatorProjectColumn()) - ->setName('In Progress') - ->setPHID(2) - ->setSequence(2); - - $columns[] = id(new PhabricatorProjectColumn()) - ->setName('Completed') - ->setPHID(3) - ->setSequence(3); - - msort($columns, 'getSequence'); + ksort($columns); $tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) @@ -61,34 +54,80 @@ final class PhabricatorProjectBoardController ->execute(); $tasks = mpull($tasks, null, 'getPHID'); - // TODO: This is also made up. - $task_map = array(); - foreach ($tasks as $task) { - $task_map[mt_rand(0, 3)][] = $task->getPHID(); + if ($tasks) { + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(mpull($tasks, 'getPHID')) + ->withEdgeTypes(array($edge_type)) + ->withDestinationPHIDs(mpull($columns, 'getPHID')); + $edge_query->execute(); } + $task_map = array(); + $default_phid = $columns[0]->getPHID(); + foreach ($tasks as $task) { + $task_phid = $task->getPHID(); + $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); + + $column_phid = head($column_phids); + $column_phid = nonempty($column_phid, $default_phid); + + $task_map[$column_phid][] = $task_phid; + } + + $board_id = celerity_generate_unique_node_id(); + $board = id(new PHUIWorkboardView()) ->setUser($viewer) - ->setFluidishLayout(true); + ->setFluidishLayout(true) + ->setID($board_id); + + $this->initBehavior( + 'project-boards', + array( + 'boardID' => $board_id, + 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), + )); foreach ($columns as $column) { $panel = id(new PHUIWorkpanelView()) - ->setHeader($column->getName()); + ->setHeader($column->getDisplayName()) + ->setHeaderColor($column->getHeaderColor()) + ->setEditURI('edit/'.$column->getID().'/'); $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setCards(true) - ->setFlush(true); + ->setFlush(true) + ->setAllowEmptyList(true) + ->addSigil('project-column') + ->setMetadata( + array( + 'columnPHID' => $column->getPHID(), + )); $task_phids = idx($task_map, $column->getPHID(), array()); foreach (array_select_keys($tasks, $task_phids) as $task) { $cards->addItem($this->renderTaskCard($task)); } $panel->setCards($cards); + if (!$task_phids) { + $cards->addClass('project-column-empty'); + } + $board->addPanel($panel); } $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + $project->getName(), + $this->getApplicationURI('view/'.$project->getID().'/')); + $crumbs->addTextCrumb(pht('Board')); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) @@ -96,7 +135,9 @@ final class PhabricatorProjectBoardController id(new PhabricatorActionView()) ->setName(pht('Add Column/Milestone/Sprint')) ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) - ->setIcon('create')); + ->setIcon('create') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); $plist = id(new PHUIPropertyListView()); // TODO: Need this to get actions to render. @@ -118,7 +159,7 @@ final class PhabricatorProjectBoardController $board_box, ), array( - 'title' => pht('Board'), + 'title' => pht('%s Board', $project->getName()), 'device' => true, )); } @@ -141,6 +182,11 @@ final class PhabricatorProjectBoardController ->setHeader($task->getTitle()) ->setGrippable($can_edit) ->setHref('/T'.$task->getID()) + ->addSigil('project-card') + ->setMetadata( + array( + 'objectPHID' => $task->getPHID(), + )) ->addAction( id(new PHUIListItemView()) ->setName(pht('Edit')) diff --git a/src/applications/project/controller/PhabricatorProjectBoardEditController.php b/src/applications/project/controller/PhabricatorProjectBoardEditController.php new file mode 100644 index 0000000000..83f3e4abce --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardEditController.php @@ -0,0 +1,138 @@ +projectID = $data['projectID']; + $this->id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($this->projectID)) + ->executeOne(); + + if (!$project) { + return new Aphront404Response(); + } + + $is_new = ($this->id ? false : true); + + if (!$is_new) { + $column = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + } else { + $column = PhabricatorProjectColumn::initializeNewColumn($viewer); + } + + $errors = array(); + $e_name = true; + $error_view = null; + $view_uri = $this->getApplicationURI('/board/'.$this->projectID.'/'); + + if ($request->isFormPost()) { + $new_name = $request->getStr('name'); + $column->setName($new_name); + + if (!strlen($column->getName())) { + $errors[] = pht('Column name is required.'); + $e_name = pht('Required'); + } else { + $e_name = null; + } + + if ($is_new) { + $column->setProjectPHID($project->getPHID()); + $column->attachProject($project); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array($project->getPHID())) + ->execute(); + + $new_sequence = 1; + if ($columns) { + $values = mpull($columns, 'getSequence'); + $new_sequence = max($values) + 1; + } + $column->setSequence($new_sequence); + } + + if (!$errors) { + $column->save(); + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + } + + $form = new AphrontFormView(); + $form->setUser($request->getUser()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setValue($column->getName()) + ->setLabel(pht('Name')) + ->setName('name') + ->setError($e_name) + ->setCaption( + pht('This will be displayed as the header of the column.'))); + + if ($is_new) { + $title = pht('Create Column'); + $submit = pht('Create Column'); + } else { + $title = pht('Edit %s', $column->getName()); + $submit = pht('Save Column'); + } + + $form->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($submit) + ->addCancelButton($view_uri)); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + $project->getName(), + $this->getApplicationURI('view/'.$project->getID().'/')); + $crumbs->addTextCrumb( + pht('Board'), + $this->getApplicationURI('board/'.$project->getID().'/')); + $crumbs->addTextCrumb($title); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $form_box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } +} diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index e9de06164d..02a6fd1d45 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -21,21 +21,4 @@ abstract class PhabricatorProjectController extends PhabricatorController { return $nav; } - public function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $can_create = $this->hasApplicationCapability( - ProjectCapabilityCreateProjects::CAPABILITY); - - if ($can_create) { - $crumbs->addAction( - id(new PHUIListItemView()) - ->setName(pht('Create Project')) - ->setHref($this->getApplicationURI('create/')) - ->setIcon('create')); - } - - return $crumbs; - } - } diff --git a/src/applications/project/controller/PhabricatorProjectCreateController.php b/src/applications/project/controller/PhabricatorProjectCreateController.php index 76de5b83d6..0a1240f746 100644 --- a/src/applications/project/controller/PhabricatorProjectCreateController.php +++ b/src/applications/project/controller/PhabricatorProjectCreateController.php @@ -67,7 +67,6 @@ final class PhabricatorProjectCreateController $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } @@ -112,14 +111,13 @@ final class PhabricatorProjectCreateController ->addCancelButton('/project/')); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Project')) - ->setHref($this->getApplicationURI().'create/')); + $crumbs->addTextCrumb( + pht('Create Project'), + $this->getApplicationURI().'create/'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Project')) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/project/controller/PhabricatorProjectHistoryController.php b/src/applications/project/controller/PhabricatorProjectHistoryController.php index d42c05afe4..cd92ed338d 100644 --- a/src/applications/project/controller/PhabricatorProjectHistoryController.php +++ b/src/applications/project/controller/PhabricatorProjectHistoryController.php @@ -37,14 +37,11 @@ final class PhabricatorProjectHistoryController ->setObjectPHID($project->getPHID()) ->setTransactions($xactions); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref($this->getApplicationURI("view/{$id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb( + $project->getName(), + $this->getApplicationURI("view/{$id}/")) + ->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( diff --git a/src/applications/project/controller/PhabricatorProjectListController.php b/src/applications/project/controller/PhabricatorProjectListController.php index 81fdab8823..23470f9418 100644 --- a/src/applications/project/controller/PhabricatorProjectListController.php +++ b/src/applications/project/controller/PhabricatorProjectListController.php @@ -51,4 +51,21 @@ final class PhabricatorProjectListController return $list; } + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $can_create = $this->hasApplicationCapability( + ProjectCapabilityCreateProjects::CAPABILITY); + + if ($can_create) { + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Create Project')) + ->setHref($this->getApplicationURI('create/')) + ->setIcon('create')); + } + + return $crumbs; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php index 54ee8a5701..129f53ded3 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -92,7 +92,7 @@ final class PhabricatorProjectMembersEditController id(new AphrontFormTokenizerControl()) ->setName('phids') ->setLabel(pht('Add Members')) - ->setDatasource('/typeahead/common/users/')) + ->setDatasource('/typeahead/common/accounts/')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') @@ -111,15 +111,11 @@ final class PhabricatorProjectMembersEditController ->setHeaderText($title) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref('/project/view/'.$project->getID().'/')); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Members')) - ->setHref($this->getApplicationURI())); + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) + ->addTextCrumb( + $project->getName(), + '/project/view/'.$project->getID().'/') + ->addTextCrumb(pht('Edit Members'), $this->getApplicationURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php new file mode 100644 index 0000000000..7478a7dc79 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -0,0 +1,120 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $column_phid = $request->getStr('columnPHID'); + $object_phid = $request->getStr('objectPHID'); + $after_phid = $request->getStr('afterPHID'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + // NOTE: I'm not requiring EDIT on the object for now, since we require + // EDIT on the project anyway and this relationship is more owned by the + // project than the object. Maybe this is worth revisiting eventually. + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->executeOne(); + + if (!$object) { + return new Aphront404Response(); + } + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array($project->getPHID())) + ->execute(); + + $columns = mpull($columns, null, 'getPHID'); + if (empty($columns[$column_phid])) { + // User is trying to drop this object into a nonexistent column, just kick + // them out. + return new Aphront404Response(); + } + + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; + + $query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($object->getPHID())) + ->withEdgeTypes(array($edge_type)) + ->withDestinationPHIDs(array_keys($columns)); + + $query->execute(); + + $edge_phids = $query->getDestinationPHIDs(); + + $this->rewriteEdges( + $object->getPHID(), + $edge_type, + $column_phid, + $edge_phids); + + // TODO: We also need to deal with priorities, so far this only gets stuff + // in the correct column. + + return id(new AphrontAjaxResponse())->setContent(array()); + } + + private function rewriteEdges($src, $edge_type, $dst, array $edges) { + $viewer = $this->getRequest()->getUser(); + + // NOTE: Normally, we expect only one edge to exist, but this works in a + // general way so it will repair any stray edges. + + $remove = array(); + $edge_missing = true; + foreach ($edges as $phid) { + if ($phid == $dst) { + $edge_missing = false; + } else { + $remove[] = $phid; + } + } + + $add = array(); + if ($edge_missing) { + $add[] = $dst; + } + + if (!$add && !$remove) { + return; + } + + $editor = id(new PhabricatorEdgeEditor()) + ->setActor($viewer) + ->setSuppressEvents(true); + + foreach ($add as $phid) { + $editor->addEdge($src, $edge_type, $phid); + } + foreach ($remove as $phid) { + $editor->removeEdge($src, $edge_type, $phid); + } + + $editor->save(); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index bf4fc6cc42..fb13948115 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -45,17 +45,10 @@ final class PhabricatorProjectProfileController $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $feed = $this->renderStories($stories); - $people = $this->renderPeoplePage($project, $profile); - $content = id(new AphrontMultiColumnView()) - ->addColumn($people) - ->addColumn($feed) - ->setFluidLayout(true); - - $content = hsprintf( - '
    %s%s
    ', - $tasks, - $content); + $content = phutil_tag_div( + 'phabricator-project-layout', + array($tasks, $feed)); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) @@ -74,9 +67,7 @@ final class PhabricatorProjectProfileController $properties = $this->buildPropertyListView($project, $profile, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName())) + $crumbs->addTextCrumb($project->getName()) ->setActionList($actions); $object_box = id(new PHUIObjectBoxView()) @@ -95,34 +86,6 @@ final class PhabricatorProjectProfileController )); } - private function renderPeoplePage( - PhabricatorProject $project, - PhabricatorProjectProfile $profile) { - - $member_phids = $project->getMemberPHIDs(); - $handles = $this->loadViewerHandles($member_phids); - - $affiliated = array(); - foreach ($handles as $phids => $handle) { - $affiliated[] = phutil_tag('li', array(), $handle->renderLink()); - } - - if ($affiliated) { - $affiliated = phutil_tag('ul', array(), $affiliated); - } else { - $affiliated = hsprintf('

    %s

    ', pht( - 'No one is affiliated with this project.')); - } - - return hsprintf( - '
    '. - '

    %s

    '. - '
    %s
    '. - '
    ', - pht('People'), - $affiliated); - } - private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { @@ -148,10 +111,8 @@ final class PhabricatorProjectProfileController $builder->setShowHovercards(true); $view = $builder->buildView(); - return hsprintf( - '
    '. - '%s'. - '
    ', + return phutil_tag_div( + 'profile-feed', $view->render()); } @@ -182,13 +143,9 @@ final class PhabricatorProjectProfileController $task_list->setTasks($tasks); $task_list->setHandles($handles); - $list = id(new PHUIBoxView()) - ->addPadding(PHUI::PADDING_LARGE) - ->appendChild($task_list); - $content = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Open Tasks')) - ->appendChild($list); + ->appendChild($task_list); return $content; } @@ -273,12 +230,25 @@ final class PhabricatorProjectProfileController $request = $this->getRequest(); $viewer = $request->getUser(); + $this->loadHandles($project->getMemberPHIDs()); + $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($project) ->setActionList($actions); - $view->addCustomTextContent( + $view->addProperty( + pht('Created'), + phabricator_datetime($project->getDateCreated(), $viewer)); + + $view->addProperty( + pht('Members'), + $project->getMemberPHIDs() + ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',') + : phutil_tag('em', array(), pht('None'))); + + $view->addSectionHeader(pht('Description')); + $view->addTextContent( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setPreserveLinebreaks(true) diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index 0d86c927af..57c2857047 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -93,13 +93,6 @@ final class PhabricatorProjectProfileEditController } } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } - $header_name = pht('Edit Project'); $title = pht('Edit Project'); $action = '/project/edit/'.$project->getID().'/'; @@ -162,18 +155,14 @@ final class PhabricatorProjectProfileEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref('/project/view/'.$project->getID().'/')); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Project')) - ->setHref($this->getApplicationURI())); + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) + ->addTextCrumb( + $project->getName(), + '/project/view/'.$project->getID().'/') + ->addTextCrumb(pht('Edit Project'), $this->getApplicationURI()); return $this->buildApplicationPage( array( diff --git a/src/applications/project/controller/PhabricatorProjectProfilePictureController.php b/src/applications/project/controller/PhabricatorProjectProfilePictureController.php index 6188f58014..8c2145f9f4 100644 --- a/src/applications/project/controller/PhabricatorProjectProfilePictureController.php +++ b/src/applications/project/controller/PhabricatorProjectProfilePictureController.php @@ -89,13 +89,8 @@ final class PhabricatorProjectProfilePictureController $title = pht('Edit Project Picture'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref($project_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($project->getName(), $project_uri); + $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); @@ -227,11 +222,6 @@ final class PhabricatorProjectProfilePictureController ->setLabel(pht('Quick Create')) ->setValue($compose_form)); - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setFormError($errors) - ->setForm($form); - $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') @@ -247,13 +237,9 @@ final class PhabricatorProjectProfilePictureController ->addCancelButton($project_uri) ->setValue(pht('Upload Picture'))); - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); $upload_box = id(new PHUIObjectBoxView()) diff --git a/src/applications/project/editor/PhabricatorProjectEditor.php b/src/applications/project/editor/PhabricatorProjectEditor.php index f8367e80f0..05f01db9e4 100644 --- a/src/applications/project/editor/PhabricatorProjectEditor.php +++ b/src/applications/project/editor/PhabricatorProjectEditor.php @@ -168,7 +168,7 @@ final class PhabricatorProjectEditor extends PhabricatorEditor { } id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($project->getPHID()); + ->queueDocumentForIndexing($project->getPHID()); return $this; } @@ -250,8 +250,8 @@ final class PhabricatorProjectEditor extends PhabricatorEditor { $old_slug = $project->getFullPhrictionSlug(); $project->setName($xaction->getNewValue()); $project->setPhrictionSlug($xaction->getNewValue()); - - if ($xaction->getOldValue()) { + $changed_slug = $old_slug != $project->getFullPhrictionSlug(); + if ($xaction->getOldValue() && $changed_slug) { $old_document = id(new PhrictionDocument()) ->loadOneWhere( 'slug = %s', diff --git a/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php b/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php index 97a81b1d0c..56165dd3cf 100644 --- a/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php +++ b/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php @@ -41,9 +41,57 @@ final class PhabricatorProjectPHIDTypeProject extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - // TODO: We should be able to load named projects by hashtag, e.g. "#yolo". - return false; + public static function getProjectMonogramPatternFragment() { + // NOTE: This explicitly does not match strings which contain only + // digits, because digit strings like "#123" are used to reference tasks at + // Facebook and are somewhat conventional in general. + return '[^\s.!,:;]*[^\s\d.!,:;]+[^\s.!,:;]*'; } + public function canLoadNamedObject($name) { + $fragment = self::getProjectMonogramPatternFragment(); + return preg_match('/^#'.$fragment.'$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + // If the user types "#YoloSwag", we still want to match "#yoloswag", so + // we normalize, query, and then map back to the original inputs. + + $map = array(); + foreach ($names as $key => $slug) { + $map[$this->normalizeSlug(substr($slug, 1))][] = $slug; + } + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($query->getViewer()) + ->withPhrictionSlugs(array_keys($map)) + ->execute(); + + $result = array(); + foreach ($projects as $project) { + $slugs = array($project->getPhrictionSlug()); + foreach ($slugs as $slug) { + foreach ($map[$slug] as $original) { + $result[$original] = $project; + } + } + } + + return $result; + } + + private function normalizeSlug($slug) { + // NOTE: We're using phutil_utf8_strtolower() (and not PhabricatorSlug's + // normalize() method) because this normalization should be only somewhat + // liberal. We want "#YOLO" to match against "#yolo", but "#\\yo!!lo" + // should not. normalize() strips out most punctuation and leads to + // excessively aggressive matches. + + return phutil_utf8_strtolower($slug).'/'; + } + + } diff --git a/src/applications/project/remarkup/ProjectRemarkupRule.php b/src/applications/project/remarkup/ProjectRemarkupRule.php index 9acd489dd6..637f0ea505 100644 --- a/src/applications/project/remarkup/ProjectRemarkupRule.php +++ b/src/applications/project/remarkup/ProjectRemarkupRule.php @@ -11,50 +11,34 @@ final class ProjectRemarkupRule } protected function getObjectIDPattern() { - // NOTE: This explicitly does not match strings which contain only - // digits, because digit strings like "#123" are used to reference tasks at - // Facebook and are somewhat conventional in general. - return '[^\s.!,:;]*[^\s\d.!,:;]+[^\s.!,:;]*'; + return + PhabricatorProjectPHIDTypeProject::getProjectMonogramPatternFragment(); } protected function loadObjects(array $ids) { $viewer = $this->getEngine()->getConfig('viewer'); - // If the user types "#YoloSwag", we still want to match "#yoloswag", so - // we normalize, query, and then map back to the original inputs. - - $map = array(); - foreach ($ids as $key => $slug) { - $map[$this->normalizeSlug($slug)][] = $slug; + // Put the "#" back on the front of these IDs. + $names = array(); + foreach ($ids as $id) { + $names[] = '#'.$id; } - $projects = id(new PhabricatorProjectQuery()) + // Issue a query by object name. + $query = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withPhrictionSlugs(array_keys($map)) - ->execute(); + ->withNames($names); + $query->execute(); + $projects = $query->getNamedResults(); + + // Slice the "#" off again. $result = array(); - foreach ($projects as $project) { - $slugs = array($project->getPhrictionSlug()); - foreach ($slugs as $slug) { - foreach ($map[$slug] as $original) { - $result[$original] = $project; - } - } + foreach ($projects as $name => $project) { + $result[substr($name, 1)] = $project; } - return $result; } - private function normalizeSlug($slug) { - // NOTE: We're using phutil_utf8_strtolower() (and not PhabricatorSlug's - // normalize() method) because this normalization should be only somewhat - // liberal. We want "#YOLO" to match against "#yolo", but "#\\yo!!lo" - // should not. normalize() strips out most punctuation and leads to - // excessively aggressive matches. - - return phutil_utf8_strtolower($slug).'/'; - } - } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 8d9c32af0d..79474aef0c 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -10,6 +10,11 @@ final class PhabricatorProjectColumn private $project = self::ATTACHABLE; + public static function initializeNewColumn(PhabricatorUser $user) { + return id(new PhabricatorProjectColumn()) + ->setName(''); + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -30,6 +35,24 @@ final class PhabricatorProjectColumn return $this->assertAttached($this->project); } + public function isDefaultColumn() { + return ($this->getSequence() == 0); + } + + public function getDisplayName() { + if ($this->isDefaultColumn()) { + return pht('Backlog'); + } + return $this->getName(); + } + + public function getHeaderColor() { + if ($this->isDefaultColumn()) { + return PhabricatorActionHeaderView::HEADER_DARK_GREY; + } + return PhabricatorActionHeaderView::HEADER_GREY; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/releeph/controller/ReleephProjectController.php b/src/applications/releeph/controller/ReleephProjectController.php index 2a0d774979..64c8678c40 100644 --- a/src/applications/releeph/controller/ReleephProjectController.php +++ b/src/applications/releeph/controller/ReleephProjectController.php @@ -135,10 +135,7 @@ abstract class ReleephProjectController extends ReleephController { $project_id = $project->getID(); $project_uri = $this->getApplicationURI("project/{$project_id}/"); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($project_uri) - ->setName($project->getName())); + $crumbs->addTextCrumb($project->getName(), $project_uri); } catch (Exception $ex) { // TODO: This is derps. } @@ -146,10 +143,7 @@ abstract class ReleephProjectController extends ReleephController { try { $branch = $this->getReleephBranch(); $branch_uri = $branch->getURI(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($branch_uri) - ->setName($branch->getDisplayNameWithDetail())); + $crumbs->addTextCrumb($branch->getDisplayNameWithDetail(), $branch_uri); } catch (Exception $ex) { // TODO: This is also derps. } diff --git a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php index 1309a3d55d..cf21e44461 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php @@ -68,7 +68,6 @@ final class ReleephBranchCreateController extends ReleephProjectController { if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); - $error_view->setTitle(pht('Form Errors')); } $project_id = $releeph_project->getID(); @@ -99,9 +98,7 @@ final class ReleephBranchCreateController extends ReleephProjectController { ->addCancelButton($project_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Branch'))); + $crumbs->addTextCrumb(pht('New Branch')); return $this->buildApplicationPage( array( diff --git a/src/applications/releeph/controller/branch/ReleephBranchEditController.php b/src/applications/releeph/controller/branch/ReleephBranchEditController.php index 1f1c0c6c47..3fcbf0b854 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchEditController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchEditController.php @@ -86,9 +86,7 @@ final class ReleephBranchEditController extends ReleephProjectController { $releeph_branch->getDisplayNameWithDetail()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb(pht('Edit')); return $this->buildApplicationPage( array( diff --git a/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php b/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php index 7e1314a03e..e0475d1f3c 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php @@ -32,9 +32,7 @@ final class ReleephBranchHistoryController extends ReleephProjectController { ->setTransactions($xactions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( diff --git a/src/applications/releeph/controller/branch/ReleephBranchViewController.php b/src/applications/releeph/controller/branch/ReleephBranchViewController.php index e0cd58f648..2a6aa1722e 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchViewController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchViewController.php @@ -103,9 +103,9 @@ final class ReleephBranchViewController extends ReleephProjectController if (!$branch->getIsActive()) { $header->addTag( - id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK) + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLACK) ->setName(pht('Closed'))); } diff --git a/src/applications/releeph/controller/project/ReleephProjectCreateController.php b/src/applications/releeph/controller/project/ReleephProjectCreateController.php index f29f52f2c9..d5c8ef20e9 100644 --- a/src/applications/releeph/controller/project/ReleephProjectCreateController.php +++ b/src/applications/releeph/controller/project/ReleephProjectCreateController.php @@ -52,12 +52,6 @@ final class ReleephProjectCreateController extends ReleephProjectController { } } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setErrors($errors); - } - $arc_project_options = $this->getArcProjectSelectOptions($arc_projects); $project_name_input = id(new AphrontFormTextControl()) @@ -110,13 +104,11 @@ final class ReleephProjectCreateController extends ReleephProjectController { $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Project')) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Project'))); + $crumbs->addTextCrumb(pht('New Project')); return $this->buildApplicationPage( array( diff --git a/src/applications/releeph/controller/project/ReleephProjectEditController.php b/src/applications/releeph/controller/project/ReleephProjectEditController.php index 47a42f46ea..1f10d94b32 100644 --- a/src/applications/releeph/controller/project/ReleephProjectEditController.php +++ b/src/applications/releeph/controller/project/ReleephProjectEditController.php @@ -116,7 +116,6 @@ final class ReleephProjectEditController extends ReleephProjectController { if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); - $error_view->setTitle(pht('Form Errors')); } $projects = mpull( diff --git a/src/applications/releeph/controller/project/ReleephProjectHistoryController.php b/src/applications/releeph/controller/project/ReleephProjectHistoryController.php index e9df27f01c..85125d4cf2 100644 --- a/src/applications/releeph/controller/project/ReleephProjectHistoryController.php +++ b/src/applications/releeph/controller/project/ReleephProjectHistoryController.php @@ -32,9 +32,7 @@ final class ReleephProjectHistoryController extends ReleephProjectController { ->setTransactions($xactions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( diff --git a/src/applications/releeph/controller/project/ReleephProjectViewController.php b/src/applications/releeph/controller/project/ReleephProjectViewController.php index 27cfd6b0d6..bd58a01f27 100644 --- a/src/applications/releeph/controller/project/ReleephProjectViewController.php +++ b/src/applications/releeph/controller/project/ReleephProjectViewController.php @@ -160,9 +160,9 @@ final class ReleephProjectViewController extends ReleephProjectController if (!$project->getIsActive()) { $header->addTag( - id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK) + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLACK) ->setName(pht('Deactivated'))); } diff --git a/src/applications/releeph/controller/request/ReleephRequestEditController.php b/src/applications/releeph/controller/request/ReleephRequestEditController.php index 65532155b8..f39085893f 100644 --- a/src/applications/releeph/controller/request/ReleephRequestEditController.php +++ b/src/applications/releeph/controller/request/ReleephRequestEditController.php @@ -204,7 +204,6 @@ final class ReleephRequestEditController extends ReleephProjectController { if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); - $error_view->setTitle('Form Errors'); } $form = id(new AphrontFormView()) @@ -272,20 +271,13 @@ final class ReleephRequestEditController extends ReleephProjectController { $title = pht('Edit Releeph Request'); $submit_name = pht('Save'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('RQ'.$rq->getID()) - ->setHref('/RQ'.$rq->getID())); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb('RQ'.$rq->getID(), '/RQ'.$rq->getID()); + $crumbs->addTextCrumb(pht('Edit')); } else { $title = pht('Create Releeph Request'); $submit_name = pht('Create'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Request'))); + $crumbs->addTextCrumb(pht('New Request')); } $form->appendChild( diff --git a/src/applications/releeph/controller/request/ReleephRequestViewController.php b/src/applications/releeph/controller/request/ReleephRequestViewController.php index 4f4bccb12b..345e3acb20 100644 --- a/src/applications/releeph/controller/request/ReleephRequestViewController.php +++ b/src/applications/releeph/controller/request/ReleephRequestViewController.php @@ -54,38 +54,33 @@ final class ReleephRequestViewController extends ReleephProjectController { ->setTransactions($xactions) ->setMarkupEngine($engine); - $add_comment_header = id(new PHUIHeaderView()) - ->setHeader('Plea or yield'); + $add_comment_header = pht('Plea or yield'); $draft = PhabricatorDraft::newFromUserAndKey( $user, $releeph_request->getPHID()); - $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) - ->setObjectPHID($releeph_request->getPHID()) - ->setDraft($draft) - ->setAction($this->getApplicationURI( - '/request/comment/'.$releeph_request->getID().'/')) - ->setSubmitButtonName('Comment'); - $title = hsprintf("RQ%d: %s", $releeph_request->getID(), $releeph_request->getSummaryForDisplay()); + $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) + ->setUser($user) + ->setObjectPHID($releeph_request->getPHID()) + ->setDraft($draft) + ->setHeaderText($add_comment_header) + ->setAction($this->getApplicationURI( + '/request/comment/'.$releeph_request->getID().'/')) + ->setSubmitButtonName('Comment'); + $crumbs = $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($releeph_project->getName()) - ->setHref($releeph_project->getURI())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($releeph_branch->getDisplayNameWithDetail()) - ->setHref($releeph_branch->getURI())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('RQ'.$releeph_request->getID()) - ->setHref('/RQ'.$releeph_request->getID())); + ->addTextCrumb($releeph_project->getName(), $releeph_project->getURI()) + ->addTextCrumb( + $releeph_branch->getDisplayNameWithDetail(), + $releeph_branch->getURI()) + ->addTextCrumb( + 'RQ'.$releeph_request->getID(), + '/RQ'.$releeph_request->getID()); return $this->buildStandardPageResponse( array( @@ -93,7 +88,6 @@ final class ReleephRequestViewController extends ReleephProjectController { array( $rq_view, $timeline, - $add_comment_header, $add_comment_form, ) ), diff --git a/src/applications/releeph/field/selector/ReleephDefaultFieldSelector.php b/src/applications/releeph/field/selector/ReleephDefaultFieldSelector.php index 0801203b89..24ec931bd1 100644 --- a/src/applications/releeph/field/selector/ReleephDefaultFieldSelector.php +++ b/src/applications/releeph/field/selector/ReleephDefaultFieldSelector.php @@ -42,6 +42,7 @@ final class ReleephDefaultFieldSelector extends ReleephFieldSelector { new ReleephBranchCommitFieldSpecification(), new ReleephDiffSizeFieldSpecification(), new ReleephDiffChurnFieldSpecification(), + new ReleephDependsOnFieldSpecification(), new ReleephFacebookTagFieldSpecification(), new ReleephFacebookTasksFieldSpecification(), ); @@ -76,6 +77,7 @@ final class ReleephDefaultFieldSelector extends ReleephFieldSelector { 'ReleephOriginalCommitFieldSpecification', 'ReleephDiffSizeFieldSpecification', 'ReleephDiffChurnFieldSpecification', + 'ReleephDependsOnFieldSpecification', 'ReleephFacebookTasksFieldSpecification', )), 'right' => self::selectFields($fields, array( diff --git a/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php new file mode 100644 index 0000000000..8af68bd444 --- /dev/null +++ b/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php @@ -0,0 +1,46 @@ +getDependentRevisionPHIDs(); + if (!$revision_phids) { + return null; + } + + $links = array(); + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->getUser()) + ->withPHIDs($revision_phids) + ->execute(); + foreach ($revision_phids as $revision_phid) { + $links[] = id(clone $handles[$revision_phid]) + // Hack to remove the strike-through rendering of diff links + ->setStatus(null) + ->renderLink(); + } + + return phutil_implode_html(phutil_tag('br'), $links); + } + + private function getDependentRevisionPHIDs() { + $revision = $this + ->getReleephRequest() + ->loadDifferentialRevision(); + if (!$revision) { + return null; + } + + return PhabricatorEdgeQuery::loadDestinationPHIDs( + $revision->getPHID(), + PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV); + } +} diff --git a/src/applications/releeph/phid/ReleephPHIDTypeBranch.php b/src/applications/releeph/phid/ReleephPHIDTypeBranch.php index 3511a67c69..8978d77853 100644 --- a/src/applications/releeph/phid/ReleephPHIDTypeBranch.php +++ b/src/applications/releeph/phid/ReleephPHIDTypeBranch.php @@ -38,8 +38,4 @@ final class ReleephPHIDTypeBranch extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/releeph/phid/ReleephPHIDTypeProject.php b/src/applications/releeph/phid/ReleephPHIDTypeProject.php index e15994bf83..275ea4a277 100644 --- a/src/applications/releeph/phid/ReleephPHIDTypeProject.php +++ b/src/applications/releeph/phid/ReleephPHIDTypeProject.php @@ -37,8 +37,4 @@ final class ReleephPHIDTypeProject extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/releeph/phid/ReleephPHIDTypeRequest.php b/src/applications/releeph/phid/ReleephPHIDTypeRequest.php index 506585fce0..36466d86ff 100644 --- a/src/applications/releeph/phid/ReleephPHIDTypeRequest.php +++ b/src/applications/releeph/phid/ReleephPHIDTypeRequest.php @@ -41,8 +41,4 @@ final class ReleephPHIDTypeRequest extends PhabricatorPHIDType { } } - public function canLoadNamedObject($name) { - return false; - } - } diff --git a/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php b/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php index 7fba83c15d..630e7c0d53 100644 --- a/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php +++ b/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php @@ -27,11 +27,7 @@ final class ConduitAPI_repository_create_Method 'encoding' => 'optional string', 'tracking' => 'optional bool', 'uri' => 'optional string', - 'sshUser' => 'optional string', - 'sshKey' => 'optional string', - 'sshKeyFile' => 'optional string', - 'httpUser' => 'optional string', - 'httpPassword' => 'optional string', + 'credentialPHID' => 'optional string', 'localPath' => 'optional string', 'svnSubpath' => 'optional string', 'branchFilter' => 'optional list', @@ -83,7 +79,7 @@ final class ConduitAPI_repository_create_Method $repository->setName($request->getValue('name')); $callsign = $request->getValue('callsign'); - if (!preg_match('/[A-Z]+$/', $callsign)) { + if (!preg_match('/^[A-Z]+$/', $callsign)) { throw new ConduitException('ERR-BAD-CALLSIGN'); } $repository->setCallsign($callsign); @@ -100,6 +96,8 @@ final class ConduitAPI_repository_create_Method } $repository->setVersionControlSystem($map[$vcs]); + $repository->setCredentialPHID($request->getValue('credentialPHID')); + $details = array( 'encoding' => $request->getValue('encoding'), 'description' => $request->getValue('description'), @@ -114,9 +112,6 @@ final class ConduitAPI_repository_create_Method true), 'pull-frequency' => $request->getValue('pullFrequency'), 'default-branch' => $request->getValue('defaultBranch'), - 'ssh-login' => $request->getValue('sshUser'), - 'ssh-key' => $request->getValue('sshKey'), - 'ssh-keyfile' => $request->getValue('sshKeyFile'), 'herald-disabled' => !$request->getValue('heraldEnabled', true), 'svn-subpath' => $request->getValue('svnSubpath'), 'disable-autoclose' => !$request->getValue('autocloseEnabled', true), diff --git a/src/applications/repository/conduit/ConduitAPI_repository_query_Method.php b/src/applications/repository/conduit/ConduitAPI_repository_query_Method.php index 8742d375ef..9f17b02c9e 100644 --- a/src/applications/repository/conduit/ConduitAPI_repository_query_Method.php +++ b/src/applications/repository/conduit/ConduitAPI_repository_query_Method.php @@ -1,8 +1,5 @@ 'optional list', + 'phids' => 'optional list', + 'callsigns' => 'optional list', + 'vcsTypes' => 'optional list', + 'remoteURIs' => 'optional list', + 'uuids' => 'optional list', ); } @@ -33,9 +36,40 @@ final class ConduitAPI_repository_query_Method } protected function execute(ConduitAPIRequest $request) { - $repositories = id(new PhabricatorRepositoryQuery()) - ->setViewer($request->getUser()) - ->execute(); + $query = id(new PhabricatorRepositoryQuery()) + ->setViewer($request->getUser()); + + $ids = $request->getValue('ids', array()); + if ($ids) { + $query->withIDs($ids); + } + + $phids = $request->getValue('phids', array()); + if ($phids) { + $query->withPHIDs($phids); + } + + $callsigns = $request->getValue('callsigns', array()); + if ($callsigns) { + $query->withCallsigns($callsigns); + } + + $vcs_types = $request->getValue('vcsTypes', array()); + if ($vcs_types) { + $query->withTypes($vcs_types); + } + + $remote_uris = $request->getValue('remoteURIs', array()); + if ($remote_uris) { + $query->withRemoteURIs($remote_uris); + } + + $uuids = $request->getValue('uuids', array()); + if ($uuids) { + $query->withUUIDs($uuids); + } + + $repositories = $query->execute(); $results = array(); foreach ($repositories as $repository) { diff --git a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php index 2330177d42..f98075ddfd 100644 --- a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php +++ b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php @@ -87,8 +87,9 @@ final class PhabricatorRepositoryArcanistProjectEditController id(new AphrontFormTextControl()) ->setLabel('Indexed Languages') ->setName('symbolIndexLanguages') - ->setCaption( - hsprintf('Separate with commas, for example: php, py')) + ->setCaption(pht( + 'Separate with commas, for example: %s', + phutil_tag('tt', array(), 'php, py'))) ->setValue($langs)) ->appendChild( id(new AphrontFormTokenizerControl()) diff --git a/src/applications/repository/daemon/PhabricatorGitGraphStream.php b/src/applications/repository/daemon/PhabricatorGitGraphStream.php index 23688b5cb0..5cbac88a7e 100644 --- a/src/applications/repository/daemon/PhabricatorGitGraphStream.php +++ b/src/applications/repository/daemon/PhabricatorGitGraphStream.php @@ -1,6 +1,7 @@ repository = $repository; $future = $repository->getLocalCommandFuture( - "log --template '{rev}\1{node}\1{date}\1{parents}\2'"); + 'log --template %s --rev %s', + '{rev}\1{node}\1{date}\1{parents}\2', + hgsprintf('reverse(ancestors(%s))', $commit)); $this->iterator = new LinesOfALargeExecFuture($future); $this->iterator->setDelimiter("\2"); diff --git a/src/applications/repository/daemon/PhabricatorRepositoryGraphStream.php b/src/applications/repository/daemon/PhabricatorRepositoryGraphStream.php new file mode 100644 index 0000000000..cf45d8e7d5 --- /dev/null +++ b/src/applications/repository/daemon/PhabricatorRepositoryGraphStream.php @@ -0,0 +1,8 @@ +repair = $repair; - return $this; - } - /* -( Pulling Repositories )----------------------------------------------- */ @@ -94,9 +87,7 @@ final class PhabricatorRepositoryPullLocalDaemon // If any repositories have the NEEDS_UPDATE flag set, pull them // as soon as possible. - $type_need_update = PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE; - $need_update_messages = id(new PhabricatorRepositoryStatusMessage()) - ->loadAllWhere('statusType = %s', $type_need_update); + $need_update_messages = $this->loadRepositoryUpdateMessages(); foreach ($need_update_messages as $message) { $retry_after[$message->getRepositoryID()] = time(); } @@ -144,12 +135,13 @@ final class PhabricatorRepositoryPullLocalDaemon $lock = PhabricatorGlobalLock::newLock($lock_name); $lock->lock(); - $repository->writeStatusMessage( - PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, - null); - try { + $repository->writeStatusMessage( + PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, + null); $this->discoverRepository($repository); + $this->updateRepositoryRefs($repository); + $this->mirrorRepository($repository); $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_FETCH, PhabricatorRepositoryStatusMessage::CODE_OKAY); @@ -191,10 +183,20 @@ final class PhabricatorRepositoryPullLocalDaemon $sleep_until = time() + $min_sleep; } - $this->sleep($sleep_until - time()); + while (($sleep_until - time()) > 0) { + $this->sleep(1); + if ($this->loadRepositoryUpdateMessages()) { + break; + } + } } } + private function loadRepositoryUpdateMessages() { + $type_need_update = PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE; + return id(new PhabricatorRepositoryStatusMessage()) + ->loadAllWhere('statusType = %s', $type_need_update); + } /** * @task pull @@ -223,273 +225,50 @@ final class PhabricatorRepositoryPullLocalDaemon } public function discoverRepository(PhabricatorRepository $repository) { - $vcs = $repository->getVersionControlSystem(); - - $result = null; - $refs = null; - switch ($vcs) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $result = $this->executeGitDiscover($repository); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: - $refs = $this->getDiscoveryEngine($repository) - ->discoverCommits(); - break; - default: - throw new Exception("Unknown VCS '{$vcs}'!"); - } - - if ($refs !== null) { - foreach ($refs as $ref) { - $this->recordCommit( - $repository, - $ref->getIdentifier(), - $ref->getEpoch(), - $ref->getBranch()); - } - } + $refs = $this->getDiscoveryEngine($repository) + ->discoverCommits(); $this->checkIfRepositoryIsFullyImported($repository); - if ($refs !== null) { - return (bool)count($refs); - } else { - return $result; + return (bool)count($refs); + } + + private function mirrorRepository(PhabricatorRepository $repository) { + try { + id(new PhabricatorRepositoryMirrorEngine()) + ->setRepository($repository) + ->pushToMirrors(); + } catch (Exception $ex) { + // TODO: We should report these into the UI properly, but for + // now just complain. These errors are much less severe than + // pull errors. + $proxy = new PhutilProxyException( + pht( + 'Error while pushing "%s" repository to mirrors.', + $repository->getCallsign()), + $ex); + phlog($proxy); } } + private function updateRepositoryRefs(PhabricatorRepository $repository) { + id(new PhabricatorRepositoryRefEngine()) + ->setRepository($repository) + ->updateRefs(); + } + private function getDiscoveryEngine(PhabricatorRepository $repository) { $id = $repository->getID(); if (empty($this->discoveryEngines[$id])) { $engine = id(new PhabricatorRepositoryDiscoveryEngine()) - ->setRepository($repository) - ->setVerbose($this->getVerbose()) - ->setRepairMode($this->repair); + ->setRepository($repository) + ->setVerbose($this->getVerbose()); $this->discoveryEngines[$id] = $engine; } return $this->discoveryEngines[$id]; } - private function isKnownCommit( - PhabricatorRepository $repository, - $target) { - - if ($this->getCache($repository, $target)) { - return true; - } - - if ($this->repair) { - // In repair mode, rediscover the entire repository, ignoring the - // database state. We can hit the local cache above, but if we miss it - // stop the script from going to the database cache. - return false; - } - - $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( - 'repositoryID = %d AND commitIdentifier = %s', - $repository->getID(), - $target); - - if (!$commit) { - return false; - } - - $this->setCache($repository, $target); - while (count($this->commitCache) > 2048) { - array_shift($this->commitCache); - } - - return true; - } - - private function isKnownCommitOnAnyAutocloseBranch( - PhabricatorRepository $repository, - $target) { - - $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( - 'repositoryID = %d AND commitIdentifier = %s', - $repository->getID(), - $target); - - if (!$commit) { - $callsign = $repository->getCallsign(); - - $console = PhutilConsole::getConsole(); - $console->writeErr( - "WARNING: Repository '%s' is missing commits ('%s' is missing from ". - "history). Run '%s' to repair the repository.\n", - $callsign, - $target, - "bin/repository discover --repair {$callsign}"); - - return false; - } - - $data = $commit->loadCommitData(); - if (!$data) { - return false; - } - - if ($repository->shouldAutocloseCommit($commit, $data)) { - return true; - } - - return false; - } - - private function recordCommit( - PhabricatorRepository $repository, - $commit_identifier, - $epoch, - $branch = null) { - - $commit = new PhabricatorRepositoryCommit(); - $commit->setRepositoryID($repository->getID()); - $commit->setCommitIdentifier($commit_identifier); - $commit->setEpoch($epoch); - - $data = new PhabricatorRepositoryCommitData(); - if ($branch) { - $data->setCommitDetail('seenOnBranches', array($branch)); - } - - try { - $commit->openTransaction(); - $commit->save(); - $data->setCommitID($commit->getID()); - $data->save(); - $commit->saveTransaction(); - - $this->insertTask($repository, $commit); - - queryfx( - $repository->establishConnection('w'), - 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) - VALUES (%d, 1, %d, %d) - ON DUPLICATE KEY UPDATE - size = size + 1, - lastCommitID = - IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), - epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', - PhabricatorRepository::TABLE_SUMMARY, - $repository->getID(), - $commit->getID(), - $epoch); - - if ($this->repair) { - // Normally, the query should throw a duplicate key exception. If we - // reach this in repair mode, we've actually performed a repair. - $this->log("Repaired commit '{$commit_identifier}'."); - } - - $this->setCache($repository, $commit_identifier); - - PhutilEventEngine::dispatchEvent( - new PhabricatorEvent( - PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT, - array( - 'repository' => $repository, - 'commit' => $commit, - ))); - - } catch (AphrontQueryDuplicateKeyException $ex) { - $commit->killTransaction(); - // Ignore. This can happen because we discover the same new commit - // more than once when looking at history, or because of races or - // data inconsistency or cosmic radiation; in any case, we're still - // in a good state if we ignore the failure. - $this->setCache($repository, $commit_identifier); - } - } - - private function updateCommit( - PhabricatorRepository $repository, - $commit_identifier, - $branch) { - - $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( - 'repositoryID = %d AND commitIdentifier = %s', - $repository->getID(), - $commit_identifier); - - if (!$commit) { - // This can happen if the phabricator DB doesn't have the commit info, - // or the commit is so big that phabricator couldn't parse it. In this - // case we just ignore it. - return; - } - - $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( - 'commitID = %d', - $commit->getID()); - if (!$data) { - $data = new PhabricatorRepositoryCommitData(); - $data->setCommitID($commit->getID()); - } - $branches = $data->getCommitDetail('seenOnBranches', array()); - $branches[] = $branch; - $data->setCommitDetail('seenOnBranches', $branches); - $data->save(); - - $this->insertTask( - $repository, - $commit, - array( - 'only' => true - )); - } - - private function insertTask( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit, - $data = array()) { - - $vcs = $repository->getVersionControlSystem(); - switch ($vcs) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: - $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; - break; - default: - throw new Exception("Unknown repository type '{$vcs}'!"); - } - - $data['commitID'] = $commit->getID(); - - PhabricatorWorker::scheduleTask($class, $data); - } - - - private function setCache( - PhabricatorRepository $repository, - $commit_identifier) { - - $key = $this->getCacheKey($repository, $commit_identifier); - $this->commitCache[$key] = true; - } - - private function getCache( - PhabricatorRepository $repository, - $commit_identifier) { - - $key = $this->getCacheKey($repository, $commit_identifier); - return idx($this->commitCache, $key, false); - } - - private function getCacheKey( - PhabricatorRepository $repository, - $commit_identifier) { - - return $repository->getID().':'.$commit_identifier; - } - private function checkIfRepositoryIsFullyImported( PhabricatorRepository $repository) { @@ -504,10 +283,11 @@ final class PhabricatorRepositoryPullLocalDaemon // Look for any commit which hasn't imported. $unparsed_commit = queryfx_one( $repository->establishConnection('r'), - 'SELECT * FROM %T WHERE repositoryID = %d AND importStatus != %d + 'SELECT * FROM %T WHERE repositoryID = %d AND (importStatus & %d) != %d LIMIT 1', id(new PhabricatorRepositoryCommit())->getTableName(), $repository->getID(), + PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); if ($unparsed_commit) { // We found a commit which still needs to import, so we can't clear the @@ -525,216 +305,4 @@ final class PhabricatorRepositoryPullLocalDaemon $repository->saveTransaction(); } -/* -( Git Implementation )------------------------------------------------- */ - - - /** - * @task git - */ - private function executeGitDiscover( - PhabricatorRepository $repository) { - - if (!$repository->isHosted()) { - list($remotes) = $repository->execxLocalCommand( - 'remote show -n origin'); - - $matches = null; - if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { - throw new Exception( - "Expected 'Fetch URL' in 'git remote show -n origin'."); - } - - self::executeGitVerifySameOrigin( - $matches[1], - $repository->getRemoteURI(), - $repository->getLocalPath()); - } - - $refs = id(new DiffusionLowLevelGitRefQuery()) - ->setRepository($repository) - ->withIsOriginBranch(true) - ->execute(); - - $branches = mpull($refs, 'getCommitIdentifier', 'getShortName'); - - if (!$branches) { - // This repository has no branches at all, so we don't need to do - // anything. Generally, this means the repository is empty. - return; - } - - $callsign = $repository->getCallsign(); - - $tracked_something = false; - - $this->log("Discovering commits in repository '{$callsign}'..."); - foreach ($branches as $name => $commit) { - $this->log("Examining branch '{$name}', at {$commit}."); - if (!$repository->shouldTrackBranch($name)) { - $this->log("Skipping, branch is untracked."); - continue; - } - - $tracked_something = true; - - if ($this->isKnownCommit($repository, $commit)) { - $this->log("Skipping, HEAD is known."); - continue; - } - - $this->log("Looking for new commits."); - $this->executeGitDiscoverCommit($repository, $commit, $name, false); - } - - if (!$tracked_something) { - $repo_name = $repository->getName(); - $repo_callsign = $repository->getCallsign(); - throw new Exception( - "Repository r{$repo_callsign} '{$repo_name}' has no tracked branches! ". - "Verify that your branch filtering settings are correct."); - } - - - $this->log("Discovering commits on autoclose branches..."); - foreach ($branches as $name => $commit) { - $this->log("Examining branch '{$name}', at {$commit}'."); - if (!$repository->shouldTrackBranch($name)) { - $this->log("Skipping, branch is untracked."); - continue; - } - - if (!$repository->shouldAutocloseBranch($name)) { - $this->log("Skipping, branch is not autoclose."); - continue; - } - - if ($this->isKnownCommitOnAnyAutocloseBranch($repository, $commit)) { - $this->log("Skipping, commit is known on an autoclose branch."); - continue; - } - - $this->log("Looking for new autoclose commits."); - $this->executeGitDiscoverCommit($repository, $commit, $name, true); - } - } - - - /** - * @task git - */ - private function executeGitDiscoverCommit( - PhabricatorRepository $repository, - $commit, - $branch, - $autoclose) { - - $discover = array($commit); - $insert = array($commit); - - $seen_parent = array(); - - $stream = new PhabricatorGitGraphStream($repository, $commit); - - while (true) { - $target = array_pop($discover); - $parents = $stream->getParents($target); - foreach ($parents as $parent) { - if (isset($seen_parent[$parent])) { - // We end up in a loop here somehow when we parse Arcanist if we - // don't do this. TODO: Figure out why and draw a pretty diagram - // since it's not evident how parsing a DAG with this causes the - // loop to stop terminating. - continue; - } - $seen_parent[$parent] = true; - if ($autoclose) { - $known = $this->isKnownCommitOnAnyAutocloseBranch( - $repository, - $parent); - } else { - $known = $this->isKnownCommit($repository, $parent); - } - if (!$known) { - $this->log("Discovered commit '{$parent}'."); - $discover[] = $parent; - $insert[] = $parent; - } - } - if (empty($discover)) { - break; - } - } - - $n = count($insert); - if ($autoclose) { - $this->log("Found {$n} new autoclose commits on branch '{$branch}'."); - } else { - $this->log("Found {$n} new commits on branch '{$branch}'."); - } - - while (true) { - $target = array_pop($insert); - $epoch = $stream->getCommitDate($target); - $epoch = trim($epoch); - - if ($autoclose) { - $this->updateCommit($repository, $target, $branch); - } else { - $this->recordCommit($repository, $target, $epoch, $branch); - } - - if (empty($insert)) { - break; - } - } - } - - - /** - * @task git - */ - public static function executeGitVerifySameOrigin($remote, $expect, $where) { - $remote_path = self::getPathFromGitURI($remote); - $expect_path = self::getPathFromGitURI($expect); - - $remote_match = self::executeGitNormalizePath($remote_path); - $expect_match = self::executeGitNormalizePath($expect_path); - - if ($remote_match != $expect_match) { - throw new Exception( - "Working copy at '{$where}' has a mismatched origin URL. It has ". - "origin URL '{$remote}' (with remote path '{$remote_path}'), but the ". - "configured URL '{$expect}' (with remote path '{$expect_path}') is ". - "expected. Refusing to proceed because this may indicate that the ". - "working copy is actually some other repository."); - } - } - - private static function getPathFromGitURI($raw_uri) { - $uri = new PhutilURI($raw_uri); - if ($uri->getProtocol()) { - return $uri->getPath(); - } - - $uri = new PhutilGitURI($raw_uri); - if ($uri->getDomain()) { - return $uri->getPath(); - } - - return $raw_uri; - } - - - /** - * @task git - */ - private static function executeGitNormalizePath($path) { - // Strip away "/" and ".git", so similar paths correctly match. - - $path = trim($path, '/'); - $path = preg_replace('/\.git$/', '', $path); - return $path; - } - - } diff --git a/src/applications/repository/data/PhabricatorRepositoryParsedChange.php b/src/applications/repository/data/PhabricatorRepositoryParsedChange.php new file mode 100644 index 0000000000..668a2f8c89 --- /dev/null +++ b/src/applications/repository/data/PhabricatorRepositoryParsedChange.php @@ -0,0 +1,77 @@ +pathID = $path_id; + return $this; + } + + public function getPathID() { + return $this->pathID; + } + + public function setCommitSequence($commit_sequence) { + $this->commitSequence = $commit_sequence; + return $this; + } + + public function getCommitSequence() { + return $this->commitSequence; + } + + public function setIsDirect($is_direct) { + $this->isDirect = $is_direct; + return $this; + } + + public function getIsDirect() { + return $this->isDirect; + } + + public function setFileType($file_type) { + $this->fileType = $file_type; + return $this; + } + + public function getFileType() { + return $this->fileType; + } + + public function setChangeType($change_type) { + $this->changeType = $change_type; + return $this; + } + + public function getChangeType() { + return $this->changeType; + } + + public function setTargetCommitID($target_commit_id) { + $this->targetCommitID = $target_commit_id; + return $this; + } + + public function getTargetCommitID() { + return $this->targetCommitID; + } + + + public function setTargetPathID($target_path_id) { + $this->targetPathID = $target_path_id; + return $this; + } + + public function getTargetPathID() { + return $this->targetPathID; + } + +} diff --git a/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php b/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php new file mode 100644 index 0000000000..bcfd6b061c --- /dev/null +++ b/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php @@ -0,0 +1,123 @@ +getNormalizedPath() == $norm_b->getNormalizedPath()) { + * // URIs appear to point at the same repository. + * } else { + * // URIs are very unlikely to be the same repository. + * } + * + * Because a repository can be hosted at arbitrarly many arbitrary URIs, there + * is no way to completely prevent false negatives by only examining URIs + * (that is, repositories with totally different URIs could really be the same). + * However, normalization is relatively agressive and false negatives should + * be rare: if normalization says two URIs are different repositories, they + * probably are. + * + * @task normal Normalizing URIs + */ +final class PhabricatorRepositoryURINormalizer extends Phobject { + + const TYPE_GIT = 'git'; + const TYPE_SVN = 'svn'; + const TYPE_MERCURIAL = 'hg'; + + private $type; + private $uri; + + public function __construct($type, $uri) { + switch ($type) { + case self::TYPE_GIT: + case self::TYPE_SVN: + case self::TYPE_MERCURIAL: + break; + default: + throw new Exception(pht('Unknown URI type "%s"!')); + } + + $this->type = $type; + $this->uri = $uri; + } + + +/* -( Normalizing URIs )--------------------------------------------------- */ + + + /** + * @task normal + */ + public function getPath() { + switch ($this->type) { + case self::TYPE_GIT: + $uri = new PhutilURI($this->uri); + if ($uri->getProtocol()) { + return $uri->getPath(); + } + + $uri = new PhutilGitURI($this->uri); + if ($uri->getDomain()) { + return $uri->getPath(); + } + + return $this->uri; + case self::TYPE_SVN: + case self::TYPE_MERCURIAL: + $uri = new PhutilURI($this->uri); + if ($uri->getProtocol()) { + return $uri->getPath(); + } + + return $this->uri; + } + } + + + /** + * @task normal + */ + public function getNormalizedPath() { + $path = $this->getPath(); + $path = trim($path, '/'); + + switch ($this->type) { + case self::TYPE_GIT: + $path = preg_replace('/\.git$/', '', $path); + break; + case self::TYPE_SVN: + case self::TYPE_MERCURIAL: + break; + } + + // If this is a Phabricator URI, strip it down to the callsign. We mutably + // allow you to clone repositories as "/diffusion/X/anything.git", for + // example. + + $matches = null; + if (preg_match('@^(diffusion/[A-Z]+)@', $path, $matches)) { + $path = $matches[1]; + } + + return $path; + } + +} diff --git a/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php b/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php new file mode 100644 index 0000000000..81dd735562 --- /dev/null +++ b/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php @@ -0,0 +1,51 @@ + 'path', + 'https://user@domain.com/path.git' => 'path', + 'git@domain.com:path.git' => 'path', + 'ssh://user@gitserv002.com/path.git' => 'path', + 'ssh://htaft@domain.com/path.git' => 'path', + 'ssh://user@domain.com/bananas.git' => 'bananas', + 'git@domain.com:bananas.git' => 'bananas', + 'user@domain.com:path/repo' => 'path/repo', + 'user@domain.com:path/repo/' => 'path/repo', + 'file:///path/to/local/repo.git' => 'path/to/local/repo', + '/path/to/local/repo.git' => 'path/to/local/repo', + 'ssh://something.com/diffusion/X/anything.git' => 'diffusion/X', + 'ssh://something.com/diffusion/X/' => 'diffusion/X', + ); + + $type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; + + foreach ($cases as $input => $expect) { + $normal = new PhabricatorRepositoryURINormalizer($type_git, $input); + $this->assertEqual( + $expect, + $normal->getNormalizedPath(), + pht('Normalized Git path for "%s".', $input)); + } + } + + public function testSVNURINormalizer() { + $cases = array( + 'file:///path/to/repo' => 'path/to/repo', + 'file:///path/to/repo/' => 'path/to/repo', + ); + + $type_svn = PhabricatorRepositoryURINormalizer::TYPE_SVN; + + foreach ($cases as $input => $expect) { + $normal = new PhabricatorRepositoryURINormalizer($type_svn, $input); + $this->assertEqual( + $expect, + $normal->getNormalizedPath(), + pht('Normalized SVN path for "%s".', $input)); + } + } + +} diff --git a/src/applications/repository/editor/PhabricatorRepositoryEditor.php b/src/applications/repository/editor/PhabricatorRepositoryEditor.php index cf28febdf0..399302c363 100644 --- a/src/applications/repository/editor/PhabricatorRepositoryEditor.php +++ b/src/applications/repository/editor/PhabricatorRepositoryEditor.php @@ -29,7 +29,11 @@ final class PhabricatorRepositoryEditor $types[] = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP; $types[] = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH; $types[] = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY; + $types[] = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL; + $types[] = PhabricatorRepositoryTransaction::TYPE_DANGEROUS; + $types[] = PhabricatorRepositoryTransaction::TYPE_CLONE_NAME; + $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -67,16 +71,6 @@ final class PhabricatorRepositoryEditor return (int)!$object->getDetail('disable-autoclose'); case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: return $object->getDetail('remote-uri'); - case PhabricatorRepositoryTransaction::TYPE_SSH_LOGIN: - return $object->getDetail('ssh-login'); - case PhabricatorRepositoryTransaction::TYPE_SSH_KEY: - return $object->getDetail('ssh-key'); - case PhabricatorRepositoryTransaction::TYPE_SSH_KEYFILE: - return $object->getDetail('ssh-keyfile'); - case PhabricatorRepositoryTransaction::TYPE_HTTP_LOGIN: - return $object->getDetail('http-login'); - case PhabricatorRepositoryTransaction::TYPE_HTTP_PASS: - return $object->getDetail('http-pass'); case PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH: return $object->getDetail('local-path'); case PhabricatorRepositoryTransaction::TYPE_HOSTING: @@ -87,6 +81,12 @@ final class PhabricatorRepositoryEditor return $object->getServeOverSSH(); case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: return $object->getPushPolicy(); + case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: + return $object->getCredentialPHID(); + case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: + return $object->shouldAllowDangerousChanges(); + case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: + return $object->getDetail('clone-name'); } } @@ -116,6 +116,9 @@ final class PhabricatorRepositoryEditor case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP: case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH: case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: + case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: + case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: + case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: return $xaction->getNewValue(); case PhabricatorRepositoryTransaction::TYPE_NOTIFY: case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: @@ -168,21 +171,6 @@ final class PhabricatorRepositoryEditor case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: $object->setDetail('remote-uri', $xaction->getNewValue()); break; - case PhabricatorRepositoryTransaction::TYPE_SSH_LOGIN: - $object->setDetail('ssh-login', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_SSH_KEY: - $object->setDetail('ssh-key', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_SSH_KEYFILE: - $object->setDetail('ssh-keyfile', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_HTTP_LOGIN: - $object->setDetail('http-login', $xaction->getNewValue()); - break; - case PhabricatorRepositoryTransaction::TYPE_HTTP_PASS: - $object->setDetail('http-pass', $xaction->getNewValue()); - break; case PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH: $object->setDetail('local-path', $xaction->getNewValue()); break; @@ -194,6 +182,14 @@ final class PhabricatorRepositoryEditor return $object->setServeOverSSH($xaction->getNewValue()); case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: return $object->setPushPolicy($xaction->getNewValue()); + case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: + return $object->setCredentialPHID($xaction->getNewValue()); + case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: + $object->setDetail('allow-dangerous-changes', $xaction->getNewValue()); + return; + case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: + $object->setDetail('clone-name', $xaction->getNewValue()); + return; case PhabricatorRepositoryTransaction::TYPE_ENCODING: // Make sure the encoding is valid by converting to UTF-8. This tests // that the user has mbstring installed, and also that they didn't type @@ -221,7 +217,32 @@ final class PhabricatorRepositoryEditor protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - return; + + switch ($xaction->getTransactionType()) { + case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: + // Adjust the object <-> credential edge for this repository. + + $old_phid = $xaction->getOldValue(); + $new_phid = $xaction->getNewValue(); + + $editor = id(new PhabricatorEdgeEditor()) + ->setActor($this->requireActor()); + + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_USES_CREDENTIAL; + $src_phid = $object->getPHID(); + + if ($old_phid) { + $editor->removeEdge($src_phid, $edge_type, $old_phid); + } + + if ($new_phid) { + $editor->addEdge($src_phid, $edge_type, $new_phid); + } + + $editor->save(); + break; + } + } protected function mergeTransactions( @@ -278,6 +299,9 @@ final class PhabricatorRepositoryEditor case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP: case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH: case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: + case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: + case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: + case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, diff --git a/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php b/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php index c010088a92..9a43a6be6a 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php +++ b/src/applications/repository/engine/PhabricatorRepositoryCommitRef.php @@ -5,6 +5,7 @@ final class PhabricatorRepositoryCommitRef { private $identifier; private $epoch; private $branch; + private $canCloseImmediately; public function setIdentifier($identifier) { $this->identifier = $identifier; @@ -33,4 +34,13 @@ final class PhabricatorRepositoryCommitRef { return $this->branch; } + public function setCanCloseImmediately($can_close_immediately) { + $this->canCloseImmediately = $can_close_immediately; + return $this; + } + + public function getCanCloseImmediately() { + return $this->canCloseImmediately; + } + } diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index 17374a02d6..3744f09d11 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -43,19 +43,21 @@ final class PhabricatorRepositoryDiscoveryEngine case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $refs = $this->discoverMercurialCommits(); break; -/* - TODO: Implement this! - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $refs = $this->discoverGitCommits(); break; -*/ default: throw new Exception("Unknown VCS '{$vcs}'!"); } - // Mark discovered commits in the cache. + // Record discovered commits and mark them in the cache. foreach ($refs as $ref) { + $this->recordCommit( + $repository, + $ref->getIdentifier(), + $ref->getEpoch(), + $ref->getCanCloseImmediately()); + $this->commitCache[$ref->getIdentifier()] = true; } @@ -63,6 +65,157 @@ final class PhabricatorRepositoryDiscoveryEngine } +/* -( Discovering Git Repositories )--------------------------------------- */ + + + /** + * @task git + */ + private function discoverGitCommits() { + $repository = $this->getRepository(); + + if (!$repository->isHosted()) { + $this->verifyGitOrigin($repository); + } + + $branches = id(new DiffusionLowLevelGitRefQuery()) + ->setRepository($repository) + ->withIsOriginBranch(true) + ->execute(); + + if (!$branches) { + // This repository has no branches at all, so we don't need to do + // anything. Generally, this means the repository is empty. + return array(); + } + + $branches = $this->sortBranches($branches); + $branches = mpull($branches, 'getCommitIdentifier', 'getShortName'); + + $this->log( + pht( + 'Discovering commits in repository %s.', + $repository->getCallsign())); + + $refs = array(); + foreach ($branches as $name => $commit) { + $this->log(pht('Examining branch "%s", at "%s".', $name, $commit)); + + if (!$repository->shouldTrackBranch($name)) { + $this->log(pht("Skipping, branch is untracked.")); + continue; + } + + if ($this->isKnownCommit($commit)) { + $this->log(pht("Skipping, HEAD is known.")); + continue; + } + + $this->log(pht("Looking for new commits.")); + + $refs[] = $this->discoverStreamAncestry( + new PhabricatorGitGraphStream($repository, $commit), + $commit, + $repository->shouldAutocloseBranch($name)); + } + + return array_mergev($refs); + } + + + /** + * Verify that the "origin" remote exists, and points at the correct URI. + * + * This catches or corrects some types of misconfiguration, and also repairs + * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones. + * See T4041. + * + * @param PhabricatorRepository Repository to verify. + * @return void + */ + private function verifyGitOrigin(PhabricatorRepository $repository) { + list($remotes) = $repository->execxLocalCommand( + 'remote show -n origin'); + + $matches = null; + if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { + throw new Exception( + "Expected 'Fetch URL' in 'git remote show -n origin'."); + } + + $remote_uri = $matches[1]; + $expect_remote = $repository->getRemoteURI(); + + if ($remote_uri == "origin") { + // If a remote does not exist, git pretends it does and prints out a + // made up remote where the URI is the same as the remote name. This is + // definitely not correct. + + // Possibly, we should use `git remote --verbose` instead, which does not + // suffer from this problem (but is a little more complicated to parse). + $valid = false; + $exists = false; + } else { + $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; + + $remote_normal = id(new PhabricatorRepositoryURINormalizer( + $normal_type_git, + $remote_uri))->getNormalizedPath(); + + $expect_normal = id(new PhabricatorRepositoryURINormalizer( + $normal_type_git, + $expect_remote))->getNormalizedPath(); + + $valid = ($remote_normal == $expect_normal); + $exists = true; + } + + if (!$valid) { + if (!$exists) { + // If there's no "origin" remote, just create it regardless of how + // strongly we own the working copy. There is almost no conceivable + // scenario in which this could do damage. + $this->log( + pht( + 'Remote "origin" does not exist. Creating "origin", with '. + 'URI "%s".', + $expect_remote)); + $repository->execxLocalCommand( + 'remote add origin %P', + $repository->getRemoteURIEnvelope()); + + // NOTE: This doesn't fetch the origin (it just creates it), so we won't + // know about origin branches until the next "pull" happens. That's fine + // for our purposes, but might impact things in the future. + } else { + if ($repository->canDestroyWorkingCopy()) { + // Bad remote, but we can try to repair it. + $this->log( + pht( + 'Remote "origin" exists, but is pointed at the wrong URI, "%s". '. + 'Resetting origin URI to "%s.', + $remote_uri, + $expect_remote)); + $repository->execxLocalCommand( + 'remote set-url origin %P', + $repository->getRemoteURIEnvelope()); + } else { + // Bad remote and we aren't comfortable repairing it. + $message = pht( + 'Working copy at "%s" has a mismatched origin URI, "%s". '. + 'The expected origin URI is "%s". Fix your configuration, or '. + 'set the remote URI correctly. To avoid breaking anything, '. + 'Phabricator will not automatically fix this.', + $repository->getLocalPath(), + $remote_uri, + $expect_remote); + throw new Exception($message); + } + } + } + } + + /* -( Discovering Subversion Repositories )-------------------------------- */ @@ -72,6 +225,10 @@ final class PhabricatorRepositoryDiscoveryEngine private function discoverSubversionCommits() { $repository = $this->getRepository(); + if (!$repository->isHosted()) { + $this->verifySubversionRoot($repository); + } + $upper_bound = null; $limit = 1; $refs = array(); @@ -88,10 +245,9 @@ final class PhabricatorRepositoryDiscoveryEngine try { list($xml, $stderr) = $repository->execxRemoteCommand( - 'log --xml --quiet --limit %d %s@%s', + 'log --xml --quiet --limit %d %s', $limit, - $repository->getSubversionBaseURI(), - $at_rev); + $repository->getSubversionBaseURI($at_rev)); } catch (CommandException $ex) { $stderr = $ex->getStdErr(); if (preg_match('/(path|File) not found/', $stderr)) { @@ -109,7 +265,8 @@ final class PhabricatorRepositoryDiscoveryEngine $epoch = (int)strtotime((string)$entry->date[0]); $refs[$identifier] = id(new PhabricatorRepositoryCommitRef()) ->setIdentifier($identifier) - ->setEpoch($epoch); + ->setEpoch($epoch) + ->setCanCloseImmediately(true); if ($upper_bound === null) { $upper_bound = $identifier; @@ -136,6 +293,43 @@ final class PhabricatorRepositoryDiscoveryEngine } + private function verifySubversionRoot(PhabricatorRepository $repository) { + list($xml) = $repository->execxRemoteCommand( + 'info --xml %s', + $repository->getSubversionPathURI()); + + $xml = phutil_utf8ize($xml); + $xml = new SimpleXMLElement($xml); + + $remote_root = (string)($xml->entry[0]->repository[0]->root[0]); + $expect_root = $repository->getSubversionPathURI(); + + $normal_type_svn = PhabricatorRepositoryURINormalizer::TYPE_SVN; + + $remote_normal = id(new PhabricatorRepositoryURINormalizer( + $normal_type_svn, + $remote_root))->getNormalizedPath(); + + $expect_normal = id(new PhabricatorRepositoryURINormalizer( + $normal_type_svn, + $expect_root))->getNormalizedPath(); + + if ($remote_normal != $expect_normal) { + throw new Exception( + pht( + 'Repository "%s" does not have a correctly configured remote URI. '. + 'The remote URI for a Subversion repository MUST point at the '. + 'repository root. The root for this repository is "%s", but the '. + 'configured URI is "%s". To resolve this error, set the remote URI '. + 'to point at the repository root. If you want to import only part '. + 'of a Subversion repository, use the "Import Only" option.', + $repository->getCallsign(), + $remote_root, + $expect_root)); + } + } + + /* -( Discovering Mercurial Repositories )--------------------------------- */ @@ -148,42 +342,49 @@ final class PhabricatorRepositoryDiscoveryEngine $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository) ->execute(); - $branches = mpull($branches, 'getHeadCommitIdentifier', 'getName'); $refs = array(); - foreach ($branches as $name => $commit) { - $this->log("Examining branch '{$name}', at {$commit}'."); + foreach ($branches as $branch) { + // NOTE: Mercurial branches may have multiple heads, so the names may + // not be unique. + $name = $branch->getShortName(); + $commit = $branch->getCommitIdentifier(); + + $this->log(pht('Examining branch "%s" head "%s".', $name, $commit)); if (!$repository->shouldTrackBranch($name)) { - $this->log("Skipping, branch is untracked."); + $this->log(pht("Skipping, branch is untracked.")); continue; } if ($this->isKnownCommit($commit)) { - $this->log("Skipping, tip is a known commit."); + $this->log(pht("Skipping, this head is a known commit.")); continue; } - $this->log("Looking for new commits."); - $refs[] = $this->discoverMercurialAncestry($repository, $commit); + $this->log(pht("Looking for new commits.")); + + $refs[] = $this->discoverStreamAncestry( + new PhabricatorMercurialGraphStream($repository, $commit), + $commit, + $close_immediately = true); } return array_mergev($refs); } - /** - * @task hg - */ - private function discoverMercurialAncestry( - PhabricatorRepository $repository, - $commit) { +/* -( Internals )---------------------------------------------------------- */ + + + private function discoverStreamAncestry( + PhabricatorRepositoryGraphStream $stream, + $commit, + $close_immediately) { $discover = array($commit); $graph = array(); $seen = array(); - $stream = new PhabricatorMercurialGraphStream($repository); - // Find all the reachable, undiscovered commits. Build a graph of the // edges. while ($discover) { @@ -215,16 +416,14 @@ final class PhabricatorRepositoryDiscoveryEngine foreach ($commits as $commit) { $refs[] = id(new PhabricatorRepositoryCommitRef()) ->setIdentifier($commit) - ->setEpoch($stream->getCommitDate($commit)); + ->setEpoch($stream->getCommitDate($commit)) + ->setCanCloseImmediately($close_immediately); } return $refs; } -/* -( Internals )---------------------------------------------------------- */ - - private function reduceGraph(array $edges) { foreach ($edges as $commit => $parents) { $edges[$commit] = array_keys($parents); @@ -272,4 +471,121 @@ final class PhabricatorRepositoryDiscoveryEngine return true; } + + /** + * Sort branches so we process closeable branches first. This makes the + * whole import process a little cheaper, since we can close these commits + * the first time through rather than catching them in the refs step. + * + * @task internal + * + * @param list List of branch heads. + * @return list Sorted list of branch heads. + */ + private function sortBranches(array $branches) { + $repository = $this->getRepository(); + + $head_branches = array(); + $tail_branches = array(); + foreach ($branches as $branch) { + $name = $branch->getShortName(); + + if ($repository->shouldAutocloseBranch($name)) { + $head_branches[] = $branch; + } else { + $tail_branches[] = $branch; + } + } + + return array_merge($head_branches, $tail_branches); + } + + + private function recordCommit( + PhabricatorRepository $repository, + $commit_identifier, + $epoch, + $close_immediately) { + + $commit = new PhabricatorRepositoryCommit(); + $commit->setRepositoryID($repository->getID()); + $commit->setCommitIdentifier($commit_identifier); + $commit->setEpoch($epoch); + if ($close_immediately) { + $commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE); + } + + $data = new PhabricatorRepositoryCommitData(); + + try { + $commit->openTransaction(); + $commit->save(); + $data->setCommitID($commit->getID()); + $data->save(); + $commit->saveTransaction(); + + $this->insertTask($repository, $commit); + + queryfx( + $repository->establishConnection('w'), + 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) + VALUES (%d, 1, %d, %d) + ON DUPLICATE KEY UPDATE + size = size + 1, + lastCommitID = + IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), + epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', + PhabricatorRepository::TABLE_SUMMARY, + $repository->getID(), + $commit->getID(), + $epoch); + + if ($this->repairMode) { + // Normally, the query should throw a duplicate key exception. If we + // reach this in repair mode, we've actually performed a repair. + $this->log(pht('Repaired commit "%s".', $commit_identifier)); + } + + PhutilEventEngine::dispatchEvent( + new PhabricatorEvent( + PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT, + array( + 'repository' => $repository, + 'commit' => $commit, + ))); + + } catch (AphrontQueryDuplicateKeyException $ex) { + $commit->killTransaction(); + // Ignore. This can happen because we discover the same new commit + // more than once when looking at history, or because of races or + // data inconsistency or cosmic radiation; in any case, we're still + // in a good state if we ignore the failure. + } + } + + private function insertTask( + PhabricatorRepository $repository, + PhabricatorRepositoryCommit $commit, + $data = array()) { + + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; + break; + default: + throw new Exception("Unknown repository type '{$vcs}'!"); + } + + $data['commitID'] = $commit->getID(); + + PhabricatorWorker::scheduleTask($class, $data); + } + } diff --git a/src/applications/repository/engine/PhabricatorRepositoryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryEngine.php index 7685a91ee5..ce8fb183a8 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryEngine.php @@ -47,6 +47,11 @@ abstract class PhabricatorRepositoryEngine { } + public function getViewer() { + return PhabricatorUser::getOmnipotentUser(); + } + + /** * @task internal */ @@ -54,7 +59,7 @@ abstract class PhabricatorRepositoryEngine { if ($this->getVerbose()) { $console = PhutilConsole::getConsole(); $argv = func_get_args(); - $argv[0] = $argv[0]."\n"; + array_unshift($argv, "%s\n"); call_user_func_array(array($console, 'writeOut'), $argv); } return $this; diff --git a/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php b/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php new file mode 100644 index 0000000000..d73b2bed0f --- /dev/null +++ b/src/applications/repository/engine/PhabricatorRepositoryMirrorEngine.php @@ -0,0 +1,99 @@ +getRepository(); + + if (!$repository->canMirror()) { + return; + } + + $mirrors = id(new PhabricatorRepositoryMirrorQuery()) + ->setViewer($this->getViewer()) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->execute(); + + $exceptions = array(); + foreach ($mirrors as $mirror) { + try { + $this->pushRepositoryToMirror($repository, $mirror); + } catch (Exception $ex) { + $exceptions[] = $ex; + } + } + + if ($exceptions) { + throw new PhutilAggregateException( + pht( + 'Exceptions occurred while mirroring the "%s" repository.', + $repository->getCallsign()), + $exceptions); + } + } + + private function pushRepositoryToMirror( + PhabricatorRepository $repository, + PhabricatorRepositoryMirror $mirror) { + + // TODO: This is a little bit janky, but we don't have first-class + // infrastructure for running remote commands against an arbitrary remote + // right now. Just make an emphemeral copy of the repository and muck with + // it a little bit. In the medium term, we should pull this command stuff + // out and use it here and for "Land to ...". + + $proxy = clone $repository; + $proxy->makeEphemeral(); + + $proxy->setDetail('hosting-enabled', false); + $proxy->setDetail('remote-uri', $mirror->getRemoteURI()); + $proxy->setCredentialPHID($mirror->getCredentialPHID()); + + $this->log(pht('Pushing to remote "%s"...', $mirror->getRemoteURI())); + + if ($proxy->isGit()) { + $this->pushToGitRepository($proxy); + } else if ($proxy->isHg()) { + $this->pushToHgRepository($proxy); + } else { + throw new Exception(pht('Unsupported VCS!')); + } + } + + private function pushToGitRepository( + PhabricatorRepository $proxy) { + + $future = $proxy->getRemoteCommandFuture( + 'push --verbose --mirror -- %P', + $proxy->getRemoteURIEnvelope()); + + $future + ->setCWD($proxy->getLocalPath()) + ->resolvex(); + } + + private function pushToHgRepository( + PhabricatorRepository $proxy) { + + $future = $proxy->getRemoteCommandFuture( + 'push --verbose --rev tip -- %P', + $proxy->getRemoteURIEnvelope()); + + try { + $future + ->setCWD($proxy->getLocalPath()) + ->resolvex(); + } catch (CommandException $ex) { + if (preg_match('/no changes found/', $ex->getStdOut())) { + // mercurial says nothing changed, but that's good + } else { + throw $ex; + } + } + } + +} diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php old mode 100644 new mode 100755 index 2d2fcb3e7e..e935740743 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -82,24 +82,33 @@ final class PhabricatorRepositoryPullEngine $this->executeSubversionCreate(); } } else { - if ($repository->isHosted()) { - $this->logPull( - pht( - "Repository '%s' is hosted, so Phabricator does not pull ". - "updates for it.", - $callsign)); - } else { + if (!$repository->isHosted()) { $this->logPull( pht( "Updating the working copy for repository '%s'.", $callsign)); if ($is_git) { $this->executeGitUpdate(); - } else { + } else if ($is_hg) { $this->executeMercurialUpdate(); } } } + + if ($repository->isHosted()) { + if ($is_git) { + $this->installGitHook(); + } else if ($is_svn) { + $this->installSubversionHook(); + } else if ($is_hg) { + $this->installMercurialHook(); + } + + foreach ($repository->getHookDirectories() as $directory) { + $this->installHookDirectory($directory); + } + } + } catch (Exception $ex) { $this->abortPull( pht('Pull of "%s" failed: %s', $callsign, $ex->getMessage()), @@ -146,6 +155,39 @@ final class PhabricatorRepositoryPullEngine )); } + private function installHook($path) { + $this->log('%s', pht('Installing commit hook to "%s"...', $path)); + + $repository = $this->getRepository(); + $callsign = $repository->getCallsign(); + + $root = dirname(phutil_get_library_root('phabricator')); + $bin = $root.'/bin/commit-hook'; + + $full_php_path = Filesystem::resolveBinary('php'); + $cmd = csprintf( + 'exec %s -f %s -- %s "$@"', + $full_php_path, + $bin, + $callsign); + + $hook = "#!/bin/sh\n{$cmd}\n"; + + Filesystem::writeFile($path, $hook); + Filesystem::changePermissions($path, 0755); + } + + private function installHookDirectory($path) { + $readme = pht( + "To add custom hook scripts to this repository, add them to this ". + "directory.\n\nPhabricator will run any executables in this directory ". + "after running its own checks, as though they were normal hook ". + "scripts."); + + Filesystem::createDirectory($path, 0755); + Filesystem::writeFile($path.'/README', $readme); + } + /* -( Pulling Git Working Copies )----------------------------------------- */ @@ -164,8 +206,8 @@ final class PhabricatorRepositoryPullEngine $path); } else { $repository->execxRemoteCommand( - 'clone --bare -- %s %s', - $repository->getRemoteURI(), + 'clone --bare -- %P %s', + $repository->getRemoteURIEnvelope(), $path); } } @@ -231,7 +273,7 @@ final class PhabricatorRepositoryPullEngine } } - if ($err && $this->canDestroyWorkingCopy($path)) { + if ($err && $repository->canDestroyWorkingCopy()) { phlog("Repository working copy at '{$path}' failed sanity check; ". "destroying and re-cloning. {$message}"); Filesystem::remove($path); @@ -256,15 +298,15 @@ final class PhabricatorRepositoryPullEngine $future->setCWD($path); list($err, $stdout, $stderr) = $future->resolve(); - if ($err && !$retry && $this->canDestroyWorkingCopy($path)) { + if ($err && !$retry && $repository->canDestroyWorkingCopy()) { $retry = true; // Fix remote origin url if it doesn't match our configuration $origin_url = $repository->execLocalCommand( 'config --get remote.origin.url'); - $remote_uri = $repository->getDetail('remote-uri'); - if ($origin_url != $remote_uri) { + $remote_uri = $repository->getRemoteURIEnvelope(); + if ($origin_url != $remote_uri->openEnvelope()) { $repository->execLocalCommand( - 'remote set-url origin %s', + 'remote set-url origin %P', $remote_uri); } } else if ($err) { @@ -279,6 +321,23 @@ final class PhabricatorRepositoryPullEngine } + /** + * @task git + */ + private function installGitHook() { + $repository = $this->getRepository(); + $root = $repository->getLocalPath(); + + if ($repository->isWorkingCopyBare()) { + $path = '/hooks/pre-receive'; + } else { + $path = '/.git/hooks/pre-receive'; + } + + $this->installHook($root.$path); + } + + /* -( Pulling Mercurial Working Copies )----------------------------------- */ @@ -296,8 +355,8 @@ final class PhabricatorRepositoryPullEngine $path); } else { $repository->execxRemoteCommand( - 'clone -- %s %s', - $repository->getRemoteURI(), + 'clone --noupdate -- %P %s', + $repository->getRemoteURIEnvelope(), $path); } } @@ -344,6 +403,43 @@ final class PhabricatorRepositoryPullEngine } + /** + * @task hg + */ + private function installMercurialHook() { + $repository = $this->getRepository(); + $path = $repository->getLocalPath().'/.hg/hgrc'; + + $root = dirname(phutil_get_library_root('phabricator')); + $bin = $root.'/bin/commit-hook'; + + $data = array(); + $data[] = '[hooks]'; + + // This hook handles normal pushes. + $data[] = csprintf( + 'pretxnchangegroup.phabricator = %s %s %s', + $bin, + $repository->getCallsign(), + 'pretxnchangegroup'); + + // This one handles creating bookmarks. + $data[] = csprintf( + 'prepushkey.phabricator = %s %s %s', + $bin, + $repository->getCallsign(), + 'prepushkey'); + + $data[] = null; + + $data = implode("\n", $data); + + $this->log('%s', pht('Installing commit hook config to "%s"...', $path)); + + Filesystem::writeFile($path, $data); + } + + /* -( Pulling Subversion Working Copies )---------------------------------- */ @@ -358,13 +454,17 @@ final class PhabricatorRepositoryPullEngine } -/* -( Internals )---------------------------------------------------------- */ + /** + * @task svn + */ + private function installSubversionHook() { + $repository = $this->getRepository(); + $root = $repository->getLocalPath(); + $path = '/hooks/pre-commit'; - private function canDestroyWorkingCopy($path) { - $default_path = PhabricatorEnv::getEnvConfig( - 'repository.default-local-path'); - return Filesystem::isDescendant($path, $default_path); + $this->installHook($root.$path); } + } diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php new file mode 100644 index 0000000000..2b0eb4846e --- /dev/null +++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php @@ -0,0 +1,382 @@ +newRefs = array(); + $this->deadRefs = array(); + $this->closeCommits = array(); + + $repository = $this->getRepository(); + + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // No meaningful refs of any type in Subversion. + $branches = array(); + $bookmarks = array(); + $tags = array(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $branches = $this->loadMercurialBranchPositions($repository); + $bookmarks = $this->loadMercurialBookmarkPositions($repository); + $tags = array(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $branches = $this->loadGitBranchPositions($repository); + $bookmarks = array(); + $tags = $this->loadGitTagPositions($repository); + break; + default: + throw new Exception(pht('Unknown VCS "%s"!', $vcs)); + } + + $maps = array( + PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches, + PhabricatorRepositoryRefCursor::TYPE_TAG => $tags, + PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks, + ); + + $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->execute(); + $cursor_groups = mgroup($all_cursors, 'getRefType'); + + $this->hasNoCursors = (!$all_cursors); + + // Find all the heads of closing refs. + $all_closing_heads = array(); + foreach ($all_cursors as $cursor) { + if ($this->shouldCloseRef($cursor->getRefType(), $cursor->getRefName())) { + $all_closing_heads[] = $cursor->getCommitIdentifier(); + } + } + $all_closing_heads = array_unique($all_closing_heads); + + foreach ($maps as $type => $refs) { + $cursor_group = idx($cursor_groups, $type, array()); + $this->updateCursors($cursor_group, $refs, $type, $all_closing_heads); + } + + if ($this->closeCommits) { + $this->setCloseFlagOnCommits($this->closeCommits); + } + + if ($this->newRefs || $this->deadRefs) { + $repository->openTransaction(); + foreach ($this->newRefs as $ref) { + $ref->save(); + } + foreach ($this->deadRefs as $ref) { + $ref->delete(); + } + $repository->saveTransaction(); + + $this->newRefs = array(); + $this->deadRefs = array(); + } + } + + private function markRefNew(PhabricatorRepositoryRefCursor $cursor) { + $this->newRefs[] = $cursor; + return $this; + } + + private function markRefDead(PhabricatorRepositoryRefCursor $cursor) { + $this->deadRefs[] = $cursor; + return $this; + } + + private function markCloseCommits(array $identifiers) { + foreach ($identifiers as $identifier) { + $this->closeCommits[$identifier] = $identifier; + } + return $this; + } + + private function updateCursors( + array $cursors, + array $new_refs, + $ref_type, + array $all_closing_heads) { + $repository = $this->getRepository(); + + // NOTE: Mercurial branches may have multiple branch heads; this logic + // is complex primarily to account for that. + + // Group all the cursors by their ref name, like "master". Since Mercurial + // branches may have multiple heads, there could be several cursors with + // the same name. + $cursor_groups = mgroup($cursors, 'getRefNameRaw'); + + // Group all the new ref values by their name. As above, these groups may + // have multiple members in Mercurial. + $ref_groups = mgroup($new_refs, 'getShortName'); + + foreach ($ref_groups as $name => $refs) { + $new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier'); + + $ref_cursors = idx($cursor_groups, $name, array()); + $old_commits = mpull($ref_cursors, null, 'getCommitIdentifier'); + + // We're going to delete all the cursors pointing at commits which are + // no longer associated with the refs. This primarily makes the Mercurial + // multiple head case easier, and means that when we update a ref we + // delete the old one and write a new one. + foreach ($ref_cursors as $cursor) { + if (isset($new_commits[$cursor->getCommitIdentifier()])) { + // This ref previously pointed at this commit, and still does. + $this->log( + pht( + 'Ref %s "%s" still points at %s.', + $ref_type, + $name, + $cursor->getCommitIdentifier())); + } else { + // This ref previously pointed at this commit, but no longer does. + $this->log( + pht( + 'Ref %s "%s" no longer points at %s.', + $ref_type, + $name, + $cursor->getCommitIdentifier())); + + // Nuke the obsolete cursor. + $this->markRefDead($cursor); + } + } + + // Now, we're going to insert new cursors for all the commits which are + // associated with this ref that don't currently have cursors. + $added_commits = array_diff_key($new_commits, $old_commits); + foreach ($added_commits as $identifier) { + $this->log( + pht( + 'Ref %s "%s" now points at %s.', + $ref_type, + $name, + $identifier)); + $this->markRefNew( + id(new PhabricatorRepositoryRefCursor()) + ->setRepositoryPHID($repository->getPHID()) + ->setRefType($ref_type) + ->setRefName($name) + ->setCommitIdentifier($identifier)); + } + + if ($this->shouldCloseRef($ref_type, $name)) { + foreach ($added_commits as $identifier) { + $new_identifiers = $this->loadNewCommitIdentifiers( + $identifier, + $all_closing_heads); + + $this->markCloseCommits($new_identifiers); + } + } + } + + // Find any cursors for refs which no longer exist. This happens when a + // branch, tag or bookmark is deleted. + + foreach ($cursor_groups as $name => $cursor_group) { + if (idx($ref_groups, $name) === null) { + $this->log( + pht( + 'Ref %s "%s" no longer exists.', + $cursor->getRefType(), + $cursor->getRefName())); + foreach ($cursor_group as $cursor) { + $this->markRefDead($cursor); + } + } + } + } + + private function shouldCloseRef($ref_type, $ref_name) { + if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) { + return false; + } + + if ($this->hasNoCursors) { + // If we don't have any cursors, don't close things. Particularly, this + // corresponds to the case where you've just updated to this code on an + // existing repository: we don't want to requeue message steps for every + // commit on a closeable ref. + return false; + } + + return $this->getRepository()->shouldAutocloseBranch($ref_name); + } + + /** + * Find all ancestors of a new closing branch head which are not ancestors + * of any old closing branch head. + */ + private function loadNewCommitIdentifiers( + $new_head, + array $all_closing_heads) { + + $repository = $this->getRepository(); + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + if ($all_closing_heads) { + $escheads = array(); + foreach ($all_closing_heads as $head) { + $escheads[] = hgsprintf('%s', $head); + } + $escheads = implode(' or ', $escheads); + list($stdout) = $this->getRepository()->execxLocalCommand( + 'log --template %s --rev %s', + '{node}\n', + hgsprintf('%s', $new_head).' - ('.$escheads.')'); + } else { + list($stdout) = $this->getRepository()->execxLocalCommand( + 'log --template %s --rev %s', + '{node}\n', + hgsprintf('%s', $new_head)); + } + return phutil_split_lines($stdout, $retain_newlines = false); + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + if ($all_closing_heads) { + list($stdout) = $this->getRepository()->execxLocalCommand( + 'log --format=%s %s --not %Ls', + '%H', + $new_head, + $all_closing_heads); + } else { + list($stdout) = $this->getRepository()->execxLocalCommand( + 'log --format=%s %s', + '%H', + $new_head); + } + return phutil_split_lines($stdout, $retain_newlines = false); + default: + throw new Exception(pht('Unsupported VCS "%s"!', $vcs)); + } + } + + /** + * Mark a list of commits as closeable, and queue workers for those commits + * which don't already have the flag. + */ + private function setCloseFlagOnCommits(array $identifiers) { + $repository = $this->getRepository(); + $commit_table = new PhabricatorRepositoryCommit(); + $conn_w = $commit_table->establishConnection('w'); + + $vcs = $repository->getVersionControlSystem(); + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; + break; + default: + throw new Exception("Unknown repository type '{$vcs}'!"); + } + + $all_commits = queryfx_all( + $conn_w, + 'SELECT id, commitIdentifier, importStatus FROM %T + WHERE commitIdentifier IN (%Ls)', + $commit_table->getTableName(), + $identifiers); + + $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; + + $all_commits = ipull($all_commits, null, 'commitIdentifier'); + foreach ($identifiers as $identifier) { + $row = idx($all_commits, $identifier); + + if (!$row) { + throw new Exception( + pht( + 'Commit "%s" has not been discovered yet! Run discovery before '. + 'updating refs.', + $identifier)); + } + + if (!($row['importStatus'] & $closeable_flag)) { + queryfx( + $conn_w, + 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', + $commit_table->getTableName(), + $closeable_flag, + $row['id']); + + $data = array( + 'commitID' => $row['id'], + 'only' => true, + ); + + PhabricatorWorker::scheduleTask($class, $data); + } + } + } + + +/* -( Updating Git Refs )-------------------------------------------------- */ + + + /** + * @task git + */ + private function loadGitBranchPositions(PhabricatorRepository $repository) { + return id(new DiffusionLowLevelGitRefQuery()) + ->setRepository($repository) + ->withIsOriginBranch(true) + ->execute(); + } + + + /** + * @task git + */ + private function loadGitTagPositions(PhabricatorRepository $repository) { + return id(new DiffusionLowLevelGitRefQuery()) + ->setRepository($repository) + ->withIsTag(true) + ->execute(); + } + + +/* -( Updating Mercurial Refs )-------------------------------------------- */ + + + /** + * @task hg + */ + private function loadMercurialBranchPositions( + PhabricatorRepository $repository) { + return id(new DiffusionLowLevelMercurialBranchesQuery()) + ->setRepository($repository) + ->execute(); + } + + + /** + * @task hg + */ + private function loadMercurialBookmarkPositions( + PhabricatorRepository $repository) { + // TODO: Implement support for Mercurial bookmarks. + return array(); + } + +} diff --git a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php index 03a4708699..b46fb4188b 100644 --- a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php +++ b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php @@ -4,12 +4,7 @@ final class PhabricatorWorkingCopyDiscoveryTestCase extends PhabricatorWorkingCopyTestCase { public function testSubversionCommitDiscovery() { - $repo = $this->buildPulledRepository('ST'); - - $engine = id(new PhabricatorRepositoryDiscoveryEngine()) - ->setRepository($repo); - - $refs = $engine->discoverCommits($repo); + $refs = $this->discoverRefs('ST'); $this->assertEqual( array( 1368319433, @@ -17,102 +12,41 @@ final class PhabricatorWorkingCopyDiscoveryTestCase ), mpull($refs, 'getEpoch'), 'Commit Epochs'); + } + + public function testMercurialCommitDiscovery() { + $refs = $this->discoverRefs('HT'); + $this->assertEqual( + array( + '4a110ae879f473f2e82ffd032475caedd6cdba91', + ), + mpull($refs, 'getIdentifier')); + } + + public function testGitCommitDiscovery() { + $refs = $this->discoverRefs('GT'); + $this->assertEqual( + array( + '763d4ab372445551c95fb5cccd1a7a223f5b2ac8', + ), + mpull($refs, 'getIdentifier')); + } + + private function discoverRefs($callsign) { + $repo = $this->buildPulledRepository($callsign); + + $engine = id(new PhabricatorRepositoryDiscoveryEngine()) + ->setRepository($repo); + + $refs = $engine->discoverCommits($repo); // The next time through, these should be cached as already discovered. - $refs = $engine->discoverCommits($repo); - $this->assertEqual(array(), $refs); + $new_refs = $engine->discoverCommits($repo); + $this->assertEqual(array(), $new_refs); + + return $refs; } - public function testExecuteGitVerifySameOrigin() { - $cases = array( - array( - 'ssh://user@domain.com/path.git', - 'ssh://user@domain.com/path.git', - true, - 'Identical paths should pass.', - ), - array( - 'ssh://user@domain.com/path.git', - 'https://user@domain.com/path.git', - true, - 'Protocol changes should pass.', - ), - array( - 'ssh://user@domain.com/path.git', - 'git@domain.com:path.git', - true, - 'Git implicit SSH should pass.', - ), - array( - 'ssh://user@gitserv001.com/path.git', - 'ssh://user@gitserv002.com/path.git', - true, - 'Domain changes should pass.', - ), - array( - 'ssh://alincoln@domain.com/path.git', - 'ssh://htaft@domain.com/path.git', - true, - 'User/auth changes should pass.', - ), - array( - 'ssh://user@domain.com/apples.git', - 'ssh://user@domain.com/bananas.git', - false, - 'Path changes should fail.', - ), - array( - 'ssh://user@domain.com/apples.git', - 'git@domain.com:bananas.git', - false, - 'Git implicit SSH path changes should fail.', - ), - array( - 'user@domain.com:path/repo.git', - 'user@domain.com:path/repo', - true, - 'Optional .git extension should not prevent matches.', - ), - array( - 'user@domain.com:path/repo/', - 'user@domain.com:path/repo', - true, - 'Optional trailing slash should not prevent matches.', - ), - array( - 'file:///path/to/local/repo.git', - 'file:///path/to/local/repo.git', - true, - 'file:// protocol should be supported.', - ), - array( - '/path/to/local/repo.git', - 'file:///path/to/local/repo.git', - true, - 'Implicit file:// protocol should be recognized.', - ), - ); - - foreach ($cases as $case) { - list($remote, $config, $expect, $message) = $case; - - $ex = null; - try { - PhabricatorRepositoryPullLocalDaemon::executeGitverifySameOrigin( - $remote, - $config, - '(a test case)'); - } catch (Exception $exception) { - $ex = $exception; - } - - $this->assertEqual( - $expect, - !$ex, - "Verification that '{$remote}' and '{$config}' are the same origin ". - "had a different outcome than expected: {$message}"); - } - } } diff --git a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php index 336a8fe89d..40412e45d0 100644 --- a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php +++ b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php @@ -11,6 +11,14 @@ abstract class PhabricatorWorkingCopyTestCase extends PhabricatorTestCase { } protected function buildBareRepository($callsign) { + $existing_repository = id(new PhabricatorRepositoryQuery()) + ->withCallsigns(array($callsign)) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->executeOne(); + if ($existing_repository) { + $existing_repository->delete(); + } + $data_dir = dirname(__FILE__).'/data/'; $types = array( @@ -76,9 +84,18 @@ abstract class PhabricatorWorkingCopyTestCase extends PhabricatorTestCase { ->setRepository($repository) ->pullRepository(); - $this->pulled[$callsign] = true; + return $repository; + } + + protected function buildDiscoveredRepository($callsign) { + $repository = $this->buildPulledRepository($callsign); + + id(new PhabricatorRepositoryDiscoveryEngine()) + ->setRepository($repository) + ->discoverCommits(); return $repository; } + } diff --git a/src/applications/repository/engine/__tests__/data/CHA.git.tgz b/src/applications/repository/engine/__tests__/data/CHA.git.tgz new file mode 100644 index 0000000000..7b625e6f7f Binary files /dev/null and b/src/applications/repository/engine/__tests__/data/CHA.git.tgz differ diff --git a/src/applications/repository/engine/__tests__/data/CHB.hg.tgz b/src/applications/repository/engine/__tests__/data/CHB.hg.tgz new file mode 100644 index 0000000000..f51e36812f Binary files /dev/null and b/src/applications/repository/engine/__tests__/data/CHB.hg.tgz differ diff --git a/src/applications/repository/engine/__tests__/data/CHC.svn.tgz b/src/applications/repository/engine/__tests__/data/CHC.svn.tgz new file mode 100644 index 0000000000..e12a64c97f Binary files /dev/null and b/src/applications/repository/engine/__tests__/data/CHC.svn.tgz differ diff --git a/src/applications/repository/engine/__tests__/data/CHD.svn.tgz b/src/applications/repository/engine/__tests__/data/CHD.svn.tgz new file mode 100644 index 0000000000..0a4d0a5700 Binary files /dev/null and b/src/applications/repository/engine/__tests__/data/CHD.svn.tgz differ diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php index 111bd6e100..dd97df8d55 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php @@ -14,6 +14,10 @@ final class PhabricatorRepositoryManagementDeleteWorkflow 'name' => 'verbose', 'help' => 'Show additional debugging information.', ), + array( + 'name' => 'force', + 'help' => 'Do not prompt for confirmation.', + ), array( 'name' => 'repos', 'wildcard' => true, @@ -30,9 +34,25 @@ final class PhabricatorRepositoryManagementDeleteWorkflow } $console = PhutilConsole::getConsole(); + + if (!$args->getArg('force')) { + $console->writeOut("%s\n\n", pht('These repositories will be deleted:')); + + foreach ($repos as $repo) { + $console->writeOut( + " %s %s\n", + 'r'.$repo->getCallsign(), + $repo->getName()); + } + + $prompt = pht('Permanently delete these repositories?'); + if (!$console->confirm($prompt)) { + return 1; + } + } + foreach ($repos as $repo) { $console->writeOut("Deleting '%s'...\n", $repo->getCallsign()); - $repo->delete(); } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php index 8a6067dadd..6fac505543 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php @@ -38,10 +38,11 @@ final class PhabricatorRepositoryManagementDiscoverWorkflow foreach ($repos as $repo) { $console->writeOut("Discovering '%s'...\n", $repo->getCallsign()); - $daemon = new PhabricatorRepositoryPullLocalDaemon(array()); - $daemon->setVerbose($args->getArg('verbose')); - $daemon->setRepair($args->getArg('repair')); - $daemon->discoverRepository($repo); + id(new PhabricatorRepositoryDiscoveryEngine()) + ->setRepository($repo) + ->setVerbose($args->getArg('verbose')) + ->setRepairMode($args->getArg('repair')) + ->discoverCommits(); } $console->writeOut("Done.\n"); diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php new file mode 100644 index 0000000000..12eec7eec8 --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php @@ -0,0 +1,96 @@ +setName('edit') + ->setExamples('**edit** --as __username__ __repository__ ...') + ->setSynopsis('Edit __repository__, named by callsign.') + ->setArguments( + array( + array( + 'name' => 'repos', + 'wildcard' => true, + ), + array( + 'name' => 'as', + 'param' => 'user', + 'help' => 'Edit as user.', + ), + array( + 'name' => 'local-path', + 'param' => 'path', + 'help' => 'Edit the local path.', + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $repos = $this->loadRepositories($args, 'repos'); + + if (!$repos) { + throw new PhutilArgumentUsageException( + "Specify one or more repositories to edit, by callsign."); + } + + $console = PhutilConsole::getConsole(); + + // TODO: It would be nice to just take this action as "Administrator" or + // similar, since that would make it easier to use this script, harder to + // impersonate users, and more clear to viewers what happened. However, + // the omnipotent user doesn't have a PHID right now, can't be loaded, + // doesn't have a handle, etc. Adding all of that is fairly involved, and + // I want to wait for stronger use cases first. + + $username = $args->getArg('as'); + if (!$username) { + throw new PhutilArgumentUsageException( + pht("Specify a user to edit as with --as .")); + } + + $actor = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withUsernames(array($username)) + ->executeOne(); + + if (!$actor) { + throw new PhutilArgumentUsageException( + pht("No such user '%s'!", $username)); + } + + foreach ($repos as $repo) { + $console->writeOut("Editing '%s'...\n", $repo->getCallsign()); + + $xactions = array(); + + $type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH; + + if ($args->getArg('local-path')) { + $xactions[] = id(new PhabricatorRepositoryTransaction()) + ->setTransactionType($type_local_path) + ->setNewValue($args->getArg('local-path')); + } + + if (!$xactions) { + throw new PhutilArgumentUsageException( + pht("Specify one or more fields to edit!")); + } + + $content_source = PhabricatorContentSource::newConsoleSource(); + + $editor = id(new PhabricatorRepositoryEditor()) + ->setActor($actor) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($repo, $xactions); + } + + $console->writeOut("Done.\n"); + + return 0; + } + +} diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php index dab8252ae6..7892ea5b59 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php @@ -40,9 +40,10 @@ final class PhabricatorRepositoryManagementImportingWorkflow $rows = queryfx_all( $conn_r, 'SELECT repositoryID, commitIdentifier, importStatus FROM %T - WHERE repositoryID IN (%Ld) AND importStatus != %d', + WHERE repositoryID IN (%Ld) AND (importStatus & %d) != %d', $table->getTableName(), array_keys($repos), + PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); $console = PhutilConsole::getConsole(); diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php index 6ea6992f24..0ee1adbce9 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php @@ -14,7 +14,7 @@ final class PhabricatorRepositoryManagementListWorkflow $console = PhutilConsole::getConsole(); $repos = id(new PhabricatorRepositoryQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->execute(); if ($repos) { foreach ($repos as $repo) { diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php new file mode 100644 index 0000000000..82fb82b3e0 --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php @@ -0,0 +1,97 @@ +setName('lookup-users') + ->setExamples('**lookup-users** __commit__ ...') + ->setSynopsis('Resolve user accounts for users attached to __commit__.') + ->setArguments( + array( + array( + 'name' => 'commits', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $commits = $this->loadCommits($args, 'commits'); + if (!$commits) { + throw new PhutilArgumentUsageException( + "Specify one or more commits to resolve users for."); + } + + $console = PhutilConsole::getConsole(); + foreach ($commits as $commit) { + $repo = $commit->getRepository(); + $name = $repo->formatCommitName($commit->getCommitIdentifier()); + + $console->writeOut( + "%s\n", + pht("Examining commit %s...", $name)); + + $ref = id(new DiffusionLowLevelCommitQuery()) + ->setRepository($repo) + ->withIdentifier($commit->getCommitIdentifier()) + ->execute(); + + $author = $ref->getAuthor(); + $console->writeOut( + "%s\n", + pht('Raw author string: %s', coalesce($author, 'null'))); + + if ($author !== null) { + $handle = $this->resolveUser($commit, $author); + if ($handle) { + $console->writeOut( + "%s\n", + pht('Phabricator user: %s', $handle->getFullName())); + } else { + $console->writeOut( + "%s\n", + pht('Unable to resolve a corresponding Phabricator user.')); + } + } + + $committer = $ref->getCommitter(); + $console->writeOut( + "%s\n", + pht('Raw committer string: %s', coalesce($committer, 'null'))); + + if ($committer !== null) { + $handle = $this->resolveUser($commit, $committer); + if ($handle) { + $console->writeOut( + "%s\n", + pht('Phabricator user: %s', $handle->getFullName())); + } else { + $console->writeOut( + "%s\n", + pht('Unable to resolve a corresponding Phabricator user.')); + } + } + } + + return 0; + } + + private function resolveUser(PhabricatorRepositoryCommit $commit, $name) { + $phid = id(new DiffusionResolveUserQuery()) + ->withCommit($commit) + ->withName($name) + ->execute(); + + if (!$phid) { + return null; + } + + return id(new PhabricatorHandleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($phid)) + ->executeOne(); + } + +} diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php new file mode 100644 index 0000000000..16c72effed --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php @@ -0,0 +1,52 @@ +setName('mirror') + ->setExamples('**mirror** [__options__] __repository__ ...') + ->setSynopsis( + pht('Push __repository__, named by callsign, to mirrors.')) + ->setArguments( + array( + array( + 'name' => 'verbose', + 'help' => pht('Show additional debugging information.'), + ), + array( + 'name' => 'repos', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $repos = $this->loadRepositories($args, 'repos'); + + if (!$repos) { + throw new PhutilArgumentUsageException( + pht( + "Specify one or more repositories to push to mirrors, by ". + "callsign.")); + } + + $console = PhutilConsole::getConsole(); + foreach ($repos as $repo) { + $console->writeOut( + "%s\n", + pht('Pushing "%s" to mirrors...', $repo->getCallsign())); + + $engine = id(new PhabricatorRepositoryMirrorEngine()) + ->setRepository($repo) + ->setVerbose($args->getArg('verbose')) + ->pushToMirrors(); + } + + $console->writeOut("Done.\n"); + + return 0; + } + +} diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php new file mode 100644 index 0000000000..380a5d153e --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php @@ -0,0 +1,49 @@ +setName('refs') + ->setExamples('**refs** [__options__] __repository__ ...') + ->setSynopsis('Update refs in __repository__, named by callsign.') + ->setArguments( + array( + array( + 'name' => 'verbose', + 'help' => 'Show additional debugging information.', + ), + array( + 'name' => 'repos', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $repos = $this->loadRepositories($args, 'repos'); + + if (!$repos) { + throw new PhutilArgumentUsageException( + pht( + "Specify one or more repositories to update refs for, ". + "by callsign.")); + } + + $console = PhutilConsole::getConsole(); + foreach ($repos as $repo) { + $console->writeOut("Updating refs in '%s'...\n", $repo->getCallsign()); + + $engine = id(new PhabricatorRepositoryRefEngine()) + ->setRepository($repo) + ->setVerbose($args->getArg('verbose')) + ->updateRefs(); + } + + $console->writeOut("Done.\n"); + + return 0; + } + +} diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php index e6192906ff..218b7e36bc 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php @@ -1,11 +1,7 @@ getArg($param); @@ -15,7 +11,7 @@ abstract class PhabricatorRepositoryManagementWorkflow } $repos = id(new PhabricatorRepositoryQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withCallsigns($callsigns) ->execute(); @@ -30,5 +26,27 @@ abstract class PhabricatorRepositoryManagementWorkflow return $repos; } + protected function loadCommits(PhutilArgumentParser $args, $param) { + $names = $args->getArg($param); + if (!$names) { + return null; + } + + $query = id(new DiffusionCommitQuery()) + ->setViewer($this->getViewer()) + ->withIdentifiers($names); + + $query->execute(); + $map = $query->getIdentifierMap(); + + foreach ($names as $name) { + if (empty($map[$name])) { + throw new PhutilArgumentUsageException( + pht('Commit "%s" does not exist or is ambiguous.', $name)); + } + } + + return $map; + } } diff --git a/src/applications/repository/phid/PhabricatorRepositoryPHIDTypeMirror.php b/src/applications/repository/phid/PhabricatorRepositoryPHIDTypeMirror.php new file mode 100644 index 0000000000..4cae9981c6 --- /dev/null +++ b/src/applications/repository/phid/PhabricatorRepositoryPHIDTypeMirror.php @@ -0,0 +1,42 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $mirror = $objects[$phid]; + + $handle->setName( + pht('Mirror %d %s', $mirror->getID(), $mirror->getRemoteURI())); + $handle->setURI("/diffusion/mirror/".$mirror->getID()."/"); + } + } + +} diff --git a/src/applications/repository/phid/PhabricatorRepositoryPHIDTypePushLog.php b/src/applications/repository/phid/PhabricatorRepositoryPHIDTypePushLog.php new file mode 100644 index 0000000000..a2e5e57133 --- /dev/null +++ b/src/applications/repository/phid/PhabricatorRepositoryPHIDTypePushLog.php @@ -0,0 +1,40 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $log = $objects[$phid]; + + $handle->setName(pht('Push Log %d', $log->getID())); + } + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryMirrorQuery.php b/src/applications/repository/query/PhabricatorRepositoryMirrorQuery.php new file mode 100644 index 0000000000..2f8f7fc96e --- /dev/null +++ b/src/applications/repository/query/PhabricatorRepositoryMirrorQuery.php @@ -0,0 +1,101 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withRepositoryPHIDs(array $repository_phids) { + $this->repositoryPHIDs = $repository_phids; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorRepositoryMirror(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + public function willFilterPage(array $mirrors) { + assert_instances_of($mirrors, 'PhabricatorRepositoryMirror'); + + $repository_phids = mpull($mirrors, 'getRepositoryPHID'); + if ($repository_phids) { + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + } else { + $repositories = array(); + } + + foreach ($mirrors as $key => $mirror) { + $phid = $mirror->getRepositoryPHID(); + if (empty($repositories[$phid])) { + unset($mirrors[$key]); + continue; + } + $mirror->attachRepository($repositories[$phid]); + } + + return $mirrors; + } + + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->repositoryPHIDs) { + $where[] = qsprintf( + $conn_r, + 'repositoryPHID IN (%Ls)', + $this->repositoryPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationDiffusion'; + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php new file mode 100644 index 0000000000..b259b59adc --- /dev/null +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php @@ -0,0 +1,138 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withRepositoryPHIDs(array $repository_phids) { + $this->repositoryPHIDs = $repository_phids; + return $this; + } + + public function withPusherPHIDs(array $pusher_phids) { + $this->pusherPHIDs = $pusher_phids; + return $this; + } + + public function withRefTypes(array $ref_types) { + $this->refTypes = $ref_types; + return $this; + } + + public function withNewRefs(array $new_refs) { + $this->newRefs = $new_refs; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorRepositoryPushLog(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + public function willFilterPage(array $logs) { + $repository_phids = mpull($logs, 'getRepositoryPHID'); + if ($repository_phids) { + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + } else { + $repositories = array(); + } + + foreach ($logs as $key => $log) { + $phid = $log->getRepositoryPHID(); + if (empty($repositories[$phid])) { + unset($logs[$key]); + continue; + } + $log->attachRepository($repositories[$phid]); + } + + return $logs; + } + + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->repositoryPHIDs) { + $where[] = qsprintf( + $conn_r, + 'repositoryPHID IN (%Ls)', + $this->repositoryPHIDs); + } + + if ($this->pusherPHIDs) { + $where[] = qsprintf( + $conn_r, + 'pusherPHID in (%Ls)', + $this->pusherPHIDs); + } + + if ($this->refTypes) { + $where[] = qsprintf( + $conn_r, + 'refType IN (%Ls)', + $this->refTypes); + } + + if ($this->newRefs) { + $where[] = qsprintf( + $conn_r, + 'refNew IN (%Ls)', + $this->newRefs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationDiffusion'; + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php new file mode 100644 index 0000000000..938ef48d12 --- /dev/null +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php @@ -0,0 +1,106 @@ +setParameter( + 'repositoryPHIDs', + $this->readPHIDsFromRequest( + $request, + 'repositories', + array( + PhabricatorRepositoryPHIDTypeRepository::TYPECONST, + ))); + + $saved->setParameter( + 'pusherPHIDs', + $this->readUsersFromRequest( + $request, + 'pushers')); + + return $saved; + } + + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $query = id(new PhabricatorRepositoryPushLogQuery()); + + $repository_phids = $saved->getParameter('repositoryPHIDs'); + if ($repository_phids) { + $query->withRepositoryPHIDs($repository_phids); + } + + $pusher_phids = $saved->getParameter('pusherPHIDs'); + if ($pusher_phids) { + $query->withPusherPHIDs($pusher_phids); + } + + return $query; + } + + public function buildSearchForm( + AphrontFormView $form, + PhabricatorSavedQuery $saved_query) { + + $repository_phids = $saved_query->getParameter('repositoryPHIDs', array()); + $pusher_phids = $saved_query->getParameter('pusherPHIDs', array()); + + $all_phids = array_merge( + $repository_phids, + $pusher_phids); + + if ($all_phids) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->requireViewer()) + ->withPHIDs($all_phids) + ->execute(); + } else { + $handles = array(); + } + + $repository_handles = array_select_keys($handles, $repository_phids); + $pusher_handles = array_select_keys($handles, $pusher_phids); + + $form + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/repositories/') + ->setName('repositories') + ->setLabel(pht('Repositories')) + ->setValue($repository_handles)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/accounts/') + ->setName('pushers') + ->setLabel(pht('Pushers')) + ->setValue($pusher_handles)); + } + + protected function getURI($path) { + return '/diffusion/pushlog/'.$path; + } + + public function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Push Logs'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 92d8b95adf..23d2911dc1 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -8,6 +8,9 @@ final class PhabricatorRepositoryQuery private $callsigns; private $types; private $uuids; + private $nameContains; + private $remoteURIs; + private $anyProjectPHIDs; const STATUS_OPEN = 'status-open'; const STATUS_CLOSED = 'status-closed'; @@ -20,8 +23,14 @@ final class PhabricatorRepositoryQuery const ORDER_NAME = 'order-name'; private $order = self::ORDER_CREATED; + const HOSTED_PHABRICATOR = 'hosted-phab'; + const HOSTED_REMOTE = 'hosted-remote'; + const HOSTED_ALL = 'hosted-all'; + private $hosted = self::HOSTED_ALL; + private $needMostRecentCommits; private $needCommitCounts; + private $needProjectPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -43,6 +52,11 @@ final class PhabricatorRepositoryQuery return $this; } + public function withHosted($hosted) { + $this->hosted = $hosted; + return $this; + } + public function withTypes(array $types) { $this->types = $types; return $this; @@ -53,6 +67,21 @@ final class PhabricatorRepositoryQuery return $this; } + public function withNameContains($contains) { + $this->nameContains = $contains; + return $this; + } + + public function withRemoteURIs(array $uris) { + $this->remoteURIs = $uris; + return $this; + } + + public function withAnyProjects(array $projects) { + $this->anyProjectPHIDs = $projects; + return $this; + } + public function needCommitCounts($need_counts) { $this->needCommitCounts = $need_counts; return $this; @@ -63,11 +92,17 @@ final class PhabricatorRepositoryQuery return $this; } + public function needProjectPHIDs($need_phids) { + $this->needProjectPHIDs = $need_phids; + return $this; + } + public function setOrder($order) { $this->order = $order; return $this; } + protected function loadPage() { $table = new PhabricatorRepository(); $conn_r = $table->establishConnection('r'); @@ -110,7 +145,6 @@ final class PhabricatorRepositoryQuery } } - return $repositories; } @@ -137,6 +171,57 @@ final class PhabricatorRepositoryQuery default: throw new Exception("Unknown status '{$status}'!"); } + + // TODO: This should also be denormalized. + $hosted = $this->hosted; + switch ($hosted) { + case self::HOSTED_PHABRICATOR: + if (!$repo->isHosted()) { + unset($repositories[$key]); + } + break; + case self::HOSTED_REMOTE: + if ($repo->isHosted()) { + unset($repositories[$key]); + } + break; + case self::HOSTED_ALL: + break; + default: + throw new Exception("Uknown hosted failed '${hosted}'!"); + } + } + + // TODO: Denormalize this, too. + if ($this->remoteURIs) { + $try_uris = $this->getNormalizedPaths(); + $try_uris = array_fuse($try_uris); + foreach ($repositories as $key => $repository) { + if (!isset($try_uris[$repository->getNormalizedPath()])) { + unset($repositories[$key]); + } + } + } + + return $repositories; + } + + public function didFilterPage(array $repositories) { + if ($this->needProjectPHIDs) { + $type_project = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT; + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(mpull($repositories, 'getPHID')) + ->withEdgeTypes(array($type_project)); + $edge_query->execute(); + + foreach ($repositories as $repository) { + $project_phids = $edge_query->getDestinationPHIDs( + array( + $repository->getPHID(), + )); + $repository->attachProjectPHIDs($project_phids); + } } return $repositories; @@ -271,6 +356,12 @@ final class PhabricatorRepositoryQuery PhabricatorRepository::TABLE_SUMMARY); } + if ($this->anyProjectPHIDs) { + $joins[] = qsprintf( + $conn_r, + 'JOIN edge e ON e.src = r.phid'); + } + return implode(' ', $joins); } @@ -312,6 +403,20 @@ final class PhabricatorRepositoryQuery $this->uuids); } + if (strlen($this->nameContains)) { + $where[] = qsprintf( + $conn_r, + 'name LIKE %~', + $this->nameContains); + } + + if ($this->anyProjectPHIDs) { + $where[] = qsprintf( + $conn_r, + 'e.dst IN (%Ls)', + $this->anyProjectPHIDs); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); @@ -322,4 +427,28 @@ final class PhabricatorRepositoryQuery return 'PhabricatorApplicationDiffusion'; } + private function getNormalizedPaths() { + $normalized_uris = array(); + + // Since we don't know which type of repository this URI is in the general + // case, just generate all the normalizations. We could refine this in some + // cases: if the query specifies VCS types, or the URI is a git-style URI + // or an `svn+ssh` URI, we could deduce how to normalize it. However, this + // would be more complicated and it's not clear if it matters in practice. + + foreach ($this->remoteURIs as $uri) { + $normalized_uris[] = new PhabricatorRepositoryURINormalizer( + PhabricatorRepositoryURINormalizer::TYPE_GIT, + $uri); + $normalized_uris[] = new PhabricatorRepositoryURINormalizer( + PhabricatorRepositoryURINormalizer::TYPE_SVN, + $uri); + $normalized_uris[] = new PhabricatorRepositoryURINormalizer( + PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, + $uri); + } + + return array_unique(mpull($normalized_uris, 'getNormalizedPath')); + } + } diff --git a/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php b/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php new file mode 100644 index 0000000000..c7231bdc27 --- /dev/null +++ b/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php @@ -0,0 +1,83 @@ +repositoryPHIDs = $phids; + return $this; + } + + public function withRefTypes(array $types) { + $this->refTypes = $types; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorRepositoryRefCursor(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T r %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + public function willFilterPage(array $refs) { + $repository_phids = mpull($refs, 'getRepositoryPHID'); + + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + + foreach ($refs as $key => $ref) { + $repository = idx($repositories, $ref->getRepositoryPHID()); + if (!$repository) { + unset($refs[$key]); + continue; + } + $ref->attachRepository($repository); + } + + return $refs; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->repositoryPHIDs) { + $where[] = qsprintf( + $conn_r, + 'repositoryPHID IN (%Ls)', + $this->repositoryPHIDs); + } + + if ($this->refTypes) { + $where[] = qsprintf( + $conn_r, + 'refType IN (%Ls)', + $this->refTypes); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationDiffusion'; + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php index 88496dce2c..270e398c21 100644 --- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php @@ -9,13 +9,17 @@ final class PhabricatorRepositorySearchEngine $saved->setParameter('callsigns', $request->getStrList('callsigns')); $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('order', $request->getStr('order')); + $saved->setParameter('hosted', $request->getStr('hosted')); $saved->setParameter('types', $request->getArr('types')); + $saved->setParameter('name', $request->getStr('name')); + $saved->setParameter('anyProjectPHIDs', $request->getArr('anyProjects')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorRepositoryQuery()) + ->needProjectPHIDs(true) ->needCommitCounts(true) ->needMostRecentCommits(true); @@ -38,11 +42,27 @@ final class PhabricatorRepositorySearchEngine $query->setOrder(head($this->getOrderValues())); } + $hosted = $saved->getParameter('hosted'); + $hosted = idx($this->getHostedValues(), $hosted); + if ($hosted) { + $query->withHosted($hosted); + } + $types = $saved->getParameter('types'); if ($types) { $query->withTypes($types); } + $name = $saved->getParameter('name'); + if (strlen($name)) { + $query->withNameContains($name); + } + + $any_project_phids = $saved->getParameter('anyProjectPHIDs'); + if ($any_project_phids) { + $query->withAnyProjects($any_project_phids); + } + return $query; } @@ -53,6 +73,17 @@ final class PhabricatorRepositorySearchEngine $callsigns = $saved_query->getParameter('callsigns', array()); $types = $saved_query->getParameter('types', array()); $types = array_fuse($types); + $name = $saved_query->getParameter('name'); + $any_project_phids = $saved_query->getParameter('anyProjectPHIDs', array()); + + if ($any_project_phids) { + $any_project_handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->requireViewer()) + ->withPHIDs($any_project_phids) + ->execute(); + } else { + $any_project_handles = array(); + } $form ->appendChild( @@ -60,12 +91,29 @@ final class PhabricatorRepositorySearchEngine ->setName('callsigns') ->setLabel(pht('Callsigns')) ->setValue(implode(', ', $callsigns))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('name') + ->setLabel(pht('Name Contains')) + ->setValue($name)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/projects/') + ->setName('anyProjects') + ->setLabel(pht('In Any Project')) + ->setValue($any_project_handles)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('status') ->setLabel(pht('Status')) ->setValue($saved_query->getParameter('status')) - ->setOptions($this->getStatusOptions())); + ->setOptions($this->getStatusOptions())) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('hosted') + ->setLabel(pht('Hosted')) + ->setValue($saved_query->getParameter('hosted')) + ->setOptions($this->getHostedOptions())); $type_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Types')); @@ -151,5 +199,20 @@ final class PhabricatorRepositorySearchEngine ); } + private function getHostedOptions() { + return array( + '' => pht('Hosted and Remote Repositories'), + 'phabricator' => pht('Hosted Repositories'), + 'remote' => pht('Remote Repositories'), + ); + } + + private function getHostedValues() { + return array( + '' => PhabricatorRepositoryQuery::HOSTED_ALL, + 'phabricator' => PhabricatorRepositoryQuery::HOSTED_PHABRICATOR, + 'remote' => PhabricatorRepositoryQuery::HOSTED_REMOTE, + ); + } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 63e4e5bb57..be8a0d25e8 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -39,11 +39,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO protected $versionControlSystem; protected $details = array(); - - private $sshKeyfile; + protected $credentialPHID; private $commitCount = self::ATTACHABLE; private $mostRecentCommit = self::ATTACHABLE; + private $projectPHIDs = self::ATTACHABLE; public static function initializeNewRepository(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) @@ -77,17 +77,25 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO public function toDictionary() { return array( + 'id' => $this->getID(), 'name' => $this->getName(), 'phid' => $this->getPHID(), 'callsign' => $this->getCallsign(), + 'monogram' => $this->getMonogram(), 'vcs' => $this->getVersionControlSystem(), 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), - 'remoteURI' => (string)$this->getPublicRemoteURI(), - 'tracking' => $this->getDetail('tracking-enabled'), + 'remoteURI' => (string)$this->getRemoteURI(), 'description' => $this->getDetail('description'), + 'isActive' => $this->isTracked(), + 'isHosted' => $this->isHosted(), + 'isImporting' => $this->isImporting(), ); } + public function getMonogram() { + return 'r'.$this->getCallsign(); + } + public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } @@ -155,7 +163,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return $this->getDetail('local-path'); } - public function getSubversionBaseURI() { + public function getSubversionBaseURI($commit = null) { + $subpath = $this->getDetail('svn-subpath'); + if (!strlen($subpath)) { + $subpath = null; + } + return $this->getSubversionPathURI($subpath, $commit); + } + + public function getSubversionPathURI($path = null, $commit = null) { $vcs = $this->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { throw new Exception("Not a subversion repository!"); @@ -167,168 +183,279 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $uri = $this->getDetail('remote-uri'); } - $subpath = $this->getDetail('svn-subpath'); - if ($subpath) { - $subpath = '/'.ltrim($subpath, '/'); + $uri = rtrim($uri, '/'); + + if (strlen($path)) { + $path = rawurlencode($path); + $path = str_replace('%2F', '/', $path); + $uri = $uri.'/'.ltrim($path, '/'); } - return $uri.$subpath; + if ($path !== null || $commit !== null) { + $uri .= '@'; + } + + if ($commit !== null) { + $uri .= $commit; + } + + return $uri; } + public function attachProjectPHIDs(array $project_phids) { + $this->projectPHIDs = $project_phids; + return $this; + } + + public function getProjectPHIDs() { + return $this->assertAttached($this->projectPHIDs); + } + + + /** + * Get the name of the directory this repository should clone or checkout + * into. For example, if the repository name is "Example Repository", a + * reasonable name might be "example-repository". This is used to help users + * get reasonable results when cloning repositories, since they generally do + * not want to clone into directories called "X/" or "Example Repository/". + * + * @return string + */ + public function getCloneName() { + $name = $this->getDetail('clone-name'); + + // Make some reasonable effort to produce reasonable default directory + // names from repository names. + if (!strlen($name)) { + $name = $this->getName(); + $name = phutil_utf8_strtolower($name); + $name = preg_replace('@[/ -:]+@', '-', $name); + $name = trim($name, '-'); + if (!strlen($name)) { + $name = $this->getCallsign(); + } + } + + return $name; + } + + +/* -( Remote Command Execution )------------------------------------------- */ + + public function execRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); - $args = $this->formatRemoteCommand($args); - return call_user_func_array('exec_manual', $args); + return $this->newRemoteCommandFuture($args)->resolve(); } public function execxRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); - $args = $this->formatRemoteCommand($args); - return call_user_func_array('execx', $args); + return $this->newRemoteCommandFuture($args)->resolvex(); } public function getRemoteCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); - $args = $this->formatRemoteCommand($args); - return newv('ExecFuture', $args); + return $this->newRemoteCommandFuture($args); } public function passthruRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); - $args = $this->formatRemoteCommand($args); - return call_user_func_array('phutil_passthru', $args); + return $this->newRemoteCommandPassthru($args)->execute(); } - public function execLocalCommand($pattern /* , $arg, ... */) { - $this->assertLocalExists(); + private function newRemoteCommandFuture(array $argv) { + $argv = $this->formatRemoteCommand($argv); + $future = newv('ExecFuture', $argv); + $future->setEnv($this->getRemoteCommandEnvironment()); + return $future; + } + private function newRemoteCommandPassthru(array $argv) { + $argv = $this->formatRemoteCommand($argv); + $passthru = newv('PhutilExecPassthru', $argv); + $passthru->setEnv($this->getRemoteCommandEnvironment()); + return $passthru; + } + + +/* -( Local Command Execution )-------------------------------------------- */ + + + public function execLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); - $args = $this->formatLocalCommand($args); - return call_user_func_array('exec_manual', $args); + return $this->newLocalCommandFuture($args)->resolve(); } public function execxLocalCommand($pattern /* , $arg, ... */) { - $this->assertLocalExists(); - $args = func_get_args(); - $args = $this->formatLocalCommand($args); - return call_user_func_array('execx', $args); + return $this->newLocalCommandFuture($args)->resolvex(); } public function getLocalCommandFuture($pattern /* , $arg, ... */) { - $this->assertLocalExists(); - $args = func_get_args(); - $args = $this->formatLocalCommand($args); - return newv('ExecFuture', $args); + return $this->newLocalCommandFuture($args); } public function passthruLocalCommand($pattern /* , $arg, ... */) { - $this->assertLocalExists(); - $args = func_get_args(); - $args = $this->formatLocalCommand($args); - return call_user_func_array('phutil_passthru', $args); + return $this->newLocalCommandPassthru($args)->execute(); } + private function newLocalCommandFuture(array $argv) { + $this->assertLocalExists(); + + $argv = $this->formatLocalCommand($argv); + $future = newv('ExecFuture', $argv); + $future->setEnv($this->getLocalCommandEnvironment()); + + if ($this->usesLocalWorkingCopy()) { + $future->setCWD($this->getLocalPath()); + } + + return $future; + } + + private function newLocalCommandPassthru(array $argv) { + $this->assertLocalExists(); + + $argv = $this->formatLocalCommand($argv); + $future = newv('PhutilExecPassthru', $argv); + $future->setEnv($this->getLocalCommandEnvironment()); + + if ($this->usesLocalWorkingCopy()) { + $future->setCWD($this->getLocalPath()); + } + + return $future; + } + + +/* -( Command Infrastructure )--------------------------------------------- */ + + + private function getSSHWrapper() { + $root = dirname(phutil_get_library_root('phabricator')); + return $root.'/bin/ssh-connect'; + } + + private function getCommonCommandEnvironment() { + $env = array( + // NOTE: Force the language to "en_US.UTF-8", which overrides locale + // settings. This makes stuff print in English instead of, e.g., French, + // so we can parse the output of some commands, error messages, etc. + 'LANG' => 'en_US.UTF-8', + + // Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155. + 'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(), + ); + + switch ($this->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + // NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if + // it can not read $HOME. For many users, $HOME points at /root (this + // seems to be a default result of Apache setup). Instead, explicitly + // point $HOME at a readable, empty directory so that Git looks for the + // config file it's after, fails to locate it, and moves on. This is + // really silly, but seems like the least damaging approach to + // mitigating the issue. + + $root = dirname(phutil_get_library_root('phabricator')); + $env['HOME'] = $root.'/support/empty/'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // NOTE: This overrides certain configuration, extensions, and settings + // which make Mercurial commands do random unusual things. + $env['HGPLAIN'] = 1; + break; + default: + throw new Exception("Unrecognized version control system."); + } + + return $env; + } + + private function getLocalCommandEnvironment() { + return $this->getCommonCommandEnvironment(); + } + + private function getRemoteCommandEnvironment() { + $env = $this->getCommonCommandEnvironment(); + + if ($this->shouldUseSSH()) { + // NOTE: This is read by `bin/ssh-connect`, and tells it which credentials + // to use. + $env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID(); + switch ($this->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + // Force SVN to use `bin/ssh-connect`. + $env['SVN_SSH'] = $this->getSSHWrapper(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + // Force Git to use `bin/ssh-connect`. + $env['GIT_SSH'] = $this->getSSHWrapper(); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // We force Mercurial through `bin/ssh-connect` too, but it uses a + // command-line flag instead of an environmental variable. + break; + default: + throw new Exception("Unrecognized version control system."); + } + } + + return $env; + } private function formatRemoteCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); - $empty = $this->getEmptyReadableDirectoryPath(); + switch ($this->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) { + $flags = array(); + $flag_args = array(); + $flags[] = '--non-interactive'; + $flags[] = '--no-auth-cache'; + if ($this->shouldUseHTTP()) { + $flags[] = '--trust-server-cert'; + } - if ($this->shouldUseSSH()) { - switch ($this->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $pattern = "SVN_SSH=%s svn --trust-server-cert --non-interactive {$pattern}"; - array_unshift( - $args, - csprintf( - 'ssh -l %P -i %P', - new PhutilOpaqueEnvelope($this->getSSHLogin()), - new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $command = call_user_func_array( - 'csprintf', - array_merge( - array( - "(ssh-add %P && HOME=%s git {$pattern})", - new PhutilOpaqueEnvelope($this->getSSHKeyfile()), - $empty, - ), - $args)); - $pattern = "ssh-agent sh -c %s"; - $args = array($command); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $credential_phid = $this->getCredentialPHID(); + if ($credential_phid) { + $key = PassphrasePasswordKey::loadFromPHID( + $credential_phid, + PhabricatorUser::getOmnipotentUser()); + $flags[] = '--username %P'; + $flags[] = '--password %P'; + $flag_args[] = $key->getUsernameEnvelope(); + $flag_args[] = $key->getPasswordEnvelope(); + } + + $flags = implode(' ', $flags); + $pattern = "svn {$flags} {$pattern}"; + $args = array_mergev(array($flag_args, $args)); + } else { + $pattern = "svn --non-interactive {$pattern}"; + } + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $pattern = "git {$pattern}"; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + if ($this->shouldUseSSH()) { $pattern = "hg --config ui.ssh=%s {$pattern}"; array_unshift( $args, - csprintf( - 'ssh -l %P -i %P', - new PhutilOpaqueEnvelope($this->getSSHLogin()), - new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); - break; - default: - throw new Exception("Unrecognized version control system."); - } - } else if ($this->shouldUseHTTP()) { - switch ($this->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $pattern = - "svn ". - "--non-interactive ". - "--no-auth-cache ". - "--trust-server-cert ". - "--username %P ". - "--password %P ". - $pattern; - array_unshift( - $args, - new PhutilOpaqueEnvelope($this->getDetail('http-login')), - new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); - break; - default: - throw new Exception( - "No support for HTTP Basic Auth in this version control system."); - } - } else if ($this->shouldUseSVNProtocol()) { - switch ($this->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $pattern = - "svn ". - "--non-interactive ". - "--trust-server-cert ". - "--no-auth-cache ". - "--username %P ". - "--password %P ". - $pattern; - array_unshift( - $args, - new PhutilOpaqueEnvelope($this->getDetail('http-login')), - new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); - break; - default: - throw new Exception( - "SVN protocol is SVN only."); - } - } else { - switch ($this->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $pattern = "svn --non-interactive --trust-server-cert {$pattern}"; - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $pattern = "HOME=%s git {$pattern}"; - array_unshift($args, $empty); - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $this->getSSHWrapper()); + } else { $pattern = "hg {$pattern}"; - break; - default: - throw new Exception("Unrecognized version control system."); - } + } + break; + default: + throw new Exception("Unrecognized version control system."); } array_unshift($args, $pattern); @@ -340,21 +467,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $pattern = $args[0]; $args = array_slice($args, 1); - $empty = $this->getEmptyReadableDirectoryPath(); - switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $pattern = "(cd %s && svn --non-interactive {$pattern})"; - array_unshift($args, $this->getLocalPath()); + $pattern = "svn --non-interactive {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $pattern = "(cd %s && HOME=%s git {$pattern})"; - array_unshift($args, $this->getLocalPath(), $empty); + $pattern = "git {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: - $hgplain = (phutil_is_windows() ? "set HGPLAIN=1 &&" : "HGPLAIN=1"); - $pattern = "(cd %s && {$hgplain} hg {$pattern})"; - array_unshift($args, $this->getLocalPath()); + $pattern = "hg {$pattern}"; break; default: throw new Exception("Unrecognized version control system."); @@ -365,46 +486,59 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return $args; } - private function getEmptyReadableDirectoryPath() { - // See T2965. Some time after Git 1.7.5.4, Git started fataling if it can - // not read $HOME. For many users, $HOME points at /root (this seems to be - // a default result of Apache setup). Instead, explicitly point $HOME at a - // readable, empty directory so that Git looks for the config file it's - // after, fails to locate it, and moves on. This is really silly, but seems - // like the least damaging approach to mitigating the issue. - $root = dirname(phutil_get_library_root('phabricator')); - return $root.'/support/empty/'; - } + /** + * Sanitize output of an `hg` command invoked with the `--debug` flag to make + * it usable. + * + * @param string Output from `hg --debug ...` + * @return string Usable output. + */ + public static function filterMercurialDebugOutput($stdout) { + // When hg commands are run with `--debug` and some config file isn't + // trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011. + // + // http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html - private function getSSHLogin() { - return $this->getDetail('ssh-login'); - } + $lines = preg_split('/(?<=\n)/', $stdout); + $regex = '/ignoring untrusted configuration option .*\n$/'; - private function getSSHKeyfile() { - if ($this->sshKeyfile === null) { - $key = $this->getDetail('ssh-key'); - $keyfile = $this->getDetail('ssh-keyfile'); - if ($keyfile) { - // Make sure we can read the file, that it exists, etc. - Filesystem::readFile($keyfile); - $this->sshKeyfile = $keyfile; - } else if ($key) { - $keyfile = new TempFile('phabricator-repository-ssh-key'); - chmod($keyfile, 0600); - Filesystem::writeFile($keyfile, $key); - $this->sshKeyfile = $keyfile; - } else { - $this->sshKeyfile = ''; - } + foreach ($lines as $key => $line) { + $lines[$key] = preg_replace($regex, '', $line); } - return (string)$this->sshKeyfile; + return implode('', $lines); } public function getURI() { return '/diffusion/'.$this->getCallsign().'/'; } + public function getNormalizedPath() { + $uri = (string)$this->getCloneURIObject(); + + switch ($this->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $normalized_uri = new PhabricatorRepositoryURINormalizer( + PhabricatorRepositoryURINormalizer::TYPE_GIT, + $uri); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $normalized_uri = new PhabricatorRepositoryURINormalizer( + PhabricatorRepositoryURINormalizer::TYPE_SVN, + $uri); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $normalized_uri = new PhabricatorRepositoryURINormalizer( + PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, + $uri); + break; + default: + throw new Exception("Unrecognized version control system."); + } + + return $normalized_uri->getNormalizedPath(); + } + public function isTracked() { return $this->getDetail('tracking-enabled', false); } @@ -480,6 +614,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO throw new Exception("Unrecognized version control system."); } + $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; + if ($commit->isPartiallyImported($closeable_flag)) { + return true; + } + + // TODO: Remove this eventually, it's no longer written to by the import + // pipeline (however, old tasks may still be queued which don't reflect + // the new data format). $branches = $data->getCommitDetail('seenOnBranches', array()); foreach ($branches as $branch) { if ($this->shouldAutocloseBranch($branch)) { @@ -511,6 +653,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return (bool)$this->getDetail('importing', false); } + /* -( Repository URI Management )------------------------------------------ */ @@ -526,26 +669,61 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO /** - * Get the remote URI for this repository, without authentication information. + * Get the remote URI for this repository, including credentials if they're + * used by this repository. + * + * @return PhutilOpaqueEnvelope URI, possibly including credentials. + * @task uri + */ + public function getRemoteURIEnvelope() { + $uri = $this->getRemoteURIObject(); + + $remote_protocol = $this->getRemoteProtocol(); + if ($remote_protocol == 'http' || $remote_protocol == 'https') { + // For SVN, we use `--username` and `--password` flags separately, so + // don't add any credentials here. + if (!$this->isSVN()) { + $credential_phid = $this->getCredentialPHID(); + if ($credential_phid) { + $key = PassphrasePasswordKey::loadFromPHID( + $credential_phid, + PhabricatorUser::getOmnipotentUser()); + + $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); + $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); + } + } + } + + return new PhutilOpaqueEnvelope((string)$uri); + } + + + /** + * Get the clone (or checkout) URI for this repository, without authentication + * information. * * @return string Repository URI. * @task uri */ - public function getPublicRemoteURI() { - $uri = $this->getRemoteURIObject(); + public function getPublicCloneURI() { + $uri = $this->getCloneURIObject(); // Make sure we don't leak anything if this repo is using HTTP Basic Auth // with the credentials in the URI or something zany like that. - if ($uri instanceof PhutilGitURI) { - if (!$this->getDetail('show-user', false)) { + // If repository is not accessed over SSH we remove both username and + // password. + if (!$this->isHosted()) { + if (!$this->shouldUseSSH()) { $uri->setUser(null); + + // This might be a Git URI or a normal URI. If it's Git, there's no + // password support. + if ($uri instanceof PhutilURI) { + $uri->setPass(null); + } } - } else { - if (!$this->getDetail('show-user', false)) { - $uri->setUser(null); - } - $uri->setPass(null); } return (string)$uri; @@ -578,7 +756,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO * @{class@libphutil:PhutilGitURI}. * @task uri */ - private function getRemoteURIObject() { + public function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!$raw_uri) { return new PhutilURI(''); @@ -590,19 +768,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { - if ($this->isSSHProtocol($uri->getProtocol())) { - if ($this->getSSHLogin()) { - $uri->setUser($this->getSSHLogin()); - } - } return $uri; } $uri = new PhutilGitURI($raw_uri); if ($uri->getDomain()) { - if ($this->getSSHLogin()) { - $uri->setUser($this->getSSHLogin()); - } return $uri; } @@ -610,6 +780,106 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } + /** + * Get the "best" clone/checkout URI for this repository, on any protocol. + */ + public function getCloneURIObject() { + if (!$this->isHosted()) { + return $this->getRemoteURIObject(); + } + + // Choose the best URI: pick a read/write URI over a URI which is not + // read/write, and SSH over HTTP. + + $serve_ssh = $this->getServeOverSSH(); + $serve_http = $this->getServeOverHTTP(); + + if ($serve_ssh === self::SERVE_READWRITE) { + return $this->getSSHCloneURIObject(); + } else if ($serve_http === self::SERVE_READWRITE) { + return $this->getHTTPCloneURIObject(); + } else if ($serve_ssh !== self::SERVE_OFF) { + return $this->getSSHCloneURIObject(); + } else if ($serve_http !== self::SERVE_OFF) { + return $this->getHTTPCloneURIObject(); + } else { + return null; + } + } + + + /** + * Get the repository's SSH clone/checkout URI, if one exists. + */ + public function getSSHCloneURIObject() { + if (!$this->isHosted()) { + if ($this->shouldUseSSH()) { + return $this->getRemoteURIObject(); + } else { + return null; + } + } + + $serve_ssh = $this->getServeOverSSH(); + if ($serve_ssh === self::SERVE_OFF) { + return null; + } + + $uri = new PhutilURI(PhabricatorEnv::getProductionURI($this->getURI())); + + if ($this->isSVN()) { + $uri->setProtocol('svn+ssh'); + } else { + $uri->setProtocol('ssh'); + } + + if ($this->isGit()) { + $uri->setPath($uri->getPath().$this->getCloneName().'.git'); + } else if ($this->isHg()) { + $uri->setPath($uri->getPath().$this->getCloneName().'/'); + } + + $ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); + if ($ssh_user) { + $uri->setUser($ssh_user); + } + + $uri->setPort(PhabricatorEnv::getEnvConfig('diffusion.ssh-port')); + + return $uri; + } + + + /** + * Get the repository's HTTP clone/checkout URI, if one exists. + */ + public function getHTTPCloneURIObject() { + if (!$this->isHosted()) { + if ($this->shouldUseHTTP()) { + return $this->getRemoteURIObject(); + } else { + return null; + } + } + + $serve_http = $this->getServeOverHTTP(); + if ($serve_http === self::SERVE_OFF) { + return null; + } + + $uri = PhabricatorEnv::getProductionURI($this->getURI()); + $uri = new PhutilURI($uri); + + if ($this->isGit()) { + $uri->setPath($uri->getPath().$this->getCloneName().'.git'); + } else if ($this->isHg()) { + $uri->setPath($uri->getPath().$this->getCloneName().'/'); + } + + return $uri; + } + + /** * Determine if we should connect to the remote using SSH flags and * credentials. @@ -624,10 +894,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $protocol = $this->getRemoteProtocol(); if ($this->isSSHProtocol($protocol)) { - return (bool)$this->getSSHKeyfile(); - } else { - return false; + return true; } + + return false; } @@ -644,11 +914,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } $protocol = $this->getRemoteProtocol(); - if ($protocol == 'http' || $protocol == 'https') { - return (bool)$this->getDetail('http-login'); - } else { - return false; - } + return ($protocol == 'http' || $protocol == 'https'); } @@ -665,11 +931,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } $protocol = $this->getRemoteProtocol(); - if ($protocol == 'svn') { - return (bool)$this->getDetail('http-login'); - } else { - return false; - } + return ($protocol == 'svn'); } @@ -708,6 +970,18 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $commit->delete(); } + $mirrors = id(new PhabricatorRepositoryMirror()) + ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); + foreach ($mirrors as $mirror) { + $mirror->delete(); + } + + $ref_cursors = id(new PhabricatorRepositoryRefCursor()) + ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); + foreach ($ref_cursors as $cursor) { + $cursor->delete(); + } + $conn_w = $this->establishConnection('w'); queryfx( @@ -758,6 +1032,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } public function getServeOverHTTP() { + if ($this->isSVN()) { + return self::SERVE_OFF; + } $serve = $this->getDetail('serve-over-http', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } @@ -848,6 +1125,70 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } } + public function getHookDirectories() { + $directories = array(); + if (!$this->isHosted()) { + return $directories; + } + + $root = $this->getLocalPath(); + + switch ($this->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + if ($this->isWorkingCopyBare()) { + $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; + } else { + $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; + } + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // NOTE: We don't support custom Mercurial hooks for now because they're + // messy and we can't easily just drop a `hooks.d/` directory next to + // the hooks. + break; + } + + return $directories; + } + + public function canDestroyWorkingCopy() { + if ($this->isHosted()) { + // Never destroy hosted working copies. + return false; + } + + $default_path = PhabricatorEnv::getEnvConfig( + 'repository.default-local-path'); + return Filesystem::isDescendant($this->getLocalPath(), $default_path); + } + + public function canMirror() { + if ($this->isGit() || $this->isHg()) { + return true; + } + + return false; + } + + public function canAllowDangerousChanges() { + if (!$this->isHosted()) { + return false; + } + + if ($this->isGit() || $this->isHg()) { + return true; + } + + return false; + } + + public function shouldAllowDangerousChanges() { + return (bool)$this->getDetail('allow-dangerous-changes'); + } + public function writeStatusMessage( $status_type, $status_code, diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index cdf7c585c0..d8137cd156 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -5,7 +5,8 @@ final class PhabricatorRepositoryCommit implements PhabricatorPolicyInterface, PhabricatorFlaggableInterface, - PhabricatorTokenReceiverInterface { + PhabricatorTokenReceiverInterface, + HarbormasterBuildableInterface { protected $repositoryID; protected $phid; @@ -23,6 +24,8 @@ final class PhabricatorRepositoryCommit const IMPORTED_HERALD = 8; const IMPORTED_ALL = 15; + const IMPORTED_CLOSEABLE = 1024; + private $commitData = self::ATTACHABLE; private $audits; private $repository = self::ATTACHABLE; @@ -41,7 +44,7 @@ final class PhabricatorRepositoryCommit } public function isImported() { - return ($this->getImportStatus() == self::IMPORTED_ALL); + return $this->isPartiallyImported(self::IMPORTED_ALL); } public function writeImportStatusFlag($flag) { @@ -231,4 +234,17 @@ final class PhabricatorRepositoryCommit return id(new PhabricatorRepositoryCommit()) ->loadFromArray($dict); } + + +/* -( HarbormasterBuildableInterface )------------------------------------- */ + + + public function getHarbormasterBuildablePHID() { + return $this->getPHID(); + } + + public function getHarbormasterContainerPHID() { + return $this->getRepository()->getPHID(); + } + } diff --git a/src/applications/repository/storage/PhabricatorRepositoryMirror.php b/src/applications/repository/storage/PhabricatorRepositoryMirror.php new file mode 100644 index 0000000000..a4205d4dc8 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryMirror.php @@ -0,0 +1,60 @@ +setRemoteURI(''); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorRepositoryPHIDTypeMirror::TYPECONST); + } + + public function attachRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->assertAttached($this->repository); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getRepository()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php new file mode 100644 index 0000000000..3c0fdff381 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -0,0 +1,144 @@ +setPusherPHID($viewer->getPHID()); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_TIMESTAMPS => false, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorRepositoryPHIDTypePushLog::TYPECONST); + } + + public function attachRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->assertAttached($this->repository); + } + + public function getRefName() { + return $this->getUTF8StringFromStorage( + $this->getRefNameRaw(), + $this->getRefNameEncoding()); + } + + public function setRefName($ref_raw) { + $this->setRefNameRaw($ref_raw); + $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); + $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); + + return $this; + } + + public function getRefOldShort() { + if ($this->getRepository()->isSVN()) { + return $this->getRefOld(); + } + return substr($this->getRefOld(), 0, 12); + } + + public function getRefNewShort() { + if ($this->getRepository()->isSVN()) { + return $this->getRefNew(); + } + return substr($this->getRefNew(), 0, 12); + } + + public function hasChangeFlags($mask) { + return ($this->changeFlags & $mask); + } + + public function attachDangerousChangeDescription($description) { + $this->dangerousChangeDescription = $description; + return $this; + } + + public function getDangerousChangeDescription() { + return $this->assertAttached($this->dangerousChangeDescription); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getRepository()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + "A repository's push logs are visible to users who can see the ". + "repository."); + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php new file mode 100644 index 0000000000..8f93490056 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php @@ -0,0 +1,75 @@ + false, + ) + parent::getConfiguration(); + } + + public function getRefName() { + return $this->getUTF8StringFromStorage( + $this->getRefNameRaw(), + $this->getRefNameEncoding()); + } + + public function setRefName($ref_raw) { + $this->setRefNameRaw($ref_raw); + $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); + $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); + + return $this; + } + + public function attachRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->assertAttached($this->repository); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getRepository()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Repository refs have the same policies as their repository.'); + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryShortcut.php b/src/applications/repository/storage/PhabricatorRepositoryShortcut.php deleted file mode 100644 index cb3d00dfc7..0000000000 --- a/src/applications/repository/storage/PhabricatorRepositoryShortcut.php +++ /dev/null @@ -1,16 +0,0 @@ - false, - ) + parent::getConfiguration(); - } - -} diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php index e40c139cf4..c29aafde9b 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php +++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php @@ -16,16 +16,21 @@ final class PhabricatorRepositoryTransaction const TYPE_NOTIFY = 'repo:notify'; const TYPE_AUTOCLOSE = 'repo:autoclose'; const TYPE_REMOTE_URI = 'repo:remote-uri'; - const TYPE_SSH_LOGIN = 'repo:ssh-login'; - const TYPE_SSH_KEY = 'repo:ssh-key'; - const TYPE_SSH_KEYFILE = 'repo:ssh-keyfile'; - const TYPE_HTTP_LOGIN = 'repo:http-login'; - const TYPE_HTTP_PASS = 'repo:http-pass'; const TYPE_LOCAL_PATH = 'repo:local-path'; const TYPE_HOSTING = 'repo:hosting'; const TYPE_PROTOCOL_HTTP = 'repo:serve-http'; const TYPE_PROTOCOL_SSH = 'repo:serve-ssh'; const TYPE_PUSH_POLICY = 'repo:push-policy'; + const TYPE_CREDENTIAL = 'repo:credential'; + const TYPE_DANGEROUS = 'repo:dangerous'; + const TYPE_CLONE_NAME = 'repo:clone-name'; + + // TODO: Clean up these legacy transaction types. + const TYPE_SSH_LOGIN = 'repo:ssh-login'; + const TYPE_SSH_KEY = 'repo:ssh-key'; + const TYPE_SSH_KEYFILE = 'repo:ssh-keyfile'; + const TYPE_HTTP_LOGIN = 'repo:http-login'; + const TYPE_HTTP_PASS = 'repo:http-pass'; public function getApplicationName() { return 'repository'; @@ -335,6 +340,33 @@ final class PhabricatorRepositoryTransaction $this->renderHandleLink($author_phid), $this->renderPolicyName($old), $this->renderPolicyName($new)); + case self::TYPE_DANGEROUS: + if ($new) { + return pht( + '%s disabled protection against dangerous changes.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s enabled protection against dangerous changes.', + $this->renderHandleLink($author_phid)); + } + case self::TYPE_CLONE_NAME: + if (strlen($old) && !strlen($new)) { + return pht( + '%s removed the clone name of this repository.', + $this->renderHandleLink($author_phid)); + } else if (strlen($new) && !strlen($old)) { + return pht( + '%s set the clone name of this repository to "%s".', + $this->renderHandleLink($author_phid), + $new); + } else { + return pht( + '%s changed the clone name of this repository from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } } return parent::getTitle(); diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php index 23feced972..5b60b2968e 100644 --- a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php +++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php @@ -55,4 +55,104 @@ final class PhabricatorRepositoryTestCase 'Do not track unlisted branches.'); } + public function testSubversionPathInfo() { + $svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; + + $repo = new PhabricatorRepository(); + $repo->setVersionControlSystem($svn); + + $repo->setDetail('remote-uri', 'http://svn.example.com/repo'); + $this->assertEqual( + 'http://svn.example.com/repo', + $repo->getSubversionPathURI()); + + $repo->setDetail('remote-uri', 'http://svn.example.com/repo/'); + $this->assertEqual( + 'http://svn.example.com/repo', + $repo->getSubversionPathURI()); + + $repo->setDetail('hosting-enabled', true); + + $repo->setDetail('local-path', '/var/repo/SVN'); + $this->assertEqual( + 'file:///var/repo/SVN', + $repo->getSubversionPathURI()); + + $repo->setDetail('local-path', '/var/repo/SVN/'); + $this->assertEqual( + 'file:///var/repo/SVN', + $repo->getSubversionPathURI()); + + $this->assertEqual( + 'file:///var/repo/SVN/a@', + $repo->getSubversionPathURI('a')); + + $this->assertEqual( + 'file:///var/repo/SVN/a@1', + $repo->getSubversionPathURI('a', 1)); + + $this->assertEqual( + 'file:///var/repo/SVN/%3F@22', + $repo->getSubversionPathURI('?', 22)); + + $repo->setDetail('svn-subpath', 'quack/trunk/'); + + $this->assertEqual( + 'file:///var/repo/SVN/quack/trunk/@', + $repo->getSubversionBaseURI()); + + $this->assertEqual( + 'file:///var/repo/SVN/quack/trunk/@HEAD', + $repo->getSubversionBaseURI('HEAD')); + + } + + public function testFilterMercurialDebugOutput() { + $map = array( + "" => "", + + "quack\n" => "quack\n", + + "ignoring untrusted configuration option x.y = z\nquack\n" => + "quack\n", + + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n". + "quack\n" => + "quack\n", + + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n". + "quack\n" => + "quack\n", + + "quack\n". + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n" => + "quack\n", + + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n". + "duck\n". + "ignoring untrusted configuration option x.y = z\n". + "ignoring untrusted configuration option x.y = z\n". + "bread\n". + "ignoring untrusted configuration option x.y = z\n". + "quack\n" => + "duck\nbread\nquack\n", + + "ignoring untrusted configuration option x.y = z\n". + "duckignoring untrusted configuration option x.y = z\n". + "quack" => + "duckquack", + ); + + foreach ($map as $input => $expect) { + $actual = PhabricatorRepository::filterMercurialDebugOutput($input); + $this->assertEqual($expect, $actual, $input); + } + } + } diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php new file mode 100644 index 0000000000..5c5d0f87c5 --- /dev/null +++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php @@ -0,0 +1,95 @@ + true, + ); + } + + public function testURIGeneration() { + $svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; + $git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; + $hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; + + $user = $this->generateNewTestUser(); + + $http_secret = id(new PassphraseSecret())->setSecretData('quack')->save(); + + $http_credential = PassphraseCredential::initializeNewCredential($user) + ->setCredentialType(PassphraseCredentialTypePassword::CREDENTIAL_TYPE) + ->setProvidesType(PassphraseCredentialTypePassword::PROVIDES_TYPE) + ->setUsername('duck') + ->setSecretID($http_secret->getID()) + ->save(); + + $repo = PhabricatorRepository::initializeNewRepository($user) + ->setVersionControlSystem($svn) + ->setName('Test Repo') + ->setCallsign('TESTREPO') + ->setCredentialPHID($http_credential->getPHID()) + ->save(); + + // Test HTTP URIs. + + $repo->setDetail('remote-uri', 'http://example.com/'); + $repo->setVersionControlSystem($svn); + + $this->assertEqual('http://example.com/', $repo->getRemoteURI()); + $this->assertEqual('http://example.com/', $repo->getPublicCloneURI()); + $this->assertEqual('http://example.com/', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + $repo->setVersionControlSystem($git); + + $this->assertEqual('http://example.com/', $repo->getRemoteURI()); + $this->assertEqual('http://example.com/', $repo->getPublicCloneURI()); + $this->assertEqual('http://duck:quack@example.com/', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + $repo->setVersionControlSystem($hg); + + $this->assertEqual('http://example.com/', $repo->getRemoteURI()); + $this->assertEqual('http://example.com/', $repo->getPublicCloneURI()); + $this->assertEqual('http://duck:quack@example.com/', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + // Test SSH URIs. + + $repo->setDetail('remote-uri', 'ssh://example.com/'); + $repo->setVersionControlSystem($svn); + + $this->assertEqual('ssh://example.com/', $repo->getRemoteURI()); + $this->assertEqual('ssh://example.com/', $repo->getPublicCloneURI()); + $this->assertEqual('ssh://example.com/', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + $repo->setVersionControlSystem($git); + + $this->assertEqual('ssh://example.com/', $repo->getRemoteURI()); + $this->assertEqual('ssh://example.com/', $repo->getPublicCloneURI()); + $this->assertEqual('ssh://example.com/', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + $repo->setVersionControlSystem($hg); + + $this->assertEqual('ssh://example.com/', $repo->getRemoteURI()); + $this->assertEqual('ssh://example.com/', $repo->getPublicCloneURI()); + $this->assertEqual('ssh://example.com/', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + // Test Git URIs. + + $repo->setDetail('remote-uri', 'git@example.com:path.git'); + $repo->setVersionControlSystem($git); + + $this->assertEqual('git@example.com:path.git', $repo->getRemoteURI()); + $this->assertEqual('git@example.com:path.git', $repo->getPublicCloneURI()); + $this->assertEqual('git@example.com:path.git', + $repo->getRemoteURIEnvelope()->openEnvelope()); + + } + +} diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php index eaa9f682e8..8f4b3416d6 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -36,14 +36,14 @@ final class PhabricatorRepositoryCommitHeraldWorker $commit->getID()); if (!$data) { - // TODO: Permanent failure. - return; + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Unable to load commit data. The data for this task is invalid '. + 'or no longer exists.')); } - $adapter = HeraldCommitAdapter::newLegacyAdapter( - $repository, - $commit, - $data); + $adapter = id(new HeraldCommitAdapter()) + ->setCommit($commit); $rules = id(new HeraldRuleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) @@ -66,6 +66,11 @@ final class PhabricatorRepositoryCommitHeraldWorker $this->createAudits($commit, $audit_phids, $cc_phids, $rules); } + HarbormasterBuildable::applyBuildPlans( + $commit->getPHID(), + $repository->getPHID(), + $adapter->getBuildPlans()); + $explicit_auditors = $this->createAuditsFromCommitMessage($commit, $data); if ($repository->getDetail('herald-disabled')) { @@ -227,14 +232,12 @@ final class PhabricatorRepositoryCommitHeraldWorker } if ($status == PhabricatorAuditStatusConstants::AUDIT_REQUIRED) { $reasons[] = pht( - 'Herald Rule #%d "%s" Triggered Audit', - $id, - $rule_name); + '%s Triggered Audit', + "H{$id} {$rule_name}"); } else { $reasons[] = pht( - 'Herald Rule #%d "%s" Triggered CC', - $id, - $rule_name); + '%s Triggered CC', + "H{$id} {$rule_name}"); } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php index e75e6022c8..b93474e71c 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php @@ -40,8 +40,7 @@ abstract class PhabricatorRepositoryCommitParserWorker } $this->repository = $repository; - - return $this->parseCommit($repository, $this->commit); + $this->parseCommit($repository, $this->commit); } final protected function shouldQueueFollowupTasks() { @@ -52,28 +51,6 @@ abstract class PhabricatorRepositoryCommitParserWorker PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit); - /** - * This method is kind of awkward here but both the SVN message and - * change parsers use it. - */ - protected function getSVNLogXMLObject($uri, $revision, $verbose = false) { - - if ($verbose) { - $verbose = '--verbose'; - } - - list($xml) = $this->repository->execxRemoteCommand( - "log --xml {$verbose} --limit 1 %s@%d", - $uri, - $revision); - - // Subversion may send us back commit messages which won't parse because - // they have non UTF-8 garbage in them. Slam them into valid UTF-8. - $xml = phutil_utf8ize($xml); - - return new SimpleXMLElement($xml); - } - protected function isBadCommit($full_commit_name) { $repository = new PhabricatorRepository(); @@ -86,19 +63,21 @@ abstract class PhabricatorRepositoryCommitParserWorker return (bool)$bad_commit; } - public function renderForDisplay() { - $suffix = parent::renderForDisplay(); - $commit = $this->loadCommit(); + public function renderForDisplay(PhabricatorUser $viewer) { + $suffix = parent::renderForDisplay($viewer); + + $commit = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withIDs(array(idx($this->getTaskData(), 'commitID'))) + ->executeOne(); if (!$commit) { return $suffix; } - // TODO: (T603) This method should probably take a viewer. + $link = DiffusionView::linkCommit( + $commit->getRepository(), + $commit->getCommitIdentifier()); - $repository = id(new PhabricatorRepository()) - ->load($commit->getRepositoryID()); - $link = DiffusionView::linkCommit($repository, - $commit->getCommitIdentifier()); - return hsprintf('%s%s', $link, $suffix); + return array($link, $suffix); } } diff --git a/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php b/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php new file mode 100644 index 0000000000..1f02ae8e7a --- /dev/null +++ b/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php @@ -0,0 +1,1186 @@ +buildDiscoveredRepository('CHA'); + $viewer = PhabricatorUser::getOmnipotentUser(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepositoryIDs(array($repository->getID())) + ->execute(); + + $this->expectChanges( + $repository, + $commits, + array( + // 8ebb73c add +x + '8ebb73c3f127625ad090472f4f3bfc72804def54' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1389892449, + ), + array( + '/file_moved', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892449, + ), + ), + + // ee9c790 add symlink + 'ee9c7909e012da7d75e8e1293c7803a6e73ac26a' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1389892436, + ), + array( + '/file_link', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_SYMLINK, + 1, + 1389892436, + ), + ), + + // 7260ca4 add directory file + '7260ca4b6cec35e755bb5365c4ccdd3f1977772e' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1389892408, + ), + array( + '/dir', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 1389892408, + ), + array( + '/dir/subfile', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892408, + ), + ), + + // 1fe783c move a file + '1fe783cf207c1e5f3e01650d2d9cb80b8a707f0e' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1389892388, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_MOVE_AWAY, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892388, + ), + array( + '/file_moved', + '/file', + '1fe783cf207c1e5f3e01650d2d9cb80b8a707f0e', + DifferentialChangeType::TYPE_MOVE_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892388, + ), + ), + + // 376af8c copy a file + '376af8cd8f5b96ec55b7d9a86ccc85b8df8fb833' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1389892377, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 1389892377, + ), + array( + '/file_copy', + '/file', + '376af8cd8f5b96ec55b7d9a86ccc85b8df8fb833', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892377, + ), + ), + + // ece6ea6 changed a file + 'ece6ea6c6836e8b11a103e21707b8f30e6840c94' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1389892352, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892352, + ), + ), + + // 513103f added a file + '513103f65b8413dd2f1a1b5c1d4852a4a598540f' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + // This is the initial commit and technically created this + // directory; arguably the parser should figure this out and + // mark this as a direct change. + 0, + 1389892330, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 1389892330, + ), + ), + )); + } + + public function testMercurialParser() { + $repository = $this->buildDiscoveredRepository('CHB'); + $viewer = PhabricatorUser::getOmnipotentUser(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepositoryIDs(array($repository->getID())) + ->execute(); + + $this->expectChanges( + $repository, + $commits, + array( + '970357a2dc4264060e65d68e42240bb4e5984085' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1390249395, + ), + array( + '/file_moved', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249395, + ), + ), + + 'fbb49af9788e5dbffbc05a060b680df1fd457be3' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1390249380, + ), + array( + '/file_link', + null, + null, + DifferentialChangeType::TYPE_ADD, + // TODO: This is not correct, and should be FILE_SYMLINK. See + // note in the parser about this. This is a known bug. + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249380, + ), + ), + + '0e8d3465944c7ed7a7c139da7edc652cf80dba69' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1390249342, + ), + array( + '/dir', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 1390249342, + ), + array( + '/dir/subfile', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249342, + ), + ), + + '22c75131ff15c8a44d7a729c4542b7f4c8ed27f4' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1390249320, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_MOVE_AWAY, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249320, + ), + array( + '/file_moved', + '/file', + '22c75131ff15c8a44d7a729c4542b7f4c8ed27f4', + DifferentialChangeType::TYPE_MOVE_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249320, + ), + ), + + 'd9d252df30cb7251ad3ea121eff30c7d2e36dd67' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1390249308, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 1390249308, + ), + array( + '/file_copy', + '/file', + 'd9d252df30cb7251ad3ea121eff30c7d2e36dd67', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249308, + ), + ), + + '1fc0445d5e3d0f33e9dcbb68bbe419a847460d25' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1390249294, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249294, + ), + ), + + '61518e196efb7f80700333cc0d00634c2578871a' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 1390249286, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 1390249286, + ), + ), + )); + } + + public function testSubversionParser() { + $repository = $this->buildDiscoveredRepository('CHC'); + $viewer = PhabricatorUser::getOmnipotentUser(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepositoryIDs(array($repository->getID())) + ->execute(); + + $this->expectChanges( + $repository, + $commits, + array( + '15' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 15, + ), + array( + '/file_copy', + null, + null, + DifferentialChangeType::TYPE_MULTICOPY, + DifferentialChangeType::FILE_NORMAL, + 1, + 15, + ), + array( + '/file_copy_x', + '/file_copy', + '12', + DifferentialChangeType::TYPE_MOVE_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 15, + ), + array( + '/file_copy_y', + '/file_copy', + '12', + DifferentialChangeType::TYPE_MOVE_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 15, + ), + array( + '/file_copy_z', + '/file_copy', + '12', + DifferentialChangeType::TYPE_MOVE_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 15, + ), + ), + + // Add a file from a different revision + '14' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 14, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 14, + ), + array( + '/file_1', + '/file', + '1', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 14, + ), + ), + + // Property change on "/" + '13' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 13, + ), + ), + + // Copy a directory, removing and adding files to the copy + '12' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 12, + ), + array( + '/dir', + null, + null, + // TODO: This might reasonbly be considered a bug in the parser; it + // should probably be COPY_AWAY. + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 12, + ), + array( + '/dir/a', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 12, + ), + array( + '/dir/b', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 12, + ), + array( + '/dir/subdir', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 12, + ), + array( + '/dir/subdir/a', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 12, + ), + array( + '/dir/subdir/b', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 12, + ), + array( + '/dir_copy', + '/dir', + '11', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 12, + ), + array( + '/dir_copy/a', + '/dir/a', + '11', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 12, + ), + array( + '/dir_copy/b', + '/dir/b', + '11', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 12, + ), + array( + '/dir_copy/subdir', + '/dir/subdir', + '11', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 12, + ), + array( + '/dir_copy/subdir/a', + '/dir/subdir/a', + '11', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 12, + ), + array( + '/dir_copy/subdir/b', + '/dir/subdir/b', + '11', + DifferentialChangeType::TYPE_DELETE, + DifferentialChangeType::FILE_NORMAL, + 1, + 12, + ), + array( + '/dir_copy/subdir/c', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 12, + ), + ), + + // Add a directory with a subdirectory and files, sets up next commit + '11' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 11, + ), + array( + '/dir', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 11, + ), + array( + '/dir/a', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 11, + ), + array( + '/dir/b', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 11, + ), + array( + '/dir/subdir', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 11, + ), + array( + '/dir/subdir/a', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 11, + ), + array( + '/dir/subdir/b', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 11, + ), + ), + + // Remove directory + '10' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 10, + ), + array( + '/dir', + null, + null, + DifferentialChangeType::TYPE_DELETE, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 10, + ), + array( + '/dir/subfile', + null, + null, + DifferentialChangeType::TYPE_DELETE, + DifferentialChangeType::FILE_NORMAL, + 1, + 10, + ), + ), + + // Replace directory with file + '9' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 9, + ), + array( + '/file_moved', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 9, + ), + ), + + // Replace file with file + '8' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 8, + ), + array( + '/file_moved', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 8, + ), + ), + + '7' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 7, + ), + array( + '/file_moved', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 7, + ), + ), + + '6' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 6, + ), + array( + '/file_link', + null, + null, + DifferentialChangeType::TYPE_ADD, + // TODO: This is not correct, and should be FILE_SYMLINK. + DifferentialChangeType::FILE_NORMAL, + 1, + 6, + ), + ), + + '5' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 5, + ), + array( + '/dir', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 5, + ), + array( + '/dir/subfile', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 5, + ), + ), + + '4' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 4, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_MOVE_AWAY, + DifferentialChangeType::FILE_NORMAL, + 1, + 4, + ), + array( + '/file_moved', + '/file', + '2', + DifferentialChangeType::TYPE_MOVE_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 4, + ), + ), + + '3' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 3, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 3, + ), + array( + '/file_copy', + '/file', + '2', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 3, + ), + ), + + '2' => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 2, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_CHANGE, + DifferentialChangeType::FILE_NORMAL, + 1, + 2, + ), + ), + + '1' => array( + array( + '/', + null, + null, + // The Git and Svn parsers don't recognize the first commit as + // creating "/", while the Mercurial parser does. All the parsers + // should probably behave like the Mercurial parser. + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 1, + ), + array( + '/file', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 1, + ), + ), + )); + } + + public function testSubversionPartialParser() { + $repository = $this->buildBareRepository('CHD'); + $repository->setDetail('svn-subpath', 'trunk/'); + + id(new PhabricatorRepositoryPullEngine()) + ->setRepository($repository) + ->pullRepository(); + + id(new PhabricatorRepositoryDiscoveryEngine()) + ->setRepository($repository) + ->discoverCommits(); + + $viewer = PhabricatorUser::getOmnipotentUser(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepositoryIDs(array($repository->getID())) + ->execute(); + + $this->expectChanges( + $repository, + $commits, + array( + // Copy of a file outside of the subpath from an earlier revision + // into the subpath. + 4 => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 4, + ), + array( + '/goat', + null, + null, + DifferentialChangeType::TYPE_COPY_AWAY, + DifferentialChangeType::FILE_NORMAL, + 0, + 4, + ), + array( + '/trunk', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 4, + ), + array( + '/trunk/goat', + '/goat', + '1', + DifferentialChangeType::TYPE_COPY_HERE, + DifferentialChangeType::FILE_NORMAL, + 1, + 4, + ), + ), + 3 => array( + array( + '/', + null, + null, + DifferentialChangeType::TYPE_CHILD, + DifferentialChangeType::FILE_DIRECTORY, + 0, + 3, + ), + array( + '/trunk', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_DIRECTORY, + 1, + 3, + ), + array( + '/trunk/apple', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 3, + ), + array( + '/trunk/banana', + null, + null, + DifferentialChangeType::TYPE_ADD, + DifferentialChangeType::FILE_NORMAL, + 1, + 3, + ), + ), + )); + } + + public function testSubversionValidRootParser() { + // First, automatically configure the root correctly. + $repository = $this->buildBareRepository('CHD'); + id(new PhabricatorRepositoryPullEngine()) + ->setRepository($repository) + ->pullRepository(); + + $caught = null; + try { + id(new PhabricatorRepositoryDiscoveryEngine()) + ->setRepository($repository) + ->discoverCommits(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertEqual( + false, + ($caught instanceof Exception), + pht('Natural SVN root should work properly.')); + + + // This time, artificially break the root. We expect this to fail. + $repository = $this->buildBareRepository('CHD'); + $repository->setDetail( + 'remote-uri', + $repository->getDetail('remote-uri').'trunk/'); + + id(new PhabricatorRepositoryPullEngine()) + ->setRepository($repository) + ->pullRepository(); + + $caught = null; + try { + id(new PhabricatorRepositoryDiscoveryEngine()) + ->setRepository($repository) + ->discoverCommits(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertEqual( + true, + ($caught instanceof Exception), + pht('Artificial SVN root should fail.')); + } + + + private function expectChanges( + PhabricatorRepository $repository, + array $commits, + array $expect) { + + switch ($repository->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $parser = 'PhabricatorRepositoryGitCommitChangeParserWorker'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $parser = 'PhabricatorRepositoryMercurialCommitChangeParserWorker'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $parser = 'PhabricatorRepositorySvnCommitChangeParserWorker'; + break; + default: + throw new Exception(pht('No support yet.')); + } + + foreach ($commits as $commit) { + $commit_identifier = $commit->getCommitIdentifier(); + $expect_changes = idx($expect, $commit_identifier); + + if ($expect_changes === null) { + $this->assertEqual( + $commit_identifier, + null, + pht( + 'No test entry for commit "%s" in repository "%s"!', + $commit_identifier, + $repository->getCallsign())); + } + + $parser_object = newv($parser, array(array())); + $changes = $parser_object->parseChangesForUnitTest($repository, $commit); + + $path_map = id(new DiffusionPathQuery()) + ->withPathIDs(mpull($changes, 'getPathID')) + ->execute(); + $path_map = ipull($path_map, 'path'); + + $target_commits = array_filter(mpull($changes, 'getTargetCommitID')); + if ($target_commits) { + $commits = id(new DiffusionCommitQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs($target_commits) + ->execute(); + $target_commits = mpull($commits, 'getCommitIdentifier', 'getID'); + } + + $dicts = array(); + foreach ($changes as $key => $change) { + $target_path = idx($path_map, $change->getTargetPathID()); + $target_commit = idx($target_commits, $change->getTargetCommitID()); + + $dicts[$key] = array( + $path_map[(int)$change->getPathID()], + $target_path, + $target_commit ? (string)$target_commit : null, + (int)$change->getChangeType(), + (int)$change->getFileType(), + (int)$change->getIsDirect(), + (int)$change->getCommitSequence(), + ); + } + + $dicts = ipull($dicts, null, 0); + $expect_changes = ipull($expect_changes, null, 0); + ksort($dicts); + ksort($expect_changes); + + $this->assertEqual( + $expect_changes, + $dicts, + pht('Commit %s', $commit_identifier)); + } + } + + +} diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php index 35d88d2eca..0fd91e83b7 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php @@ -24,14 +24,21 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker $this->log("Parsing %s...\n", $full_name); if ($this->isBadCommit($full_name)) { $this->log("This commit is marked bad!"); - $result = null; - } else { - $result = $this->parseCommitChanges($repository, $commit); + return; + } + + $results = $this->parseCommitChanges($repository, $commit); + if ($results) { + $this->writeCommitChanges($repository, $commit, $results); } $this->finishParse(); + } - return $result; + public function parseChangesForUnitTest( + PhabricatorRepository $repository, + PhabricatorRepositoryCommit $commit) { + return $this->parseCommitChanges($repository, $commit); } public static function lookupOrCreatePaths(array $paths) { @@ -87,7 +94,7 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker PhabricatorRepositoryCommit::IMPORTED_CHANGE); id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($commit->getPHID()); + ->queueDocumentForIndexing($commit->getPHID()); PhabricatorOwnersPackagePathValidator::updateOwnersPackagePaths($commit); if ($this->shouldQueueFollowupTasks()) { @@ -99,4 +106,52 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker } } + private function writeCommitChanges( + PhabricatorRepository $repository, + PhabricatorRepositoryCommit $commit, + array $changes) { + + $repository_id = (int)$repository->getID(); + $commit_id = (int)$commit->getID(); + + // NOTE: This SQL is being built manually instead of with qsprintf() + // because some SVN changes affect an enormous number of paths (millions) + // and this showed up as significantly slow on a profile at some point. + + $changes_sql = array(); + foreach ($changes as $change) { + $values = array( + $repository_id, + (int)$change->getPathID(), + $commit_id, + nonempty((int)$change->getTargetPathID(), 'null'), + nonempty((int)$change->getTargetCommitID(), 'null'), + (int)$change->getChangeType(), + (int)$change->getFileType(), + (int)$change->getIsDirect(), + (int)$change->getCommitSequence(), + ); + $changes_sql[] = '('.implode(', ', $values).')'; + } + + $conn_w = $repository->establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE commitID = %d', + PhabricatorRepository::TABLE_PATHCHANGE, + $commit_id); + + foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) { + queryfx( + $conn_w, + 'INSERT INTO %T + (repositoryID, pathID, commitID, targetPathID, targetCommitID, + changeType, fileType, isDirect, commitSequence) + VALUES %Q', + PhabricatorRepository::TABLE_PATHCHANGE, + $chunk); + } + } + } diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php index da2cdbf18f..fb191631b9 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php @@ -228,43 +228,21 @@ final class PhabricatorRepositoryGitCommitChangeParserWorker } } - $conn_w = $repository->establishConnection('w'); - - $changes_sql = array(); + $results = array(); foreach ($changes as $change) { - $values = array( - (int)$change['repositoryID'], - (int)$change['pathID'], - (int)$change['commitID'], - $change['targetPathID'] - ? (int)$change['targetPathID'] - : 'null', - $change['targetCommitID'] - ? (int)$change['targetCommitID'] - : 'null', - (int)$change['changeType'], - (int)$change['fileType'], - (int)$change['isDirect'], - (int)$change['commitSequence'], - ); - $changes_sql[] = '('.implode(', ', $values).')'; + $result = id(new PhabricatorRepositoryParsedChange()) + ->setPathID($change['pathID']) + ->setTargetPathID($change['targetPathID']) + ->setTargetCommitID($change['targetCommitID']) + ->setChangeType($change['changeType']) + ->setFileType($change['fileType']) + ->setIsDirect($change['isDirect']) + ->setCommitSequence($change['commitSequence']); + + $results[] = $result; } - queryfx( - $conn_w, - 'DELETE FROM %T WHERE commitID = %d', - PhabricatorRepository::TABLE_PATHCHANGE, - $commit->getID()); - foreach (array_chunk($changes_sql, 256) as $sql_chunk) { - queryfx( - $conn_w, - 'INSERT INTO %T - (repositoryID, pathID, commitID, targetPathID, targetCommitID, - changeType, fileType, isDirect, commitSequence) - VALUES %Q', - PhabricatorRepository::TABLE_PATHCHANGE, - implode(', ', $sql_chunk)); - } + return $results; } } diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php index 14360b6f7f..0fdd18b20f 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php @@ -263,43 +263,21 @@ final class PhabricatorRepositoryMercurialCommitChangeParserWorker } } - $conn_w = $repository->establishConnection('w'); - - $changes_sql = array(); + $results = array(); foreach ($changes as $change) { - $values = array( - (int)$change['repositoryID'], - (int)$change['pathID'], - (int)$change['commitID'], - $change['targetPathID'] - ? (int)$change['targetPathID'] - : 'null', - $change['targetCommitID'] - ? (int)$change['targetCommitID'] - : 'null', - (int)$change['changeType'], - (int)$change['fileType'], - (int)$change['isDirect'], - (int)$change['commitSequence'], - ); - $changes_sql[] = '('.implode(', ', $values).')'; + $result = id(new PhabricatorRepositoryParsedChange()) + ->setPathID($change['pathID']) + ->setTargetPathID($change['targetPathID']) + ->setTargetCommitID($change['targetCommitID']) + ->setChangeType($change['changeType']) + ->setFileType($change['fileType']) + ->setIsDirect($change['isDirect']) + ->setCommitSequence($change['commitSequence']); + + $results[] = $result; } - queryfx( - $conn_w, - 'DELETE FROM %T WHERE commitID = %d', - PhabricatorRepository::TABLE_PATHCHANGE, - $commit->getID()); - foreach (array_chunk($changes_sql, 256) as $sql_chunk) { - queryfx( - $conn_w, - 'INSERT INTO %T - (repositoryID, pathID, commitID, targetPathID, targetCommitID, - changeType, fileType, isDirect, commitSequence) - VALUES %Q', - PhabricatorRepository::TABLE_PATHCHANGE, - implode(', ', $sql_chunk)); - } + return $results; } private function mercurialPathExists( diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php index 386458c7b0..5ac6752a17 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php @@ -24,12 +24,12 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker // recursive paths were affected if it was moved or copied. This is very // complicated and has many special cases. - $uri = $repository->getDetail('remote-uri'); + $uri = $repository->getSubversionPathURI(); $svn_commit = $commit->getCommitIdentifier(); // Pull the top-level path changes out of "svn log". This is pretty // straightforward; just parse the XML log. - $log = $this->getSVNLogXMLObject($uri, $svn_commit, $verbose = true); + $log = $this->getSVNLogXMLObject($repository, $uri, $svn_commit); $entry = $log->logentry[0]; @@ -37,7 +37,7 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker // TODO: Explicitly mark this commit as broken elsewhere? This isn't // supposed to happen but we have some cases like rE27 and rG935 in the // Facebook repositories where things got all clowned up. - return; + return array(); } $raw_paths = array(); @@ -357,56 +357,50 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker $path_map = $this->lookupOrCreatePaths($lookup_paths); $commit_map = $this->lookupSvnCommits($repository, $lookup_commits); - $this->writeChanges($repository, $commit, $effects, $path_map, $commit_map); $this->writeBrowse($repository, $commit, $effects, $path_map); + + return $this->buildChanges( + $repository, + $commit, + $effects, + $path_map, + $commit_map); } - private function writeChanges( + private function buildChanges( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $effects, array $path_map, array $commit_map) { - $conn_w = $repository->establishConnection('w'); - - $sql = array(); + $results = array(); foreach ($effects as $effect) { - $sql[] = qsprintf( - $conn_w, - '(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)', - $repository->getID(), - $path_map[$effect['rawPath']], - $commit->getID(), - $effect['rawTargetPath'] - ? $path_map[$effect['rawTargetPath']] - : null, - $effect['rawTargetCommit'] - ? $commit_map[$effect['rawTargetCommit']] - : null, - $effect['changeType'], - $effect['fileType'], - $effect['rawDirect'] - ? 1 - : 0, - $commit->getCommitIdentifier()); + $path_id = $path_map[$effect['rawPath']]; + + $target_path_id = null; + if ($effect['rawTargetPath']) { + $target_path_id = $path_map[$effect['rawTargetPath']]; + } + + $target_commit_id = null; + if ($effect['rawTargetCommit']) { + $target_commit_id = $commit_map[$effect['rawTargetCommit']]; + } + + $result = id(new PhabricatorRepositoryParsedChange()) + ->setPathID($path_id) + ->setTargetPathID($target_path_id) + ->setTargetCommitID($target_commit_id) + ->setChangeType($effect['changeType']) + ->setFileType($effect['fileType']) + ->setIsDirect($effect['rawDirect']) + ->setCommitSequence($commit->getCommitIdentifier()); + + $results[] = $result; } - queryfx( - $conn_w, - 'DELETE FROM %T WHERE commitID = %d', - PhabricatorRepository::TABLE_PATHCHANGE, - $commit->getID()); - foreach (array_chunk($sql, 512) as $sql_chunk) { - queryfx( - $conn_w, - 'INSERT INTO %T - (repositoryID, pathID, commitID, targetPathID, targetCommitID, - changeType, fileType, isDirect, commitSequence) - VALUES %Q', - PhabricatorRepository::TABLE_PATHCHANGE, - implode(', ', $sql_chunk)); - } + return $results; } private function writeBrowse( @@ -561,7 +555,7 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker array $paths) { $result_map = array(); - $repository_uri = $repository->getDetail('remote-uri'); + $repository_uri = $repository->getSubversionPathURI(); if (isset($paths['/'])) { $result_map['/'] = DifferentialChangeType::FILE_DIRECTORY; @@ -572,9 +566,10 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker $path_mapping = array(); foreach ($paths as $path => $lookup) { $parent = dirname($lookup['rawPath']); - $parent = ltrim($parent, '/'); - $parent = $this->encodeSVNPath($parent); - $parent = $repository_uri.$parent.'@'.$lookup['rawCommit']; + $parent = $repository->getSubversionPathURI( + $parent, + $lookup['rawCommit']); + $parent = escapeshellarg($parent); $parents[$parent] = true; $path_mapping[$parent][] = dirname($path); @@ -626,12 +621,6 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker return $result_map; } - private function encodeSVNPath($path) { - $path = rawurlencode($path); - $path = str_replace('%2F', '/', $path); - return $path; - } - private function getFileTypeFromSVNKind($kind) { $kind = (string)$kind; switch ($kind) { @@ -648,9 +637,9 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker $path = $info['rawPath']; $rev = $info['rawCommit']; - $path = $this->encodeSVNPath($path); - $hashkey = md5($repository->getDetail('remote-uri').$path.'@'.$rev); + $path_uri = $repository->getSubversionPathURI($path, $rev); + $hashkey = md5($path_uri); // This method is quite horrible. The underlying challenge is that some // commits in the Facebook repository are enormous, taking multiple hours @@ -664,10 +653,8 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker if (!Filesystem::pathExists($cache_loc)) { $tmp = new TempFile(); $repository->execxRemoteCommand( - '--xml ls -R %s%s@%d > %s', - $repository->getDetail('remote-uri'), - $path, - $rev, + '--xml ls -R %s > %s', + $path_uri, $tmp); execx( 'mv %s %s', @@ -776,4 +763,20 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker return $parents; } + private function getSVNLogXMLObject( + PhabricatorRepository $repository, + $uri, + $revision) { + list($xml) = $repository->execxRemoteCommand( + 'log --xml --verbose --limit 1 %s@%d', + $uri, + $revision); + + // Subversion may send us back commit messages which won't parse because + // they have non UTF-8 garbage in them. Slam them into valid UTF-8. + $xml = phutil_utf8ize($xml); + + return new SimpleXMLElement($xml); + } + } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index cced97b33d..aa1295523d 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -3,14 +3,12 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker extends PhabricatorRepositoryCommitParserWorker { - abstract protected function getCommitHashes( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit); - - final protected function updateCommitData($author, $message, - $committer = null) { - + final protected function updateCommitData(DiffusionCommitRef $ref) { $commit = $this->commit; + $author = $ref->getAuthor(); + $message = $ref->getMessage(); + $committer = $ref->getCommitter(); + $hashes = $ref->getHashes(); $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', @@ -22,24 +20,21 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $data->setAuthorName($author); $data->setCommitDetail( 'authorPHID', - $this->resolveUserPHID($author)); + $this->resolveUserPHID($commit, $author)); $data->setCommitMessage($message); - if ($committer) { + if (strlen($committer)) { $data->setCommitDetail('committer', $committer); $data->setCommitDetail( 'committerPHID', - $this->resolveUserPHID($committer)); + $this->resolveUserPHID($commit, $committer)); } $repository = $this->repository; - $author_phid = $this->lookupUser( - $commit, - $data->getAuthorName(), - $data->getCommitDetail('authorPHID')); - $data->setCommitDetail('authorPHID', $author_phid); + $author_phid = $data->getCommitDetail('authorPHID'); + $committer_phid = $data->getCommitDetail('committerPHID'); $user = new PhabricatorUser(); if ($author_phid) { @@ -48,16 +43,11 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $author_phid); } - $call = new ConduitCall( - 'differential.parsecommitmessage', - array( - 'corpus' => $message, - 'partial' => true, - )); - $call->setUser(PhabricatorUser::getOmnipotentUser()); - $result = $call->execute(); - - $field_values = $result['fields']; + $field_values = id(new DiffusionLowLevelCommitFieldsQuery()) + ->setRepository($repository) + ->withCommitRef($ref) + ->execute(); + $revision_id = idx($field_values, 'revisionID'); if (!empty($field_values['reviewedByPHIDs'])) { $data->setCommitDetail( @@ -65,33 +55,7 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker reset($field_values['reviewedByPHIDs'])); } - $revision_id = idx($field_values, 'revisionID'); - if (!$revision_id) { - $hashes = $this->getCommitHashes( - $repository, - $commit); - if ($hashes) { - $revisions = id(new DifferentialRevisionQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withCommitHashes($hashes) - ->execute(); - - if (!empty($revisions)) { - $revision = $this->identifyBestRevision($revisions); - $revision_id = $revision->getID(); - } - } - } - - $data->setCommitDetail( - 'differential.revisionID', - $revision_id); - - $committer_phid = $this->lookupUser( - $commit, - $data->getCommitDetail('committer'), - $data->getCommitDetail('committerPHID')); - $data->setCommitDetail('committerPHID', $committer_phid); + $data->setCommitDetail('differential.revisionID', $revision_id); if ($author_phid != $commit->getAuthorPHID()) { $commit->setAuthorPHID($author_phid); @@ -241,15 +205,20 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker DifferentialRevision $revision, $actor_phid) { + $viewer = PhabricatorUser::getOmnipotentUser(); + $drequest = DiffusionRequest::newFromDictionary(array( - 'user' => PhabricatorUser::getOmnipotentUser(), - 'initFromConduit' => false, + 'user' => $viewer, 'repository' => $this->repository, - 'commit' => $this->commit->getCommitIdentifier(), )); - $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) - ->loadRawDiff(); + $raw_diff = DiffusionQuery::callConduitWithDiffusionRequest( + $viewer, + $drequest, + 'diffusion.rawdiffquery', + array( + 'commit' => $this->commit->getCommitIdentifier(), + )); // TODO: Support adds, deletes and moves under SVN. if (strlen($raw_diff)) { @@ -284,10 +253,15 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $diff->setArcanistProjectPHID($arcanist_project->getPHID()); } - $parents = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest) - ->loadParents(); + $parents = DiffusionQuery::callConduitWithDiffusionRequest( + $viewer, + $drequest, + 'diffusion.commitparentsquery', + array( + 'commit' => $this->commit->getCommitIdentifier(), + )); if ($parents) { - $diff->setSourceControlBaseRevision(head_key($parents)); + $diff->setSourceControlBaseRevision(head($parents)); } // TODO: Attach binary files. @@ -398,157 +372,14 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker return null; } - /** - * When querying for revisions by hash, more than one revision may be found. - * This function identifies the "best" revision from such a set. Typically, - * there is only one revision found. Otherwise, we try to pick an accepted - * revision first, followed by an open revision, and otherwise we go with a - * closed or abandoned revision as a last resort. - */ - private function identifyBestRevision(array $revisions) { - assert_instances_of($revisions, 'DifferentialRevision'); - // get the simplest, common case out of the way - if (count($revisions) == 1) { - return reset($revisions); - } - - $first_choice = array(); - $second_choice = array(); - $third_choice = array(); - foreach ($revisions as $revision) { - switch ($revision->getStatus()) { - // "Accepted" revisions -- ostensibly what we're looking for! - case ArcanistDifferentialRevisionStatus::ACCEPTED: - $first_choice[] = $revision; - break; - // "Open" revisions - case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: - case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: - $second_choice[] = $revision; - break; - // default is a wtf? here - default: - case ArcanistDifferentialRevisionStatus::ABANDONED: - case ArcanistDifferentialRevisionStatus::CLOSED: - $third_choice[] = $revision; - break; - } - } - - // go down the ladder like a bro at last call - if (!empty($first_choice)) { - return $this->identifyMostRecentRevision($first_choice); - } - if (!empty($second_choice)) { - return $this->identifyMostRecentRevision($second_choice); - } - if (!empty($third_choice)) { - return $this->identifyMostRecentRevision($third_choice); - } - } - - /** - * Given a set of revisions, returns the revision with the latest - * updated time. This is ostensibly the most recent revision. - */ - private function identifyMostRecentRevision(array $revisions) { - assert_instances_of($revisions, 'DifferentialRevision'); - $revisions = msort($revisions, 'getDateModified'); - return end($revisions); - } - - /** - * Emit an event so installs can do custom lookup of commit authors who may - * not be naturally resolvable. - */ - private function lookupUser( + private function resolveUserPHID( PhabricatorRepositoryCommit $commit, - $query, - $guess) { + $user_name) { - $type = PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER; - $data = array( - 'commit' => $commit, - 'query' => $query, - 'result' => $guess, - ); - - $event = new PhabricatorEvent($type, $data); - PhutilEventEngine::dispatchEvent($event); - - return $event->getValue('result'); - } - - private function resolveUserPHID($user_name) { - if (!strlen($user_name)) { - return null; - } - - $phid = $this->findUserByUserName($user_name); - if ($phid) { - return $phid; - } - $phid = $this->findUserByEmailAddress($user_name); - if ($phid) { - return $phid; - } - $phid = $this->findUserByRealName($user_name); - if ($phid) { - return $phid; - } - - // No hits yet, try to parse it as an email address. - - $email = new PhutilEmailAddress($user_name); - - $phid = $this->findUserByEmailAddress($email->getAddress()); - if ($phid) { - return $phid; - } - - $display_name = $email->getDisplayName(); - if ($display_name) { - $phid = $this->findUserByUserName($display_name); - if ($phid) { - return $phid; - } - $phid = $this->findUserByRealName($display_name); - if ($phid) { - return $phid; - } - } - - return null; - } - - private function findUserByUserName($user_name) { - $by_username = id(new PhabricatorUser())->loadOneWhere( - 'userName = %s', - $user_name); - if ($by_username) { - return $by_username->getPHID(); - } - return null; - } - - private function findUserByRealName($real_name) { - // Note, real names are not guaranteed unique, which is why we do it this - // way. - $by_realname = id(new PhabricatorUser())->loadAllWhere( - 'realName = %s', - $real_name); - if (count($by_realname) == 1) { - return reset($by_realname)->getPHID(); - } - return null; - } - - private function findUserByEmailAddress($email_address) { - $by_email = PhabricatorUser::loadOneWithEmailAddress($email_address); - if ($by_email) { - return $by_email->getPHID(); - } - return null; + return id(new DiffusionResolveUserQuery()) + ->withCommit($commit) + ->withName($user_name) + ->execute(); } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php index 213b32e3fc..62c7572678 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php @@ -7,50 +7,12 @@ final class PhabricatorRepositoryGitCommitMessageParserWorker PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { - // NOTE: %B was introduced somewhat recently in git's history, so pull - // commit message information with %s and %b instead. - // Even though we pass --encoding here, git doesn't always succeed, so - // we try a little harder, since git *does* tell us what the actual encoding - // is correctly (unless it doesn't; encoding is sometimes empty). - list($info) = $repository->execxLocalCommand( - 'log -n 1 --encoding=%s --format=%s %s --', - 'UTF-8', - implode('%x00', array('%e', '%cn', '%ce', '%an', '%ae', '%s%n%n%b')), - $commit->getCommitIdentifier()); + $ref = id(new DiffusionLowLevelCommitQuery()) + ->setRepository($repository) + ->withIdentifier($commit->getCommitIdentifier()) + ->execute(); - $parts = explode("\0", $info); - $encoding = array_shift($parts); - - foreach ($parts as $key => $part) { - if ($encoding) { - $part = phutil_utf8_convert($part, 'UTF-8', $encoding); - } - $parts[$key] = phutil_utf8ize($part); - } - - $committer_name = $parts[0]; - $committer_email = $parts[1]; - $author_name = $parts[2]; - $author_email = $parts[3]; - $message = $parts[4]; - - if (strlen($author_email)) { - $author = "{$author_name} <{$author_email}>"; - } else { - $author = "{$author_name}"; - } - - if (strlen($committer_email)) { - $committer = "{$committer_name} <{$committer_email}>"; - } else { - $committer = "{$committer_name}"; - } - - if ($committer == $author) { - $committer = null; - } - - $this->updateCommitData($author, $message, $committer); + $this->updateCommitData($ref); if ($this->shouldQueueFollowupTasks()) { PhabricatorWorker::scheduleTask( @@ -61,24 +23,4 @@ final class PhabricatorRepositoryGitCommitMessageParserWorker } } - protected function getCommitHashes( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit) { - - list($stdout) = $repository->execxLocalCommand( - 'log -n 1 --format=%s %s --', - '%T', - $commit->getCommitIdentifier()); - - $commit_hash = $commit->getCommitIdentifier(); - $tree_hash = trim($stdout); - - return array( - array(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, - $commit_hash), - array(ArcanistDifferentialRevisionHash::HASH_GIT_TREE, - $tree_hash), - ); - } - } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php index 106570a419..dedf14db82 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php @@ -7,18 +7,12 @@ final class PhabricatorRepositoryMercurialCommitMessageParserWorker PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { - list($stdout) = $repository->execxLocalCommand( - 'log --template %s --rev %s', - '{author}\\n{desc}', - $commit->getCommitIdentifier()); + $ref = id(new DiffusionLowLevelCommitQuery()) + ->setRepository($repository) + ->withIdentifier($commit->getCommitIdentifier()) + ->execute(); - list($author, $message) = explode("\n", $stdout, 2); - - $author = phutil_utf8ize($author); - $message = phutil_utf8ize($message); - $message = trim($message); - - $this->updateCommitData($author, $message); + $this->updateCommitData($ref); if ($this->shouldQueueFollowupTasks()) { PhabricatorWorker::scheduleTask( @@ -29,16 +23,4 @@ final class PhabricatorRepositoryMercurialCommitMessageParserWorker } } - protected function getCommitHashes( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit) { - - $commit_hash = $commit->getCommitIdentifier(); - - return array( - array(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, - $commit_hash), - ); - } - } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php index ea5bc37606..d15613850d 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php @@ -7,19 +7,12 @@ final class PhabricatorRepositorySvnCommitMessageParserWorker PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { - $uri = $repository->getDetail('remote-uri'); + $ref = id(new DiffusionLowLevelCommitQuery()) + ->setRepository($repository) + ->withIdentifier($commit->getCommitIdentifier()) + ->execute(); - $log = $this->getSVNLogXMLObject( - $uri, - $commit->getCommitIdentifier(), - $verbose = false); - - $entry = $log->logentry[0]; - - $author = (string)$entry->author; - $message = (string)$entry->msg; - - $this->updateCommitData($author, $message); + $this->updateCommitData($ref); if ($this->shouldQueueFollowupTasks()) { PhabricatorWorker::scheduleTask( @@ -30,10 +23,4 @@ final class PhabricatorRepositorySvnCommitMessageParserWorker } } - protected function getCommitHashes( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit) { - return array(); - } - } diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index c1e37603fa..b1b8504dcf 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -160,6 +160,10 @@ $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); } else { $saved_query = $engine->buildSavedQueryFromRequest($request); + + // Save the query to generate a query key, so "Save Custom Query..." and + // other features like Maniphest's "Export..." work correctly. + $this->saveQuery($saved_query); } $nav->selectFilter( @@ -216,7 +220,16 @@ $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); - $pager->setPageSize($engine->getPageSize($saved_query)); + $page_size = $engine->getPageSize($saved_query); + if (is_finite($page_size)) { + $pager->setPageSize($page_size); + } else { + // Consider an INF pagesize to mean a large finite pagesize. + + // TODO: It would be nice to handle this more gracefully, but math + // with INF seems to vary across PHP versions, systems, and runtimes. + $pager->setPageSize(0xFFFF); + } $objects = $query->setViewer($request->getUser()) ->executeWithCursorPager($pager); @@ -252,9 +265,7 @@ $crumbs = $parent ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Search"))); + ->addTextCrumb(pht("Search")); $nav->setCrumbs($crumbs); @@ -336,10 +347,7 @@ $crumbs = $parent ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Saved Queries")) - ->setHref($engine->getQueryManagementURI())); + ->addTextCrumb(pht("Saved Queries"), $engine->getQueryManagementURI()); $nav->selectFilter('query/edit'); $nav->setCrumbs($crumbs); diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php index f9296b8a7b..02f615c380 100644 --- a/src/applications/search/controller/PhabricatorSearchController.php +++ b/src/applications/search/controller/PhabricatorSearchController.php @@ -252,18 +252,17 @@ final class PhabricatorSearchController $results[] = $view->render(); } - $results = hsprintf( - '
    '. - '%s'. - '
    %s
    '. - '
    ', + $results = phutil_tag_div('phabricator-search-result-list', array( phutil_implode_html("\n", $results), - $pager->render()); + phutil_tag_div('search-results-pager', $pager->render()), + )); } else { - $results = hsprintf( - '
    '. - '

    No search results.

    '. - '
    '); + $results = phutil_tag_div( + 'phabricator-search-result-list', + phutil_tag( + 'p', + array('class' => 'phabricator-search-no-results'), + pht('No search results.'))); } $results = id(new PHUIBoxView()) ->addMargin(PHUI::MARGIN_LARGE) @@ -276,9 +275,7 @@ final class PhabricatorSearchController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Search'))); + $crumbs->addTextCrumb(pht('Search')); return $this->buildApplicationPage( array( diff --git a/src/applications/search/controller/PhabricatorSearchEditController.php b/src/applications/search/controller/PhabricatorSearchEditController.php index 4d84af847e..8c18f5ab59 100644 --- a/src/applications/search/controller/PhabricatorSearchEditController.php +++ b/src/applications/search/controller/PhabricatorSearchEditController.php @@ -66,11 +66,6 @@ final class PhabricatorSearchEditController } } - if ($errors) { - $errors = id(new AphrontErrorView()) - ->setErrors($errors); - } - $form = id(new AphrontFormView()) ->setUser($user); @@ -94,13 +89,11 @@ final class PhabricatorSearchEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($errors) + ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 3011671e3a..acdf9d18af 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -107,7 +107,6 @@ abstract class PhabricatorApplicationSearchEngine { ->setEngineClassName(get_class($this)); } - public function addNavigationItems(PHUIListView $menu) { $viewer = $this->requireViewer(); @@ -263,10 +262,8 @@ abstract class PhabricatorApplicationSearchEngine { AphrontRequest $request, $key, array $allow_types = array()) { - $list = $request->getArr($key, null); - if ($list === null) { - $list = $request->getStrList($key); - } + + $list = $this->readListFromRequest($request, $key); $phids = array(); $names = array(); @@ -274,7 +271,6 @@ abstract class PhabricatorApplicationSearchEngine { $user_type = PhabricatorPHIDConstants::PHID_TYPE_USER; foreach ($list as $item) { $type = phid_get_type($item); - phlog($type); if ($type == $user_type) { $phids[] = $item; } else if (isset($allow_types[$type])) { @@ -299,6 +295,79 @@ abstract class PhabricatorApplicationSearchEngine { } + /** + * Read a list of generic PHIDs from a request in a flexible way. Like + * @{method:readUsersFromRequest}, this method supports either array or + * comma-delimited forms. Objects can be specified either by PHID or by + * object name. + * + * @param AphrontRequest Request to read PHIDs from. + * @param string Key to read in the request. + * @param list Optional, list of permitted PHID types. + * @return list List of object PHIDs. + * + * @task read + */ + protected function readPHIDsFromRequest( + AphrontRequest $request, + $key, + array $allow_types = array()) { + + $list = $this->readListFromRequest($request, $key); + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->requireViewer()) + ->withNames($list) + ->execute(); + $list = mpull($objects, 'getPHID'); + + if (!$list) { + return array(); + } + + // If only certain PHID types are allowed, filter out all the others. + if ($allow_types) { + $allow_types = array_fuse($allow_types); + foreach ($list as $key => $phid) { + if (empty($allow_types[phid_get_type($phid)])) { + unset($list[$key]); + } + } + } + + return $list; + } + + + /** + * Read a list of items from the request, in either array format or string + * format: + * + * list[]=item1&list[]=item2 + * list=item1,item2 + * + * This provides flexibility when constructing URIs, especially from external + * sources. + * + * @param AphrontRequest Request to read PHIDs from. + * @param string Key to read in the request. + * @return list List of values. + */ + protected function readListFromRequest( + AphrontRequest $request, + $key) { + $list = $request->getArr($key, null); + if ($list === null) { + $list = $request->getStrList($key); + } + + if (!$list) { + return array(); + } + + return $list; + } + protected function readBoolFromRequest( AphrontRequest $request, $key) { diff --git a/src/applications/search/engine/PhabricatorJumpNavHandler.php b/src/applications/search/engine/PhabricatorJumpNavHandler.php index b0e80962f0..e3081dfebd 100644 --- a/src/applications/search/engine/PhabricatorJumpNavHandler.php +++ b/src/applications/search/engine/PhabricatorJumpNavHandler.php @@ -21,6 +21,7 @@ final class PhabricatorJumpNavHandler { '/^@(.+)$/i' => 'user', '/^task:\s*(.+)/i' => 'create-task', '/^(?:s|symbol)\s+(\S+)/i' => 'find-symbol', + '/^r\s+(.+)$/i' => 'find-repository', ); foreach ($patterns as $pattern => $effect) { @@ -53,6 +54,20 @@ final class PhabricatorJumpNavHandler { } return id(new AphrontRedirectResponse()) ->setURI("/diffusion/symbol/$symbol/?jump=true$context"); + case 'find-repository': + $name = $matches[1]; + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withNameContains($name) + ->execute(); + if (count($repositories) == 1) { + // Just one match, jump to repository. + $uri = '/diffusion/'.head($repositories)->getCallsign().'/'; + } else { + // More than one match, jump to search. + $uri = urisprintf('/diffusion/?order=name&name=%s', $name); + } + return id(new AphrontRedirectResponse())->setURI($uri); case 'create-task': return id(new AphrontRedirectResponse()) ->setURI('/maniphest/task/create/?title=' diff --git a/src/applications/search/index/PhabricatorSearchIndexer.php b/src/applications/search/index/PhabricatorSearchIndexer.php index b6c2b13efb..2f67a10b93 100644 --- a/src/applications/search/index/PhabricatorSearchIndexer.php +++ b/src/applications/search/index/PhabricatorSearchIndexer.php @@ -1,10 +1,15 @@ $phid, + )); + } + public function indexDocumentByPHID($phid) { $doc_indexer_symbols = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorSearchDocumentIndexer') diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php index eddc311d38..dd608cd444 100644 --- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php @@ -28,13 +28,8 @@ final class PhabricatorSearchManagementIndexWorkflow array( 'name' => 'background', 'help' => 'Instead of indexing in this process, queue tasks for '. - 'the daemons. This is better if you are indexing a lot '. - 'of stuff, but less helpful for debugging.', - ), - array( - 'name' => 'foreground', - 'help' => 'Index in this process, even if there are many objects '. - 'to index. This is helpful for debugging.', + 'the daemons. This can improve performance, but makes '. + 'it more difficult to debug search indexing.', ), array( 'name' => 'objects', @@ -51,7 +46,6 @@ final class PhabricatorSearchManagementIndexWorkflow $obj_names = $args->getArg('objects'); - if ($obj_names && ($is_all || $is_type)) { throw new PhutilArgumentUsageException( "You can not name objects to index alongside the '--all' or '--type' ". @@ -72,19 +66,31 @@ final class PhabricatorSearchManagementIndexWorkflow "Nothing to index!"); } + if ($args->getArg('background')) { + $is_background = true; + } else { + PhabricatorWorker::setRunAllTasksInProcess(true); + $is_background = false; + } + $groups = phid_group_by_type($phids); foreach ($groups as $group_type => $group) { $console->writeOut( + "%s\n", pht( "Indexing %d object(s) of type %s.", count($group), - $group_type)."\n"); + $group_type)); } $indexer = new PhabricatorSearchIndexer(); foreach ($phids as $phid) { - $indexer->indexDocumentByPHID($phid); - $console->writeOut(pht("Indexing '%s'...\n", $phid)); + if ($is_background) { + $console->writeOut("%s\n", pht("Queueing '%s'...", $phid)); + } else { + $console->writeOut("%s\n", pht("Indexing '%s'...", $phid)); + } + $indexer->queueDocumentForIndexing($phid); } $console->writeOut("Done.\n"); @@ -92,7 +98,7 @@ final class PhabricatorSearchManagementIndexWorkflow private function loadPHIDsByNames(array $names) { $query = id(new PhabricatorObjectQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($this->getViewer()) ->withNames($names); $query->execute(); $objects = $query->getNamedResults(); diff --git a/src/applications/search/management/PhabricatorSearchManagementWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementWorkflow.php index 4f7fad9ef3..00c3399610 100644 --- a/src/applications/search/management/PhabricatorSearchManagementWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementWorkflow.php @@ -1,13 +1,6 @@ getFullName(); + $title = $this->emboldenQuery($handle->getFullName()); + if ($handle->getStatus() == PhabricatorObjectHandleStatus::STATUS_CLOSED) { + $title = phutil_tag('del', array(), $title); + } return hsprintf( '
    '. @@ -71,7 +74,7 @@ final class PhabricatorSearchResultView extends AphrontView { 'class' => 'result-name', 'href' => $handle->getURI(), ), - $this->emboldenQuery($object_name)), + $title), $type_name, $link); } diff --git a/src/applications/search/worker/PhabricatorSearchWorker.php b/src/applications/search/worker/PhabricatorSearchWorker.php new file mode 100644 index 0000000000..15179da6e5 --- /dev/null +++ b/src/applications/search/worker/PhabricatorSearchWorker.php @@ -0,0 +1,13 @@ +getTaskData(); + $phid = idx($data, 'documentPHID'); + + id(new PhabricatorSearchIndexer()) + ->indexDocumentByPHID($phid); + } + +} diff --git a/src/applications/settings/application/PhabricatorApplicationSettings.php b/src/applications/settings/application/PhabricatorApplicationSettings.php index 6a7d6096ff..40950dedbf 100644 --- a/src/applications/settings/application/PhabricatorApplicationSettings.php +++ b/src/applications/settings/application/PhabricatorApplicationSettings.php @@ -37,14 +37,15 @@ final class PhabricatorApplicationSettings extends PhabricatorApplication { $items = array(); - if ($user->isLoggedIn()) { + if ($user->isLoggedIn() && $user->isUserActivated()) { $selected = ($controller instanceof PhabricatorSettingsMainController); - $item = new PHUIListItemView(); - $item->setName(pht('Settings')); - $item->setIcon('settings'); - $item->addClass('core-menu-item'); - $item->setSelected($selected); - $item->setHref('/settings/'); + $item = id(new PHUIListItemView()) + ->setName(pht('Settings')) + ->setIcon('settings-sm') + ->addClass('core-menu-item') + ->setSelected($selected) + ->setHref('/settings/') + ->setOrder(400); $items[] = $item; } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php b/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php index 2a2443aeac..0c291f2996 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php @@ -51,21 +51,6 @@ final class PhabricatorSettingsPanelAccount } } - $notice = null; - if (!$errors) { - if ($request->getStr('saved')) { - $notice = new AphrontErrorView(); - $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); - $notice->setTitle(pht('Changes Saved')); - $notice->appendChild( - phutil_tag('p', array(), pht('Your changes have been saved.'))); - $notice = $notice->render(); - } - } else { - $notice = new AphrontErrorView(); - $notice->setErrors($errors); - } - $timezone_ids = DateTimeZone::listIdentifiers(); $timezone_id_map = array_fuse($timezone_ids); @@ -139,10 +124,11 @@ final class PhabricatorSettingsPanelAccount $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Account Settings')) + ->setFormSaved($request->getStr('saved')) + ->setFormErrors($errors) ->setForm($form); return array( - $notice, $form_box, ); } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php b/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php index eb79fa2c5a..4645c04d56 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php @@ -34,13 +34,14 @@ final class PhabricatorSettingsPanelConduit ->setDialog($dialog); } - $conn = $user->establishConnection('w'); - queryfx( - $conn, - 'DELETE FROM %T WHERE userPHID = %s AND type LIKE %>', - PhabricatorUser::SESSION_TABLE, - $user->getPHID(), - 'conduit'); + $sessions = id(new PhabricatorAuthSessionQuery()) + ->setViewer($user) + ->withIdentityPHIDs(array($user->getPHID())) + ->withSessionTypes(array(PhabricatorAuthSession::TYPE_CONDUIT)) + ->execute(); + foreach ($sessions as $session) { + $session->delete(); + } // This implicitly regenerates the certificate. $user->setConduitCertificate(null); @@ -64,18 +65,23 @@ final class PhabricatorSettingsPanelConduit $notice = null; } + Javelin::initBehavior('select-on-click'); + $cert_form = new AphrontFormView(); $cert_form ->setUser($user) - ->appendChild(hsprintf( - '

    %s

    ', + ->appendChild(phutil_tag( + 'p', + array('class' => 'aphront-form-instructions'), pht('This certificate allows you to authenticate over Conduit, '. 'the Phabricator API. Normally, you just run %s to install it.', - hsprintf('%s', 'arc install-certificate')))) + phutil_tag('tt', array(), 'arc install-certificate')))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Certificate')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT) + ->setReadonly(true) + ->setSigil('select-on-click') ->setValue($user->getConduitCertificate())); $cert_form = id(new PHUIObjectBoxView()) @@ -90,8 +96,10 @@ final class PhabricatorSettingsPanelConduit ->setUser($user) ->setAction($this->getPanelURI()) ->setWorkflow(true) - ->appendChild(hsprintf( - '

    %s

    ', $regen_instruction)) + ->appendChild(phutil_tag( + 'p', + array('class' => 'aphront-form-instructions'), + $regen_instruction)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Regenerate Certificate'))); diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php index 0ebd964dae..95c19d9d5e 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php @@ -56,18 +56,10 @@ final class PhabricatorSettingsPanelConpherencePreferences id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); - $error_view = null; - if ($request->getBool('saved')) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Preferences Saved')) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors(array(pht('Your preferences have been saved.'))); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Conpherence Preferences')) - ->setFormError($error_view) - ->setForm($form); + ->setForm($form) + ->setFormSaved($request->getBool('saved')); return array( $form_box, diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php index e0de6482aa..3c23cc05a8 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php @@ -85,17 +85,9 @@ final class PhabricatorSettingsPanelDeveloperPreferences id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); - $error_view = null; - if ($request->getBool('saved')) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Preferences Saved')) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors(array(pht('Your preferences have been saved.'))); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Developer Settings')) - ->setFormError($error_view) + ->setFormSaved($request->getBool('saved')) ->setForm($form); return array( diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php index efbb4ffd56..26115e3e77 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php @@ -58,17 +58,9 @@ final class PhabricatorSettingsPanelDiffPreferences id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); - $error_view = null; - if ($request->getBool('saved')) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Preferences Saved')) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors(array(pht('Your preferences have been saved.'))); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Diff Preferences')) - ->setFormError($error_view) + ->setFormSaved($request->getBool('saved')) ->setForm($form); return array( diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php index 9a6100f467..c97f9d6046 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php @@ -73,7 +73,7 @@ EXAMPLE; $editor_instructions = pht('Link to edit files in external editor. '. '%%f is replaced by filename, %%l by line number, %%r by repository '. 'callsign, %%%% by literal %%. For documentation, see: %s', - hsprintf('%s', $editor_doc_link)); + $editor_doc_link); $form = id(new AphrontFormView()) ->setUser($user) @@ -137,17 +137,9 @@ EXAMPLE; id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); - $error_view = null; - if ($request->getStr('saved') === 'true') { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Preferences Saved')) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors(array(pht('Your preferences have been saved.'))); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Display Preferences')) - ->setFormError($error_view) + ->setFormSaved($request->getStr('saved') === 'true') ->setForm($form); return array( diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php index 02495e88bf..7f166a3540 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php @@ -130,21 +130,25 @@ final class PhabricatorSettingsPanelEmailAddresses $editable, )); - $view = new AphrontPanelView(); + $view = new PHUIObjectBoxView(); + $header = new PHUIHeaderView(); + $header->setHeader(pht('Email Addresses')); + if ($editable) { - $view->addButton( - javelin_tag( - 'a', - array( - 'href' => $uri->alter('new', 'true'), - 'class' => 'green button', - 'sigil' => 'workflow', - ), - pht('Add New Address'))); + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('new'); + + $button = new PHUIButtonView(); + $button->setText(pht('Add New Address')); + $button->setTag('a'); + $button->setHref($uri->alter('new', 'true')); + $button->setIcon($icon); + $button->addSigil('workflow'); + $header->addActionLink($button); } - $view->setHeader(pht('Email Addresses')); + $view->setHeader($header); $view->appendChild($table); - $view->setNoBackground(); return $view; } @@ -301,9 +305,9 @@ final class PhabricatorSettingsPanelEmailAddresses ->setUser($user) ->addHiddenInput('verify', $email_id) ->setTitle(pht("Send Another Verification Email?")) - ->appendChild(hsprintf( - '

    %s

    ', - pht('Send another copy of the verification email to %s?', $address))) + ->appendChild(phutil_tag('p', array(), pht( + 'Send another copy of the verification email to %s?', + $address))) ->addSubmitButton(pht('Send Email')) ->addCancelButton($uri); @@ -342,10 +346,10 @@ final class PhabricatorSettingsPanelEmailAddresses ->setUser($user) ->addHiddenInput('primary', $email_id) ->setTitle(pht("Change primary email address?")) - ->appendChild(hsprintf( - '

    If you change your primary address, Phabricator will send'. - ' all email to %s.

    ', - $address)) + ->appendChild(phutil_tag('p', array(), pht( + 'If you change your primary address, Phabricator will send'. + ' all email to %s.', + $address))) ->addSubmitButton(pht('Change Primary Address')) ->addCancelButton($uri); diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php index f3045cc3ef..f38d6c0cb5 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php @@ -69,21 +69,6 @@ final class PhabricatorSettingsPanelEmailPreferences ->setURI($this->getPanelURI('?saved=true')); } - $notice = null; - if (!$errors) { - if ($request->getStr('saved')) { - $notice = new AphrontErrorView(); - $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); - $notice->setTitle(pht('Changes Saved')); - $notice->appendChild( - phutil_tag('p', array(), pht('Your changes have been saved.'))); - } - } else { - $notice = new AphrontErrorView(); - $notice->setTitle(pht('Form Errors')); - $notice->setErrors($errors); - } - $re_prefix_default = PhabricatorEnv::getEnvConfig('metamta.re-prefix') ? pht('Enabled') : pht('Disabled'); @@ -218,7 +203,8 @@ final class PhabricatorSettingsPanelEmailPreferences $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Email Preferences')) - ->setFormError($notice) + ->setFormSaved($request->getStr('saved')) + ->setFormErrors($errors) ->setForm($form); return id(new AphrontNullView()) diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php b/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php index 3411ef5ec5..18030c35c7 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php @@ -35,6 +35,8 @@ final class PhabricatorSettingsPanelExternalAccounts $linked = id(new PHUIObjectItemListView()) ->setUser($viewer) + ->setCards(true) + ->setFlush(true) ->setNoDataString(pht('You have no linked accounts.')); $login_accounts = 0; @@ -97,6 +99,8 @@ final class PhabricatorSettingsPanelExternalAccounts $linkable = id(new PHUIObjectItemListView()) ->setUser($viewer) + ->setCards(true) + ->setFlush(true) ->setNoDataString( pht('Your account is linked with all available providers.')); @@ -126,11 +130,17 @@ final class PhabricatorSettingsPanelExternalAccounts $linkable->addItem($item); } + $linked_box = id(new PHUIObjectBoxView()) + ->setHeader($linked_head) + ->appendChild($linked); + + $linkable_box = id(new PHUIObjectBoxView()) + ->setHeader($linkable_head) + ->appendChild($linkable); + return array( - $linked_head, - $linked, - $linkable_head, - $linkable, + $linked_box, + $linkable_box, ); } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php index 13a46ba0ef..82a7b13a55 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php @@ -140,9 +140,10 @@ final class PhabricatorSettingsPanelHomePreferences 'checked' => $full_radio_button_status, )); + $desc = $app->getShortDescription(); $app_column = hsprintf( - "%s
    Default: %s" - , $app->getName(), $default_name); + "%s
    %s, Default: %s", + $app->getName(), $desc, $default_name); $rows[] = array( $app_column, @@ -179,20 +180,23 @@ final class PhabricatorSettingsPanelHomePreferences )); - $panel = id(new AphrontPanelView()) - ->setHeader($group_name) - ->addClass('phabricator-settings-panelview') - ->appendChild($table) - ->setNoBackground(); + $panel = id(new PHUIObjectBoxView()) + ->setHeaderText($group_name) + ->appendChild($table); $output[] = $panel; } - $form - ->appendChild($output) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Preferences'))); + $save_button = + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save Preferences')); + + $output[] = id(new PHUIBoxView()) + ->addPadding(PHUI::PADDING_LARGE) + ->addClass('phabricator-settings-homepagetable-button') + ->appendChild($save_button); + + $form->appendChild($output); $error_view = null; if ($request->getStr('saved') === 'true') { @@ -202,12 +206,14 @@ final class PhabricatorSettingsPanelHomePreferences ->setErrors(array(pht('Your preferences have been saved.'))); } - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Home Page Preferences')) - ->setFormError($error_view) - ->setForm($form); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Home Page Preferences')); - return $form_box; + $form = id(new PHUIBoxView()) + ->addClass('phabricator-settings-homepagetable-wrap') + ->appendChild($form); + + return array($header, $error_view, $form); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php index 69e9170ed5..dcc4d293a9 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php @@ -75,11 +75,15 @@ final class PhabricatorSettingsPanelPassword if (strlen($pass) < $min_len) { $errors[] = pht('Your new password is too short.'); $e_new = pht('Too Short'); - } - - if ($pass !== $conf) { + } else if ($pass !== $conf) { $errors[] = pht('New password and confirmation do not match.'); $e_conf = pht('Invalid'); + } else if (PhabricatorCommonPasswords::isCommonPassword($pass)) { + $e_new = pht('Very Weak'); + $e_conf = pht('Very Weak'); + $errors[] = pht( + 'Your new password is very weak: it is one of the most common '. + 'passwords in use. Choose a stronger password.'); } if (!$errors) { @@ -108,21 +112,6 @@ final class PhabricatorSettingsPanelPassword } } - $notice = null; - if (!$errors) { - if ($request->getStr('saved')) { - $notice = new AphrontErrorView(); - $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); - $notice->setTitle(pht('Changes Saved')); - $notice->appendChild( - phutil_tag('p', array(), pht('Your password has been updated.'))); - } - } else { - $notice = new AphrontErrorView(); - $notice->setTitle(pht('Error Changing Password')); - $notice->setErrors($errors); - } - $len_caption = null; if ($min_len) { $len_caption = pht('Minimum password length: %d characters.', $min_len); @@ -161,7 +150,8 @@ final class PhabricatorSettingsPanelPassword $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Change Password')) - ->setFormError($notice) + ->setFormSaved($request->getStr('saved')) + ->setFormErrors($errors) ->setForm($form); return array( diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php index 908671669e..f89aeefd54 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php @@ -81,9 +81,20 @@ final class PhabricatorSettingsPanelSSHKeys if (!$errors) { list($type, $body, $comment) = $parts; - if (!preg_match('/^ssh-dsa|ssh-rsa$/', $type)) { + + $recognized_keys = array( + 'ssh-dsa', + 'ssh-dss', + 'ssh-rsa', + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + ); + + if (!in_array($type, $recognized_keys)) { $e_key = pht('Invalid'); - $errors[] = pht('Public key should be "ssh-dsa" or "ssh-rsa".'); + $type_list = implode(', ', $recognized_keys); + $errors[] = pht('Public key should be one of: %s', $type_list); } else { $key->setKeyType($type); $key->setKeyBody($body); @@ -115,13 +126,6 @@ final class PhabricatorSettingsPanelSSHKeys } } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } - $is_new = !$key->getID(); if ($is_new) { @@ -152,16 +156,12 @@ final class PhabricatorSettingsPanelSSHKeys ->addCancelButton($this->getPanelURI()) ->setValue($save)); - $header_title = new PHUIHeaderView(); - $header_title->setHeader($header); + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->setFormErrors($errors) + ->setForm($form); - return id(new AphrontNullView()) - ->appendChild( - array( - $error_view, - $header_title, - $form, - )); + return $form_box; } private function renderKeyListView(AphrontRequest $request) { @@ -217,18 +217,24 @@ final class PhabricatorSettingsPanelSSHKeys 'action', )); - $panel = new AphrontPanelView(); - $panel->addButton( - phutil_tag( - 'a', - array( - 'href' => $this->getPanelURI('?edit=true'), - 'class' => 'green button', - ), - pht('Add New Public Key'))); - $panel->setHeader(pht('SSH Public Keys')); + $panel = new PHUIObjectBoxView(); + $header = new PHUIHeaderView(); + + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('new'); + + $button = new PHUIButtonView(); + $button->setText(pht('Add New Public Key')); + $button->setHref($this->getPanelURI('?edit=true')); + $button->setTag('a'); + $button->setIcon($icon); + + $header->setHeader(pht('SSH Public Keys')); + $header->addActionLink($button); + + $panel->setHeader($header); $panel->appendChild($table); - $panel->setNoBackground(); return $panel; } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php index 1e261a68fb..4330a6dc93 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php @@ -50,17 +50,9 @@ final class PhabricatorSettingsPanelSearchPreferences id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))); - $error_view = null; - if ($request->getStr('saved') === 'true') { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Preferences Saved')) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors(array(pht('Your preferences have been saved.'))); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Search Preferences')) - ->setFormError($error_view) + ->setFormSaved($request->getStr('saved') === 'true') ->setForm($form); return array( diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php b/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php new file mode 100644 index 0000000000..2081ec2825 --- /dev/null +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php @@ -0,0 +1,95 @@ +getUser(); + + $accounts = id(new PhabricatorExternalAccountQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($viewer->getPHID())) + ->execute(); + + $identity_phids = mpull($accounts, 'getPHID'); + $identity_phids[] = $viewer->getPHID(); + + $sessions = id(new PhabricatorAuthSessionQuery()) + ->setViewer($viewer) + ->withIdentityPHIDs($identity_phids) + ->execute(); + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($identity_phids) + ->execute(); + + $current_key = PhabricatorHash::digest( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); + + $rows = array(); + $rowc = array(); + foreach ($sessions as $session) { + if ($session->getSessionKey() == $current_key) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + + $rows[] = array( + $handles[$session->getUserPHID()]->renderLink(), + substr($session->getSessionKey(), 0, 12), + $session->getType(), + phabricator_datetime($session->getSessionStart(), $viewer), + phabricator_datetime($session->getSessionExpires(), $viewer), + ); + } + + $table = new AphrontTableView($rows); + $table->setNoDataString(pht("You don't have any active sessions.")); + $table->setRowClasses($rowc); + $table->setHeaders( + array( + pht('Identity'), + pht('Session'), + pht('Type'), + pht('Created'), + pht('Expires'), + )); + $table->setColumnClasses( + array( + 'wide', + 'n', + '', + 'right', + 'right', + )); + + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Active Login Sessions')); + + $panel = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($table); + + return $panel; + } + +} diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php index f4409e756f..4154fa3020 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php @@ -123,13 +123,6 @@ final class PhabricatorSlowvoteEditController } } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle(pht('Form Errors')); - $error_view->setErrors($errors); - } - $instructions = phutil_tag( 'p', @@ -244,13 +237,11 @@ final class PhabricatorSlowvoteEditController ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormError($error_view) + ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php index e073ce1419..1ceeda6cbd 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php @@ -50,9 +50,7 @@ final class PhabricatorSlowvotePollController $properties = $this->buildPropertyView($poll, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('V'.$poll->getID())); + $crumbs->addTextCrumb('V'.$poll->getID()); $xactions = $this->buildTransactions($poll); $add_comment = $this->buildCommentForm($poll); @@ -162,11 +160,9 @@ final class PhabricatorSlowvotePollController $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $add_comment_header = id(new PHUIHeaderView()) - ->setHeader( - $is_serious - ? pht('Add Comment') - : pht('Enter Deliberations')); + $add_comment_header = $is_serious + ? pht('Add Comment') + : pht('Enter Deliberations'); $submit_button_name = $is_serious ? pht('Add Comment') @@ -174,18 +170,14 @@ final class PhabricatorSlowvotePollController $draft = PhabricatorDraft::newFromUserAndKey($viewer, $poll->getPHID()); - $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) + return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($poll->getPHID()) ->setDraft($draft) + ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$poll->getID().'/')) ->setSubmitButtonName($submit_button_name); - return id(new PHUIObjectBoxView()) - ->setFlush(true) - ->setHeader($add_comment_header) - ->appendChild($add_comment_form); - } } diff --git a/src/applications/system/PhabricatorDebugController.php b/src/applications/system/PhabricatorDebugController.php index e26dd0e6c9..7a66906684 100644 --- a/src/applications/system/PhabricatorDebugController.php +++ b/src/applications/system/PhabricatorDebugController.php @@ -28,7 +28,7 @@ final class PhabricatorDebugController extends PhabricatorController { $out = ob_get_clean(); $response = new AphrontWebpageResponse(); - $response->setContent(hsprintf('
    %s
    ', $out)); + $response->setContent(phutil_tag('pre', array(), $out)); return $response; } diff --git a/src/applications/tokens/controller/PhabricatorTokenGivenController.php b/src/applications/tokens/controller/PhabricatorTokenGivenController.php index 471bb47fc1..1928c89fee 100644 --- a/src/applications/tokens/controller/PhabricatorTokenGivenController.php +++ b/src/applications/tokens/controller/PhabricatorTokenGivenController.php @@ -60,9 +60,7 @@ final class PhabricatorTokenGivenController extends PhabricatorTokenController { $nav = $this->buildSideNav(); $nav->setCrumbs( $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title))); + ->addTextCrumb($title)); $nav->selectFilter('given/'); $nav->appendChild($list); diff --git a/src/applications/tokens/controller/PhabricatorTokenLeaderController.php b/src/applications/tokens/controller/PhabricatorTokenLeaderController.php index 271a5f1ab7..50527488b6 100644 --- a/src/applications/tokens/controller/PhabricatorTokenLeaderController.php +++ b/src/applications/tokens/controller/PhabricatorTokenLeaderController.php @@ -42,9 +42,7 @@ final class PhabricatorTokenLeaderController $nav = $this->buildSideNav(); $nav->setCrumbs( $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title))); + ->addTextCrumb($title)); $nav->selectFilter('leaders/'); $nav->appendChild($list); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index d1a6e2a1ea..8a864443b0 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -228,6 +228,7 @@ abstract class PhabricatorApplicationTransactionEditor private function applyInternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); @@ -322,13 +323,19 @@ abstract class PhabricatorApplicationTransactionEditor protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - throw new Exception("Capability not supported!"); + $type = $xaction->getTransactionType(); + throw new Exception( + "Transaction type '{$type}' is missing an internal apply ". + "implementation!"); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - throw new Exception("Capability not supported!"); + $type = $xaction->getTransactionType(); + throw new Exception( + "Transaction type '{$type}' is missing an external apply ". + "implementation!"); } protected function applyFinalEffects( @@ -527,7 +534,7 @@ abstract class PhabricatorApplicationTransactionEditor if ($this->supportsSearch()) { id(new PhabricatorSearchIndexer()) - ->indexDocumentByPHID($object->getPHID()); + ->queueDocumentForIndexing($object->getPHID()); } if ($this->supportsFeed()) { @@ -1129,6 +1136,39 @@ abstract class PhabricatorApplicationTransactionEditor } + /** + * Check for a missing text field. + * + * A text field is missing if the object has no value and there are no + * transactions which set a value, or if the transactions remove the value. + * This method is intended to make implementing @{method:validateTransaction} + * more convenient: + * + * $missing = $this->validateIsEmptyTextField( + * $object->getName(), + * $xactions); + * + * This will return `true` if the net effect of the object and transactions + * is an empty field. + * + * @param wild Current field value. + * @param list Transactions editing the + * field. + * @return bool True if the field will be an empty text field after edits. + */ + protected function validateIsEmptyTextField($field_value, array $xactions) { + if (strlen($field_value) && empty($xactions)) { + return false; + } + + if ($xactions && strlen(last($xactions)->getNewValue())) { + return false; + } + + return true; + } + + /* -( Implicit CCs )------------------------------------------------------- */ diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index e4fb7c52b9..fbb13222c9 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -233,7 +233,7 @@ abstract class PhabricatorApplicationTransaction return 'link'; } - return null; + return 'edit'; } public function getColor() { @@ -537,6 +537,57 @@ abstract class PhabricatorApplicationTransaction return $this->transactionGroup; } + /** + * Should this transaction be visually grouped with an existing transaction + * group? + * + * @param list List of transactions. + * @return bool True to display in a group with the other transactions. + */ + public function shouldDisplayGroupWith(array $group) { + $type_comment = PhabricatorTransactions::TYPE_COMMENT; + + $this_source = null; + if ($this->getContentSource()) { + $this_source = $this->getContentSource()->getSource(); + } + + foreach ($group as $xaction) { + // Don't group transactions by different authors. + if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) { + return false; + } + + // Don't group transactions for different objects. + if ($xaction->getObjectPHID() != $this->getObjectPHID()) { + return false; + } + + // Don't group anything into a group which already has a comment. + if ($xaction->getTransactionType() == $type_comment) { + return false; + } + + // Don't group transactions from different content sources. + $other_source = null; + if ($xaction->getContentSource()) { + $other_source = $xaction->getContentSource()->getSource(); + } + + if ($other_source != $this_source) { + return false; + } + + // Don't group transactions which happened more than 2 minutes apart. + $apart = abs($xaction->getDateCreated() - $this->getDateCreated()); + if ($apart > (60 * 2)) { + return false; + } + } + + return true; + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 8a66f17f95..463347d215 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -18,6 +18,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { private $requestURI; private $showPreview = true; private $objectPHID; + private $headerText; public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; @@ -72,25 +73,29 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { return $this->action; } + public function setHeaderText($text) { + $this->headerText = $text; + return $this; + } + public function render() { $user = $this->getUser(); if (!$user->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string) $this->getRequestURI()); - return phutil_tag( - 'div', - array( - 'class' => 'login-to-comment' - ), - javelin_tag( - 'a', - array( - 'class' => 'button', - 'sigil' => 'workflow', - 'href' => $uri - ), - pht('Login to Comment'))); + return id(new PHUIObjectBoxView()) + ->setFlush(true) + ->setHeaderText(pht('Add Comment')) + ->appendChild( + javelin_tag( + 'a', + array( + 'class' => 'login-to-comment button', + 'sigil' => 'workflow', + 'href' => $uri + ), + pht('Login to Comment'))); } $data = array(); @@ -124,7 +129,12 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { : null, )); - return array($comment, $preview); + $comment_box = id(new PHUIObjectBoxView()) + ->setFlush(true) + ->setHeaderText($this->headerText) + ->appendChild($comment); + + return array($comment_box, $preview); } private function renderCommentPanel() { @@ -174,23 +184,13 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { $preview = id(new PhabricatorTimelineView()) ->setID($this->getPreviewTimelineID()); - $header = phutil_tag( - 'div', - array( - 'class' => 'phabricator-timeline-preview-header', - ), - pht('Preview')); - return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', ), - array( - $header, - $preview, - )); + $preview); } private function getPreviewPanelID() { diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index e64b5b86ff..aa1d856c12 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -61,91 +61,26 @@ class PhabricatorApplicationTransactionView extends AphrontView { $user = $this->getUser(); $anchor = $this->anchorOffset; - $events = array(); $xactions = $this->transactions; - foreach ($xactions as $key => $xaction) { - if ($xaction->shouldHide()) { - unset($xactions[$key]); - } - } - $last = null; - $last_key = null; - $groups = array(); - foreach ($xactions as $key => $xaction) { - if ($last && $this->shouldGroupTransactions($last, $xaction)) { - $groups[$last_key][] = $xaction; - unset($xactions[$key]); - } else { - $last = $xaction; - $last_key = $key; - } - } - - foreach ($xactions as $key => $xaction) { - $xaction->attachTransactionGroup(idx($groups, $key, array())); - - $event = id(new PhabricatorTimelineEventView()) - ->setUser($user) - ->setTransactionPHID($xaction->getPHID()) - ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) - ->setIcon($xaction->getIcon()) - ->setColor($xaction->getColor()); - - $title = $xaction->getTitle(); - if ($xaction->hasChangeDetails()) { - if ($this->isPreview || $this->isDetailView) { - $details = $this->buildChangeDetails($xaction); - } else { - $details = $this->buildChangeDetailsLink($xaction); - } - $title = array( - $title, - ' ', - $details, - ); - } - $event->setTitle($title); - - if ($this->isPreview) { - $event->setIsPreview(true); - } else { - $event - ->setDateCreated($xaction->getDateCreated()) - ->setContentSource($xaction->getContentSource()) - ->setAnchor($anchor); + $xactions = $this->filterHiddenTransactions($xactions); + $xactions = $this->groupRelatedTransactions($xactions); + $groups = $this->groupDisplayTransactions($xactions); + $events = array(); + foreach ($groups as $group) { + $group_event = null; + foreach ($group as $xaction) { + $event = $this->renderEvent($xaction, $group, $anchor); $anchor++; - } - - $has_deleted_comment = $xaction->getComment() && - $xaction->getComment()->getIsDeleted(); - - if ($this->getShowEditActions() && !$this->isPreview) { - if ($xaction->getCommentVersion() > 1) { - $event->setIsEdited(true); - } - - $can_edit = PhabricatorPolicyCapability::CAN_EDIT; - - if ($xaction->hasComment() || $has_deleted_comment) { - $has_edit_capability = PhabricatorPolicyFilter::hasCapability( - $user, - $xaction, - $can_edit); - if ($has_edit_capability) { - $event->setIsEditable(true); - } + if (!$group_event) { + $group_event = $event; + } else { + $group_event->addEventToGroup($event); } } - - $content = $this->renderTransactionContent($xaction); - if ($content) { - $event->appendChild($content); - } - - $events[] = $event; + $events[] = $group_event; } return $events; @@ -295,5 +230,148 @@ class PhabricatorApplicationTransactionView extends AphrontView { return null; } + private function filterHiddenTransactions(array $xactions) { + foreach ($xactions as $key => $xaction) { + if ($xaction->shouldHide()) { + unset($xactions[$key]); + } + } + return $xactions; + } + + private function groupRelatedTransactions(array $xactions) { + $last = null; + $last_key = null; + $groups = array(); + foreach ($xactions as $key => $xaction) { + if ($last && $this->shouldGroupTransactions($last, $xaction)) { + $groups[$last_key][] = $xaction; + unset($xactions[$key]); + } else { + $last = $xaction; + $last_key = $key; + } + } + + foreach ($xactions as $key => $xaction) { + $xaction->attachTransactionGroup(idx($groups, $key, array())); + } + + return $xactions; + } + + private function groupDisplayTransactions(array $xactions) { + $groups = array(); + $group = array(); + foreach ($xactions as $xaction) { + if ($xaction->shouldDisplayGroupWith($group)) { + $group[] = $xaction; + } else { + if ($group) { + $groups[] = $group; + } + $group = array($xaction); + } + } + + if ($group) { + $groups[] = $group; + } + + foreach ($groups as $key => $group) { + $group = msort($group, 'getActionStrength'); + $group = array_reverse($group); + $groups[$key] = $group; + } + + return $groups; + } + + private function renderEvent( + PhabricatorApplicationTransaction $xaction, + array $group, + $anchor) { + $viewer = $this->getUser(); + + $event = id(new PhabricatorTimelineEventView()) + ->setUser($viewer) + ->setTransactionPHID($xaction->getPHID()) + ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) + ->setIcon($xaction->getIcon()) + ->setColor($xaction->getColor()); + + if (!$this->shouldSuppressTitle($xaction, $group)) { + $title = $xaction->getTitle(); + if ($xaction->hasChangeDetails()) { + if ($this->isPreview || $this->isDetailView) { + $details = $this->buildChangeDetails($xaction); + } else { + $details = $this->buildChangeDetailsLink($xaction); + } + $title = array( + $title, + ' ', + $details, + ); + } + $event->setTitle($title); + } + + if ($this->isPreview) { + $event->setIsPreview(true); + } else { + $event + ->setDateCreated($xaction->getDateCreated()) + ->setContentSource($xaction->getContentSource()) + ->setAnchor($anchor); + } + + $has_deleted_comment = $xaction->getComment() && + $xaction->getComment()->getIsDeleted(); + + if ($this->getShowEditActions() && !$this->isPreview) { + if ($xaction->getCommentVersion() > 1) { + $event->setIsEdited(true); + } + + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; + + if ($xaction->hasComment() || $has_deleted_comment) { + $has_edit_capability = PhabricatorPolicyFilter::hasCapability( + $viewer, + $xaction, + $can_edit); + if ($has_edit_capability) { + $event->setIsEditable(true); + } + } + } + + $content = $this->renderTransactionContent($xaction); + if ($content) { + $event->appendChild($content); + } + + return $event; + } + + private function shouldSuppressTitle( + PhabricatorApplicationTransaction $xaction, + array $group) { + + // This is a little hard-coded, but we don't have any other reasonable + // cases for now. Suppress "commented on" if there are other actions in + // the display group. + + if (count($group) > 1) { + $type_comment = PhabricatorTransactions::TYPE_COMMENT; + if ($xaction->getTransactionType() == $type_comment) { + return true; + } + } + + return false; + } + } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php index ce2bcce99f..e7d9ab0f97 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php @@ -35,6 +35,9 @@ final class PhabricatorTypeaheadCommonDatasourceController $need_noproject = false; $need_symbols = false; $need_jump_objects = false; + $need_build_plans = false; + $need_macros = false; + $need_legalpad_documents = false; switch ($this->type) { case 'mainsearch': $need_users = true; @@ -93,6 +96,15 @@ final class PhabricatorTypeaheadCommonDatasourceController case 'arcanistprojects': $need_arcanist_projects = true; break; + case 'buildplans': + $need_build_plans = true; + break; + case 'macros': + $need_macros = true; + break; + case 'legalpaddocuments': + $need_legalpad_documents = true; + break; } $results = array(); @@ -109,6 +121,7 @@ final class PhabricatorTypeaheadCommonDatasourceController ->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT); } + if ($need_users) { $columns = array( 'isSystemAgent', @@ -219,6 +232,42 @@ final class PhabricatorTypeaheadCommonDatasourceController } } + if ($need_build_plans) { + $plans = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->execute(); + foreach ($plans as $plan) { + $results[] = id(new PhabricatorTypeaheadResult()) + ->setName($plan->getName()) + ->setPHID($plan->getPHID()); + } + } + + if ($need_macros) { + $macros = id(new PhabricatorMacroQuery()) + ->setViewer($viewer) + ->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE) + ->execute(); + $macros = mpull($macros, 'getName', 'getPHID'); + foreach ($macros as $phid => $name) { + $results[] = id(new PhabricatorTypeaheadResult()) + ->setPHID($phid) + ->setName($name); + } + } + + if ($need_legalpad_documents) { + $documents = id(new LegalpadDocumentQuery()) + ->setViewer($viewer) + ->execute(); + $documents = mpull($documents, 'getTitle', 'getPHID'); + foreach ($documents as $phid => $title) { + $results[] = id(new PhabricatorTypeaheadResult()) + ->setPHID($phid) + ->setName($title); + } + } + if ($need_projs) { $projs = id(new PhabricatorProjectQuery()) ->setViewer($viewer) diff --git a/src/applications/uiexample/examples/PHUIBoxExample.php b/src/applications/uiexample/examples/PHUIBoxExample.php index c972e9e316..d9daecd8cc 100644 --- a/src/applications/uiexample/examples/PHUIBoxExample.php +++ b/src/applications/uiexample/examples/PHUIBoxExample.php @@ -58,6 +58,26 @@ final class PHUIBoxExample extends PhabricatorUIExample { ->addPadding(PHUI::PADDING_LARGE) ->addMargin(PHUI::MARGIN_LARGE_BOTTOM)); + $image = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon('love'); + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor(PHUIButtonView::SIMPLE) + ->setIcon($image) + ->setText('Such Wow') + ->addClass(PHUI::MARGIN_SMALL_RIGHT); + + $header = id(new PHUIHeaderView()) + ->setHeader('Fancy Box') + ->addActionLink($button); + + $obj4 = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild(id(new PHUIBoxView()) + ->addPadding(PHUI::PADDING_MEDIUM) + ->appendChild('Such Fancy, Nice Box, Many Corners.')); + $head1 = id(new PHUIHeaderView()) ->setHeader(pht('Plain Box')); @@ -67,6 +87,9 @@ final class PHUIBoxExample extends PhabricatorUIExample { $head3 = id(new PHUIHeaderView()) ->setHeader(pht('Shadow Box with space')); + $head4 = id(new PHUIHeaderView()) + ->setHeader(pht('PHUIObjectBoxView')); + $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE); @@ -88,7 +111,9 @@ final class PHUIBoxExample extends PhabricatorUIExample { $head2, $wrap2, $head3, - $wrap3 + $wrap3, + $head4, + $obj4, )); } } diff --git a/src/applications/uiexample/examples/PHUIButtonExample.php b/src/applications/uiexample/examples/PHUIButtonExample.php index 10872bcb20..10b7159864 100644 --- a/src/applications/uiexample/examples/PHUIButtonExample.php +++ b/src/applications/uiexample/examples/PHUIButtonExample.php @@ -119,8 +119,25 @@ final class PHUIButtonExample extends PhabricatorUIExample { ->addClass(PHUI::MARGIN_SMALL_RIGHT); } + $column2 = array(); + $icons = array( + 'Subscribe' => 'meta-mta', + 'Edit' => 'edit'); + foreach ($icons as $text => $icon) { + $image = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon($icon); + $column2[] = id(new PHUIButtonView()) + ->setTag('a') + ->setColor(PHUIButtonView::SIMPLE) + ->setIcon($image) + ->setText($text) + ->addClass(PHUI::MARGIN_SMALL_RIGHT); + } + $layout3 = id(new AphrontMultiColumnView()) ->addColumn($column) + ->addColumn($column2) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); diff --git a/src/applications/uiexample/examples/PHUIObjectItemListExample.php b/src/applications/uiexample/examples/PHUIObjectItemListExample.php index 04003b6773..a4d7a105c6 100644 --- a/src/applications/uiexample/examples/PHUIObjectItemListExample.php +++ b/src/applications/uiexample/examples/PHUIObjectItemListExample.php @@ -130,6 +130,7 @@ final class PHUIObjectItemListExample extends PhabricatorUIExample { id(new PHUIObjectItemView()) ->setHeader(pht('House of Cards')) ->setBarColor('yellow') + ->setDisabled(true) ->addByline(pht('Owner: %s', $owner))); $author = phutil_tag('a', array('href' => '#'), pht('agoat')); diff --git a/src/applications/uiexample/examples/PHUITagExample.php b/src/applications/uiexample/examples/PHUITagExample.php new file mode 100644 index 0000000000..2f3ec474f4 --- /dev/null +++ b/src/applications/uiexample/examples/PHUITagExample.php @@ -0,0 +1,177 @@ +PHUITagView to render various tags.'); + } + + public function renderExample() { + $intro = array(); + + $intro[] = 'Hey, '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_PERSON) + ->setName('@alincoln') + ->setHref('#'); + $intro[] = ' how is stuff?'; + $intro[] = hsprintf('

    '); + + + $intro[] = 'Did you hear that '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_PERSON) + ->setName('@gwashington') + ->setDotColor(PHUITagView::COLOR_RED) + ->setHref('#'); + $intro[] = ' is away, '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_PERSON) + ->setName('@tjefferson') + ->setDotColor(PHUITagView::COLOR_ORANGE) + ->setHref('#'); + $intro[] = ' has some errands, and '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_PERSON) + ->setName('@rreagan') + ->setDotColor(PHUITagView::COLOR_GREY) + ->setHref('#'); + $intro[] = ' is gone?'; + $intro[] = hsprintf('

    '); + + $intro[] = 'Take a look at '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_OBJECT) + ->setName('D123') + ->setHref('#'); + $intro[] = ' when you get a chance.'; + $intro[] = hsprintf('

    '); + + $intro[] = 'Hmm? '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_OBJECT) + ->setName('D123') + ->setClosed(true) + ->setHref('#'); + $intro[] = ' is '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLACK) + ->setName('Abandoned'); + $intro[] = '.'; + $intro[] = hsprintf('

    '); + + $intro[] = 'I hope someone is going to '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_OBJECT) + ->setName('T123: Water The Dog') + ->setHref('#'); + $intro[] = ' -- that task is '; + $intro[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_RED) + ->setName('High Priority'); + $intro[] = '!'; + + $intro = id(new PHUIBoxView()) + ->appendChild($intro) + ->addPadding(PHUI::PADDING_LARGE); + + $header1 = id(new PHUIHeaderView()) + ->setHeader('Colors'); + + $colors = PHUITagView::getColors(); + $tags = array(); + foreach ($colors as $color) { + $tags[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor($color) + ->setName(ucwords($color)); + $tags[] = hsprintf('

    '); + } + + $content1 = id(new PHUIBoxView()) + ->appendChild($tags) + ->addPadding(PHUI::PADDING_LARGE); + + $tags = array(); + $tags[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_GREEN) + ->setDotColor(PHUITagView::COLOR_RED) + ->setName('Christmas'); + $tags[] = hsprintf('

    '); + $tags[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_OBJECT) + ->setBackgroundColor(PHUITagView::COLOR_ORANGE) + ->setDotColor(PHUITagView::COLOR_BLACK) + ->setName('Halloween'); + $tags[] = hsprintf('

    '); + $tags[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_INDIGO) + ->setDotColor(PHUITagView::COLOR_YELLOW) + ->setName('Easter'); + + $content2 = id(new PHUIBoxView()) + ->appendChild($tags) + ->addPadding(PHUI::PADDING_LARGE); + + $icons = array(); + $icons[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_GREEN) + ->setIcon('ok-white') + ->setName('Passed'); + $icons[] = hsprintf('

    '); + $icons[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_RED) + ->setIcon('delete-white') + ->setName('Failed'); + $icons[] = hsprintf('

    '); + $icons[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLUE) + ->setIcon('play-white') + ->setName('Running'); + $icons[] = hsprintf('

    '); + $icons[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_GREY) + ->setIcon('pause-white') + ->setName('Paused'); + $icons[] = hsprintf('

    '); + $icons[] = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLACK) + ->setIcon('stop-white') + ->setName('Stopped'); + + $content3 = id(new PHUIBoxView()) + ->appendChild($icons) + ->addPadding(PHUI::PADDING_LARGE); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText('Inline') + ->appendChild($intro); + + $box1 = id(new PHUIObjectBoxView()) + ->setHeaderText('Colors') + ->appendChild($content1); + + $box2 = id(new PHUIObjectBoxView()) + ->setHeaderText('Holidays') + ->appendChild($content2); + + $box3 = id(new PHUIObjectBoxView()) + ->setHeaderText('Icons') + ->appendChild($content3); + + return array($box, $box1, $box2, $box3); + } +} diff --git a/src/applications/uiexample/examples/PhabricatorActionHeaderExample.php b/src/applications/uiexample/examples/PhabricatorActionHeaderExample.php index 560cf9d988..35a6adeef4 100644 --- a/src/applications/uiexample/examples/PhabricatorActionHeaderExample.php +++ b/src/applications/uiexample/examples/PhabricatorActionHeaderExample.php @@ -189,14 +189,14 @@ final class PhabricatorActionHeaderExample extends PhabricatorUIExample { $title4 = id(new PHUIHeaderView()) ->setHeader(pht('With Tags')); - $tag1 = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_RED) + $tag1 = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_RED) ->setName('Open'); - $tag2 = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) + $tag2 = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) + ->setBackgroundColor(PHUITagView::COLOR_BLUE) ->setName('Closed'); $action1 = new PHUIIconView(); diff --git a/src/applications/uiexample/examples/PhabricatorFormExample.php b/src/applications/uiexample/examples/PhabricatorFormExample.php index 8956f4387b..2c9e006f71 100644 --- a/src/applications/uiexample/examples/PhabricatorFormExample.php +++ b/src/applications/uiexample/examples/PhabricatorFormExample.php @@ -38,11 +38,22 @@ final class PhabricatorFormExample extends PhabricatorUIExample { $null_value = $null_time->readValueFromRequest($request); } + $divider_control = new AphrontFormDividerControl(); + + $credentials = array(); + $password_control = id(new PassphraseCredentialControl()) + ->setName('credentialPHID') + ->setLabel(pht('Password')) + ->setCredentialType('password') + ->setOptions($credentials); + $form = id(new AphrontFormView()) ->setUser($user) ->appendChild($start_time) ->appendChild($end_time) ->appendChild($null_time) + ->appendChild($divider_control) + ->appendChild($password_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Submit')); diff --git a/src/applications/uiexample/examples/PhabricatorHovercardExample.php b/src/applications/uiexample/examples/PhabricatorHovercardExample.php index 1936b525e9..1ce0d7e170 100644 --- a/src/applications/uiexample/examples/PhabricatorHovercardExample.php +++ b/src/applications/uiexample/examples/PhabricatorHovercardExample.php @@ -36,8 +36,8 @@ final class PhabricatorHovercardExample extends PhabricatorUIExample { ManiphestPHIDTypeTask::TYPECONST, "Improve Mobile Experience for Phabricator"); - $tag = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_STATE) ->setName('Closed, Resolved'); $panel = $this->createPanel("Maniphest Hovercard"); $panel->appendChild(id(new PhabricatorHovercardView()) diff --git a/src/applications/uiexample/examples/PhabricatorPagedFormExample.php b/src/applications/uiexample/examples/PhabricatorPagedFormExample.php index e34e4cf23e..64846f48db 100644 --- a/src/applications/uiexample/examples/PhabricatorPagedFormExample.php +++ b/src/applications/uiexample/examples/PhabricatorPagedFormExample.php @@ -18,24 +18,28 @@ final class PhabricatorPagedFormExample extends PhabricatorUIExample { $page1 = id(new PHUIFormPageView()) + ->setPageName(pht('Page 1')) ->addControl( id(new AphrontFormTextControl()) ->setName('page1') ->setLabel('Page 1')); $page2 = id(new PHUIFormPageView()) + ->setPageName(pht('Page 2')) ->addControl( id(new AphrontFormTextControl()) ->setName('page2') ->setLabel('Page 2')); $page3 = id(new PHUIFormPageView()) + ->setPageName(pht('Page 3')) ->addControl( id(new AphrontFormTextControl()) ->setName('page3') ->setLabel('Page 3')); $page4 = id(new PHUIFormPageView()) + ->setPageName(pht('Page 4')) ->addControl( id(new AphrontFormTextControl()) ->setName('page4') diff --git a/src/applications/uiexample/examples/PhabricatorSortTableExample.php b/src/applications/uiexample/examples/PhabricatorSortTableExample.php index de7df560cd..5176669485 100644 --- a/src/applications/uiexample/examples/PhabricatorSortTableExample.php +++ b/src/applications/uiexample/examples/PhabricatorSortTableExample.php @@ -87,8 +87,8 @@ final class PhabricatorSortTableExample extends PhabricatorUIExample { $reverse, $orders); - $panel = new AphrontPanelView(); - $panel->setHeader('Sortable Table of Vehicles'); + $panel = new PHUIObjectBoxView(); + $panel->setHeaderText('Sortable Table of Vehicles'); $panel->appendChild($table); return $panel; diff --git a/src/applications/uiexample/examples/PhabricatorTagExample.php b/src/applications/uiexample/examples/PhabricatorTagExample.php deleted file mode 100644 index a63762a7e6..0000000000 --- a/src/applications/uiexample/examples/PhabricatorTagExample.php +++ /dev/null @@ -1,140 +0,0 @@ -PhabricatorTagView to render various tags.'); - } - - public function renderExample() { - $intro = array(); - - $intro[] = 'Hey, '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_PERSON) - ->setName('@alincoln') - ->setHref('#'); - $intro[] = ' how is stuff?'; - $intro[] = hsprintf('

    '); - - - $intro[] = 'Did you hear that '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_PERSON) - ->setName('@gwashington') - ->setDotColor(PhabricatorTagView::COLOR_RED) - ->setHref('#'); - $intro[] = ' is away, '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_PERSON) - ->setName('@tjefferson') - ->setDotColor(PhabricatorTagView::COLOR_ORANGE) - ->setHref('#'); - $intro[] = ' has some errands, and '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_PERSON) - ->setName('@rreagan') - ->setDotColor(PhabricatorTagView::COLOR_GREY) - ->setHref('#'); - $intro[] = ' is gone?'; - $intro[] = hsprintf('

    '); - - $intro[] = 'Take a look at '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_OBJECT) - ->setName('D123') - ->setHref('#'); - $intro[] = ' when you get a chance.'; - $intro[] = hsprintf('

    '); - - $intro[] = 'Hmm? '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_OBJECT) - ->setName('D123') - ->setClosed(true) - ->setHref('#'); - $intro[] = ' is '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK) - ->setName('Abandoned'); - $intro[] = '.'; - $intro[] = hsprintf('

    '); - - $intro[] = 'I hope someone is going to '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_OBJECT) - ->setName('T123: Water The Dog') - ->setBarColor(PhabricatorTagView::COLOR_RED) - ->setHref('#'); - $intro[] = ' -- that task is '; - $intro[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_RED) - ->setName('High Priority'); - $intro[] = '!'; - - $intro = id(new PHUIBoxView()) - ->appendChild($intro) - ->setShadow(true) - ->addPadding(PHUI::PADDING_LARGE) - ->addMargin(PHUI::MARGIN_LARGE); - - $header1 = id(new PHUIHeaderView()) - ->setHeader('Colors'); - - $colors = PhabricatorTagView::getColors(); - $tags = array(); - foreach ($colors as $color) { - $tags[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor($color) - ->setName(ucwords($color)); - $tags[] = hsprintf('

    '); - } - - $content1 = id(new PHUIBoxView()) - ->appendChild($tags) - ->setShadow(true) - ->addPadding(PHUI::PADDING_LARGE) - ->addMargin(PHUI::MARGIN_LARGE); - - $header2 = id(new PHUIHeaderView()) - ->setHeader('Holidays?'); - - - $tags = array(); - $tags[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_GREEN) - ->setDotColor(PhabricatorTagView::COLOR_RED) - ->setBarColor(PhabricatorTagView::COLOR_RED) - ->setName('Christmas'); - $tags[] = hsprintf('

    '); - $tags[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_OBJECT) - ->setBackgroundColor(PhabricatorTagView::COLOR_ORANGE) - ->setDotColor(PhabricatorTagView::COLOR_BLACK) - ->setBarColor(PhabricatorTagView::COLOR_BLACK) - ->setName('Halloween'); - $tags[] = hsprintf('

    '); - $tags[] = id(new PhabricatorTagView()) - ->setType(PhabricatorTagView::TYPE_STATE) - ->setBackgroundColor(PhabricatorTagView::COLOR_INDIGO) - ->setDotColor(PhabricatorTagView::COLOR_YELLOW) - ->setBarColor(PhabricatorTagView::COLOR_BLUE) - ->setName('Easter'); - - $content2 = id(new PHUIBoxView()) - ->appendChild($tags) - ->setShadow(true) - ->addPadding(PHUI::PADDING_LARGE) - ->addMargin(PHUI::MARGIN_LARGE); - - return array($intro, $header1, $content1, $header2, $content2); - } -} diff --git a/src/docs/tech/celerity.diviner b/src/docs/tech/celerity.diviner index 9c707565ce..f9bb7ab875 100644 --- a/src/docs/tech/celerity.diviner +++ b/src/docs/tech/celerity.diviner @@ -55,10 +55,8 @@ neessary preprocessing. It uses @{class:CelerityResourceMap} to locate resources and read packaging rules. The dependency and packaging maps are generated by -##scripts/celerity_mapper.php##, which updates -##src/__celerity_resource_map__.php##. This file is automatically included and -just calls @{function:celerity_register_resource_map} with a large blob of -static data to populate @{class:CelerityResourceMap}. +##bin/celerity map##, which updates +##resources/celerity/map.php##.. @{class:CelerityStaticResourceResponse} also manages some Javelin information, and @{function:celerity_generate_unique_node_id} uses this metadata to provide diff --git a/src/docs/user/configuration/configuration_guide.diviner b/src/docs/user/configuration/configuration_guide.diviner index 7c09335a5f..6666dd0ca2 100644 --- a/src/docs/user/configuration/configuration_guide.diviner +++ b/src/docs/user/configuration/configuration_guide.diviner @@ -55,13 +55,24 @@ this: If Apache isn't currently configured to serve documents out of the directory -where you put Phabricator, you may also need to add a section like this: +where you put Phabricator, you may also need to add `` section. The +syntax for this section depends on which version of Apache you're running. +(If you don't know, you can usually figure this out by running `httpd -v`.) +For Apache versions older than 2.4, use this: + name="Apache Older Than 2.4" Order allow,deny Allow from all +For Apache versions 2.4 and newer, use this: + + name="Apache 2.4 and Newer" + + Require all granted + + After making your edits, restart Apache, then continue to "Setup" below. = Webserver: Configuring nginx = @@ -193,5 +204,6 @@ Continue by: - configuring inbound mail with @{article:Configuring Inbound Email}; or - importing repositories with @{article:Diffusion User Guide}; or - learning about daemons with @{article:Managing Daemons with phd}; or - - configuring backups with @{article:Configuring Backups}; or + - configuring backups with + @{article:Configuring Backups and Performing Migrations}; or - contributing to Phabricator with @{article:Contributor Introduction}. diff --git a/src/docs/user/configuration/configuring_backups.diviner b/src/docs/user/configuration/configuring_backups.diviner index 20aee18e33..d779499dd2 100644 --- a/src/docs/user/configuration/configuring_backups.diviner +++ b/src/docs/user/configuration/configuring_backups.diviner @@ -1,7 +1,7 @@ -@title Configuring Backups +@title Configuring Backups and Performing Migrations @group config -Advice for backing up Phabricator. +Advice for backing up Phabricator, or migrating from one machine to another. = Overview = @@ -16,12 +16,17 @@ which needs to be backed up are: This document discusses approaches for backing up this data. +If you are migrating from one machine to another, you can generally follow the +same steps you would if you were creating a backup and then restoring it, you +will just backup the old machine and then restore the data onto the new +machine. + = Backup: MySQL Databases = Most of Phabricator's data is stored in MySQL, and it's the most important thing to back up. You can run `bin/storage dump` to get a dump of all the MySQL -databases. This is a convenience script which just runs a normal `mysqldump` -of every database Phabricator owns. +databases. This is a convenience script which just runs a normal `mysqldump`, +but will only dump databases Phabricator owns. Since most of this data is compressible, it may be helpful to run it through gzip prior to storage. For example: diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index 56a08eb006..7489ef645e 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -152,17 +152,6 @@ disable outbound mail, if you don't want to send mail or don't want to configure it yet. Just set **metamta.mail-adapter** to "PhabricatorMailImplementationTestAdapter". -= Configuring MetaMTA to Send Mail Using a Daemon = - -Regardless of how you are sending outbound email, you can move the handoff to -the MTA out of the main process and into a daemon. This will greatly improve -application performance if your mailer is slow, like Amazon SES. In particular, -commenting on Differential Revisions and Maniphest Tasks sends outbound email. - -If you set **metamta.send-immediately** to ##false## in your configuration, -MetaMTA will queue mail to be send by a PhabricatorTaskmasterDaemon. -For more information on using daemons, see @{article:Managing Daemons with phd}. - = Testing and Debugging Outbound Email = You can use the `bin/mail` utility to test, debug, and examine outbound mail. In diff --git a/src/docs/user/developer/adding_new_css_and_js.diviner b/src/docs/user/developer/adding_new_css_and_js.diviner index 5bb5f5914f..2443f9d5d6 100644 --- a/src/docs/user/developer/adding_new_css_and_js.diviner +++ b/src/docs/user/developer/adding_new_css_and_js.diviner @@ -58,7 +58,7 @@ If you've only changed file content things will generally work even if you don't, but they might start not working as well in the future if you skip this step. -The generated file `src/__celerity_resource_map__.php` causes merge conflicts +The generated file `resources/celerity/map.php` causes merge conflicts quite often. They can be resolved by running the Celerity mapper. You can automate this process by running: diff --git a/src/docs/user/installation_guide.diviner b/src/docs/user/installation_guide.diviner index 2e2836cacb..ee6277834b 100644 --- a/src/docs/user/installation_guide.diviner +++ b/src/docs/user/installation_guide.diviner @@ -155,10 +155,10 @@ See for more information. Since Phabricator is under active development, you should update frequently. To update Phabricator: - - Stop the webserver. + - Stop the webserver (including `php-fpm`, if you use it). - Run `git pull` in `libphutil/`, `arcanist/` and `phabricator/`. - Run `phabricator/bin/storage upgrade`. - - Restart the webserver. + - Restart the webserver (and `php-fpm`, if you stopped it earlier). For more details, see @{article:Configuration Guide}. You can use a script similar to this one to automate the process: diff --git a/src/docs/user/userguide/diffusion_hosting.diviner b/src/docs/user/userguide/diffusion_hosting.diviner new file mode 100644 index 0000000000..99224c8a81 --- /dev/null +++ b/src/docs/user/userguide/diffusion_hosting.diviner @@ -0,0 +1,207 @@ +@title Diffusion User Guide: Repository Hosting +@group userguide + +Guide to configuring Phabricator repository hosting. + += Overview = + +Phabricator can host repositories and provide authenticated read and write +access to them over HTTP and SSH. This document describes how to configure +repository hosting. + +NOTE: This feature is new and has some rough edges. Let us know if you run into +issues (see @{article:Give Feedback! Get Support!}). + += Understanding Supported Protocols = + +Phabricator supports hosting over these protocols: + +| VCS | SSH | HTTP | +|-----|-----|------| +| Git | Supported | Supported | +| Mercurial | Supported | Supported | +| Subversion | Supported | Not Supported | + +All supported protocols handle reads (pull/checkout/clone) and writes +(push/commit). Of the two protocols, SSH is generally more robust, secure and +performant, but HTTP is easier to set up and supports anonymous access. + +| | SSH | HTTP | +| |-----|------| +| Reads | Yes | Yes | +| Writes | Yes | Yes | +| Authenticated Access | Yes | Yes | +| Push Logs | Yes | Yes | +| Commit Hooks | Yes | Yes | +| Anonymous Access | No | Yes | +| Security | Better (Asymmetric Key) | Okay (Password) | +| Performance | Better | Okay | +| Setup | Hard | Easy | + +Each repository can be configured individually, and you can use either protocol, +or both, or a mixture across different repositories. + +SSH is recommended unless you need anonymous access, or are not able to +configure it for technical reasons. + += Configuring System User Accounts = + +Phabricator uses as many as three user accounts. This section will guide you +through creating and configuring them. These are system user accounts on the +machine Phabricator runs on, not Phabricator user accounts. + +The system accounts are: + + - The user the daemons run as. We'll call this `daemon-user`. For more + information on the daemons, see @{article:Managing Daemons with phd}. + - The user the webserver runs as. We'll call this `www-user`. If you do not + plan to make repositories available over HTTP, you do not need to perform + any special configuration for this user. + - The user that users will connect over SSH as. We'll call this `vcs-user`. + If you do not plan to make repositories available over SSH, you do not need + to perform any special configuration for this user. + +To configure these users: + + - Create a `daemon-user` if one does not already exist (you can call this user + whatever you want, or use an existing account). When you start the daemons, + start them using this user. + - Create a `www-user` if one does not already exist. Run your webserver as + this user. In most cases, this user will already exist. + - Create a `vcs-user` if one does not already exist. Common names for this + user are `git` or `hg`. When users clone repositories, they will use a URI + like `vcs-user@phabricator.yourcompany.com`. + +Now, allow the `vcs-user` and `www-user` to `sudo` as the `daemon-user`. Add +this to `/etc/sudoers`, using `visudo` or `sudoedit`. + +If you plan to use SSH: + + vcs-user ALL=(daemon-user) SETENV: NOPASSWD: /path/to/bin/git-upload-pack, /path/to/bin/git-receive-pack, /path/to/bin/hg, /path/to/bin/svnserve + +If you plan to use HTTP: + + www-user ALL=(daemon-user) SETENV: NOPASSWD: /usr/bin/git-http-backend, /usr/bin/hg + +Replace `vcs-user`, `www-user` and `daemon-user` with the right usernames for +your configuration. Make sure all the paths point to the real locations of the +binaries on your system. You can omit any binaries associated with VCSes you do +not use. + +Adding these commands to `sudoers` will allow the daemon and webserver users to +write to repositories as the daemon user. + +Finally, once you've configured `sudoers`, set `phd.user` to the `daemon-user`: + + phabricator/ $ ./bin/config set phd.user daemon-user + += Configuring HTTP = + +If you plan to use authenticated HTTP, you need to set +`diffusion.allow-http-auth` in Config. If you don't plan to use HTTP, or plan to +use only anonymous HTTP, you can leave this setting disabled. + +Otherwise, if you've configured system accounts above, you're all set. No +additional server configuration is required to make HTTP work. + += Configuring SSH = + +SSH access requires some additional setup. Here's an overview of how setup +works: + + - You'll move the normal `sshd` daemon to another port, like `222`. When + connecting to the machine to administrate it, you'll use this alternate + port. + - You'll run a highly restricted `sshd` on port 22, with a special locked-down + configuration that uses Phabricator to authorize users and execute commands. + - The `sshd` on port 22 **MUST** be 6.2 or newer, because Phabricator relies + on the `AuthorizedKeysCommand` option. + +Here's a walkthrough of how to perform this configuration in detail: + +**Move Normal SSHD**: Be careful when editing the configuration for `sshd`. If +you get it wrong, you may lock yourself out of the machine. Restarting `sshd` +generally will not interrupt existing connections, but you should exercise +caution. Two strategies you can use to mitigate this risk are: smoke-test +configuration by starting a second `sshd`; and use a `screen` session which +automatically repairs configuration unless stopped. + +To smoke-test a configuration, just start another `sshd` using the `-f` flag: + + sudo sshd -f /path/to/config_file.edited + +You can then connect and make sure the edited config file is valid before +replacing your primary configuration file. + +To automatically repair configuration, start a `screen` session with a command +like this in it: + + sleep 60 ; mv sshd_config.good sshd_config ; /etc/init.d/sshd restart + +The specific command may vary for your system, but the general idea is to have +the machine automatically restore configuration after some period of time if +you don't stop it. If you lock yourself out, this will fix things automatically. + +Now that you're ready to edit your configuration, open up your `sshd` config +(often `/etc/ssh/sshd_config`) and change the `Port` setting to some other port, +like `222` (you can choose any port other than 22). + + Port 222 + +Very carefully, restart `sshd`. Verify that you can connect on the new port: + + ssh -p 222 ... + +**Configure and Start Phabricator SSHD**: Now, configure and start a second +`sshd` instance which will run on port `22`. This instance will use a special +locked-down configuration that uses Phabricator to handle authentication and +command execution. + +There are three major steps: + + - Create a `phabricator-ssh-hook.sh` file. + - Create a `sshd_phabricator` config file. + - Start a copy of `sshd` using the new configuration. + +**Create `phabricator-ssh-hook.sh`**: Copy the template in +`phabricator/resources/sshd/phabricator-ssh-hook.sh` to somewhere like +`/usr/libexec/phabricator-ssh-hook.sh` and edit it to have the correct +settings. Then make it owned by `root` and restrict editing: + + sudo chown root /path/to/phabricator-ssh-hook.sh + sudo chmod 755 /path/to/phabricator-ssh-hook.sh + +If you don't do this, `sshd` will refuse to execute the hook. + +**Create `sshd_config` for Phabricator**: Copy the template in +`phabricator/resources/sshd/sshd_config.phabricator.example` to somewhere like +`/etc/ssh/sshd_config.phabricator`. + +Open the file and edit the `AuthorizedKeysCommand` and +`AuthorizedKeysCommandUser` settings to be correct for your system. + +**Start SSHD**: Now, start the Phabricator `sshd`: + + sudo sshd -f /path/to/sshd_config.phabricator + +If you did everything correctly, you should be able to run this: + + echo {} | ssh vcs-user@phabricator.yourcompany.com conduit conduit.ping + +...and get a response like this: + + {"result":"orbital","error_code":null,"error_info":null} + +(If you get an authentication error, make sure you added your public key in +**Settings > SSH Public Keys**.) + += Authentication Over HTTP = + +To authenticate over HTTP, users should configure a **VCS Password** in the +**Settings** screen. This panel is available only if `diffusion.allow-http-auth` +is enabled. + += Authentication Over SSH = + +To authenticate over SSH, users should add **SSH Public Keys** in the +**Settings** screen. diff --git a/src/docs/user/userguide/jump.diviner b/src/docs/user/userguide/jump.diviner index 06b1e4fa9d..5910484760 100644 --- a/src/docs/user/userguide/jump.diviner +++ b/src/docs/user/userguide/jump.diviner @@ -18,6 +18,7 @@ a navigational command into the box and press return. - **r** - Jump to Diffusion. - **rXYZ** - Jump to Diffusion Repository XYZ. - **rXYZabcdef** - Jump to Diffusion Commit rXYZabcdef. + - **r ** - Search for repositories by name. - **u** - Jump to People - **u username** - Jump to username's Profile - **p** - Jump to Project diff --git a/src/docs/user/userguide/remarkup.diviner b/src/docs/user/userguide/remarkup.diviner index a3d23619f7..b15dcc0e35 100644 --- a/src/docs/user/userguide/remarkup.diviner +++ b/src/docs/user/userguide/remarkup.diviner @@ -18,7 +18,7 @@ formatting text in Remarkup. These are inline styles, and can be applied to most text: - **bold** //italic// ##monospaced## `monospaced` ~~deleted~~ + **bold** //italic// ##monospaced## `monospaced` ~~deleted~~ __underlined__ D123 T123 rX123 # Link to Objects {D123} {T123} # Link to Objects (Full Name) {F123} # Embed Images @@ -58,9 +58,10 @@ Format **basic text styles** like this: ##monospaced text## `monospaced text` ~~deleted text~~ + __underlined text__ Those produce **bold text**, //italic text//, ##monospaced text##, -`monospaced text` and ~~deleted text~~, respectively. +`monospaced text`, ~~deleted text~~, and __underlined text__, respectively. = Layout = @@ -240,9 +241,21 @@ You can use ##lines=N## to limit the vertical size of a chunk of code, and

    Mangostine

    Melon

    -You can also use "NOTE:" to call out an important idea. +You can also use "NOTE:", "WARNING:", or "IMPORTANT:" to call out an important + idea. -NOTE: Don't cross the streams! +NOTE: Best practices in proton pack operation include not crossing the streams. + +WARNING: Crossing the streams can result in total protonic reversal! + +IMPORTANT: Don't cross the streams! + +You can also use "(NOTE)", "(WARNING)", or "(IMPORTANT)" to get the same + effect but without "(NOTE"), "(WARNING)", or "(IMPORTANT)" appearing in the + rendered result. For example + +(NOTE) Dr. Egon Spengler is the best resource for additional proton pack + questions. = Linking URIs = @@ -345,6 +358,8 @@ Valid options are: - **size** thumb (default), full - **name** with `layout=link` or for non-images, use this name for the link text + - **width** Scale image to a specific width. + - **height** Scale image to a specific height. == Embedding Countdowns diff --git a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php index dc0edcfd76..3f42e50c57 100644 --- a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php +++ b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php @@ -12,34 +12,39 @@ final class CelerityPhabricatorResourceController private $path; private $hash; - private $package; + private $library; - protected function getRootDirectory() { - $root = dirname(phutil_get_library_root('phabricator')); - return $root.'/webroot/'; + public function getCelerityResourceMap() { + return CelerityResourceMap::getNamedInstance($this->library); } public function willProcessRequest(array $data) { $this->path = $data['path']; $this->hash = $data['hash']; - $this->package = !empty($data['package']); + $this->library = $data['library']; } public function processRequest() { - $package_hash = null; - if ($this->package) { - $package_hash = $this->hash; + // Check that the resource library exists before trying to serve resources + // from it. + try { + $this->getCelerityResourceMap(); + } catch (Exception $ex) { + return new Aphront400Response(); } - return $this->serveResource($this->path, $package_hash); + + return $this->serveResource($this->path); } protected function buildResourceTransformer() { - $xformer = new CelerityResourceTransformer(); - $xformer->setMinify( - !PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && - PhabricatorEnv::getEnvConfig('celerity.minify')); - $xformer->setCelerityMap(CelerityResourceMap::getInstance()); - return $xformer; + $minify_on = PhabricatorEnv::getEnvConfig('celerity.minify'); + $developer_on = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); + + $should_minify = ($minify_on && !$developer_on); + + return id(new CelerityResourceTransformer()) + ->setMinify($should_minify) + ->setCelerityMap($this->getCelerityResourceMap()); } } diff --git a/src/infrastructure/celerity/CelerityResourceController.php b/src/infrastructure/celerity/CelerityResourceController.php index 8c72134875..17b8403276 100644 --- a/src/infrastructure/celerity/CelerityResourceController.php +++ b/src/infrastructure/celerity/CelerityResourceController.php @@ -2,8 +2,6 @@ abstract class CelerityResourceController extends PhabricatorController { - abstract protected function getRootDirectory(); - protected function buildResourceTransformer() { return null; } @@ -16,9 +14,7 @@ abstract class CelerityResourceController extends PhabricatorController { return false; } - private function getDiskPath($to_resource = null) { - return $this->getRootDirectory().$to_resource; - } + abstract public function getCelerityResourceMap(); protected function serveResource($path, $package_hash = null) { // Sanity checking to keep this from exposing anything sensitive, since it @@ -41,18 +37,18 @@ abstract class CelerityResourceController extends PhabricatorController { return $this->makeResponseCacheable(new Aphront304Response()); } - if ($package_hash) { - $map = CelerityResourceMap::getInstance(); - $paths = $map->resolvePackage($package_hash); - if (!$paths) { + $map = $this->getCelerityResourceMap(); + + if ($map->isPackageResource($path)) { + $resource_names = $map->getResourceNamesForPackageName($path); + if (!$resource_names) { return new Aphront404Response(); } try { $data = array(); - foreach ($paths as $package_path) { - $disk_path = $this->getDiskPath($package_path); - $data[] = Filesystem::readFile($disk_path); + foreach ($resource_names as $resource_name) { + $data[] = $map->getResourceDataForName($resource_name); } $data = implode("\n\n", $data); } catch (Exception $ex) { @@ -60,8 +56,7 @@ abstract class CelerityResourceController extends PhabricatorController { } } else { try { - $disk_path = $this->getDiskPath($path); - $data = Filesystem::readFile($disk_path); + $data = $map->getResourceDataForName($path); } catch (Exception $ex) { return new Aphront404Response(); } diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php index 9157729b75..030fd3233d 100644 --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -5,38 +5,60 @@ * resources, resource dependencies, and packaging information. You generally do * not need to invoke it directly; instead, you call higher-level Celerity APIs * and it uses the resource map to satisfy your requests. - * - * @group celerity */ final class CelerityResourceMap { - private static $instance; - private $resourceMap; + private static $instances = array(); + + private $resources; + private $symbolMap; + private $requiresMap; private $packageMap; - private $reverseMap; + private $nameMap; + private $hashMap; - public static function getInstance() { - if (empty(self::$instance)) { - self::$instance = new CelerityResourceMap(); - $root = phutil_get_library_root('phabricator'); + public function __construct(CelerityResources $resources) { + $this->resources = $resources; - $path = PhabricatorEnv::getEnvConfig('celerity.resource-path'); - $ok = include_once $root.'/'.$path; - if (!$ok) { - throw new Exception( - "Failed to load Celerity resource map! Check the ". - "'celerity.resource-path' setting in your configuration."); + $map = $resources->loadMap(); + $this->symbolMap = idx($map, 'symbols', array()); + $this->requiresMap = idx($map, 'requires', array()); + $this->packageMap = idx($map, 'packages', array()); + $this->nameMap = idx($map, 'names', array()); + + // We derive these reverse maps at runtime. + + $this->hashMap = array_flip($this->nameMap); + $this->componentMap = array(); + foreach ($this->packageMap as $package_name => $symbols) { + foreach ($symbols as $symbol) { + $this->componentMap[$symbol] = $package_name; } } - return self::$instance; } - public function setResourceMap($resource_map) { - $this->resourceMap = $resource_map; - return $this; + public static function getNamedInstance($name) { + if (empty(self::$instances[$name])) { + $resources_list = CelerityPhysicalResources::getAll(); + if (empty($resources_list[$name])) { + throw new Exception( + pht( + 'No resource source exists with name "%s"!', $name)); + } + + $instance = new CelerityResourceMap($resources_list[$name]); + self::$instances[$name] = $instance; + } + + return self::$instances[$name]; } - public function resolveResources(array $symbols) { + public function getPackagedNamesForSymbols(array $symbols) { + $resolved = $this->resolveResources($symbols); + return $this->packageResources($resolved); + } + + private function resolveResources(array $symbols) { $map = array(); foreach ($symbols as $symbol) { if (!empty($map[$symbol])) { @@ -49,75 +71,173 @@ final class CelerityResourceMap { } private function resolveResource(array &$map, $symbol) { - if (empty($this->resourceMap[$symbol])) { + if (empty($this->symbolMap[$symbol])) { throw new Exception( - "Attempting to resolve unknown Celerity resource, '{$symbol}'."); + pht( + 'Attempting to resolve unknown resource, "%s".', + $symbol)); } - $info = $this->resourceMap[$symbol]; - foreach ($info['requires'] as $requires) { - if (!empty($map[$requires])) { + $hash = $this->symbolMap[$symbol]; + + $map[$symbol] = $hash; + + if (isset($this->requiresMap[$hash])) { + $requires = $this->requiresMap[$hash]; + } else { + $requires = array(); + } + + foreach ($requires as $required_symbol) { + if (!empty($map[$required_symbol])) { continue; } - $this->resolveResource($map, $requires); + $this->resolveResource($map, $required_symbol); } - - $map[$symbol] = $info; } - public function setPackageMap($package_map) { - $this->packageMap = $package_map; - return $this; - } - - public function packageResources(array $resolved_map) { + private function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); - foreach ($resolved_map as $symbol => $info) { + foreach ($resolved_map as $symbol => $hash) { if (isset($handled[$symbol])) { continue; } - if (empty($this->packageMap['reverse'][$symbol])) { - $packaged[$symbol] = $info; + + if (empty($this->componentMap[$symbol])) { + $packaged[] = $this->hashMap[$hash]; } else { - $package = $this->packageMap['reverse'][$symbol]; - $package_info = $this->packageMap['packages'][$package]; - $packaged[$package_info['name']] = $package_info; - foreach ($package_info['symbols'] as $packaged_symbol) { - $handled[$packaged_symbol] = true; + $package_name = $this->componentMap[$symbol]; + $packaged[] = $package_name; + + $package_symbols = $this->packageMap[$package_name]; + foreach ($package_symbols as $package_symbol) { + $handled[$package_symbol] = true; } } } + return $packaged; } - public function resolvePackage($package_hash) { - $package = idx($this->packageMap['packages'], $package_hash); - if (!$package) { + public function getResourceDataForName($resource_name) { + return $this->resources->getResourceData($resource_name); + } + + public function getResourceNamesForPackageName($package_name) { + $package_symbols = idx($this->packageMap, $package_name); + if (!$package_symbols) { return null; } - $paths = array(); - foreach ($package['symbols'] as $symbol) { - $paths[] = $this->resourceMap[$symbol]['disk']; + $resource_names = array(); + foreach ($package_symbols as $symbol) { + $resource_names[] = $this->hashMap[$this->symbolMap[$symbol]]; } - return $paths; + return $resource_names; } - public function lookupSymbolInformation($symbol) { - return idx($this->resourceMap, $symbol); - } - public function lookupFileInformation($path) { - if (empty($this->reverseMap)) { - $this->reverseMap = array(); - foreach ($this->resourceMap as $symbol => $data) { - $data['provides'] = $symbol; - $this->reverseMap[$data['disk']] = $data; + /** + * Get the epoch timestamp of the last modification time of a symbol. + * + * @param string Resource symbol to lookup. + * @return int Epoch timestamp of last resource modification. + */ + public function getModifiedTimeForName($name) { + if ($this->isPackageResource($name)) { + $names = array(); + foreach ($this->packageMap[$name] as $symbol) { + $names[] = $this->getResourceNameForSymbol($symbol); } + } else { + $names = array($name); } - return idx($this->reverseMap, $path); + + $mtime = 0; + foreach ($names as $name) { + $mtime = max($mtime, $this->resources->getResourceModifiedTime($name)); + } + + return $mtime; + } + + + /** + * Return the absolute URI for the resource associated with a symbol. This + * method is fairly low-level and ignores packaging. + * + * @param string Resource symbol to lookup. + * @return string|null Resource URI, or null if the symbol is unknown. + */ + public function getURIForSymbol($symbol) { + $hash = idx($this->symbolMap, $symbol); + return $this->getURIForHash($hash); + } + + + /** + * Return the absolute URI for the resource associated with a resource name. + * This method is fairly low-level and ignores packaging. + * + * @param string Resource name to lookup. + * @return string|null Resource URI, or null if the name is unknown. + */ + public function getURIForName($name) { + $hash = idx($this->nameMap, $name); + return $this->getURIForHash($hash); + } + + + /** + * Return the absolute URI for a resource, identified by hash. + * This method is fairly low-level and ignores packaging. + * + * @param string Resource hash to lookup. + * @return string|null Resource URI, or null if the hash is unknown. + */ + private function getURIForHash($hash) { + if ($hash === null) { + return null; + } + return $this->resources->getResourceURI($hash, $this->hashMap[$hash]); + } + + + /** + * Return the resource symbols required by a named resource. + * + * @param string Resource name to lookup. + * @return list|null List of required symbols, or null if the name + * is unknown. + */ + public function getRequiredSymbolsForName($name) { + $hash = idx($this->nameMap, $name); + if ($hash === null) { + return null; + } + return idx($this->requiresMap, $hash, array()); + } + + + /** + * Return the resource name for a given symbol. + * + * @param string Resource symbol to lookup. + * @return string|null Resource name, or null if the symbol is unknown. + */ + public function getResourceNameForSymbol($symbol) { + $hash = idx($this->symbolMap, $symbol); + return idx($this->hashMap, $hash); + } + + public function isPackageResource($name) { + return isset($this->packageMap[$name]); + } + + public function getResourceTypeForName($name) { + return $this->resources->getResourceType($name); } } diff --git a/src/infrastructure/celerity/CelerityResourceTransformer.php b/src/infrastructure/celerity/CelerityResourceTransformer.php index 9ba6805d6e..770528b0be 100644 --- a/src/infrastructure/celerity/CelerityResourceTransformer.php +++ b/src/infrastructure/celerity/CelerityResourceTransformer.php @@ -6,7 +6,7 @@ final class CelerityResourceTransformer { private $minify; - private $rawResourceMap; + private $rawURIMap; private $celerityMap; private $translateURICallback; private $currentPath; @@ -21,16 +21,20 @@ final class CelerityResourceTransformer { return $this; } - public function setRawResourceMap(array $raw_resource_map) { - $this->rawResourceMap = $raw_resource_map; - return $this; - } - public function setCelerityMap(CelerityResourceMap $celerity_map) { $this->celerityMap = $celerity_map; return $this; } + public function setRawURIMap(array $raw_urimap) { + $this->rawURIMap = $raw_urimap; + return $this; + } + + public function getRawURIMap() { + return $this->rawURIMap; + } + /** * @phutil-external-symbol function jsShrink */ @@ -108,14 +112,26 @@ final class CelerityResourceTransformer { public function translateResourceURI(array $matches) { $uri = trim($matches[1], "'\" \r\t\n"); - if ($this->rawResourceMap) { - if (isset($this->rawResourceMap[$uri]['uri'])) { - $uri = $this->rawResourceMap[$uri]['uri']; + $alternatives = array_unique( + array( + $uri, + ltrim($uri, '/'), + )); + + foreach ($alternatives as $alternative) { + if ($this->rawURIMap !== null) { + if (isset($this->rawURIMap[$alternative])) { + $uri = $this->rawURIMap[$alternative]; + break; + } } - } else if ($this->celerityMap) { - $info = $this->celerityMap->lookupFileInformation($uri); - if ($info) { - $uri = $info['uri']; + + if ($this->celerityMap) { + $resource_uri = $this->celerityMap->getURIForName($alternative); + if ($resource_uri) { + $uri = $resource_uri; + break; + } } } diff --git a/src/infrastructure/celerity/CeleritySpriteGenerator.php b/src/infrastructure/celerity/CeleritySpriteGenerator.php index 8bebc20907..3ff0217b1c 100644 --- a/src/infrastructure/celerity/CeleritySpriteGenerator.php +++ b/src/infrastructure/celerity/CeleritySpriteGenerator.php @@ -615,6 +615,33 @@ final class CeleritySpriteGenerator { return $sheet; } + public function buildMainHeaderSheet() { + $gradients = $this->getDirectoryList('main_header'); + $template = new PhutilSprite(); + + $sprites = array(); + foreach ($gradients as $gradient) { + $path = $this->getPath('main_header/'.$gradient.'.png'); + $sprite = id(clone $template) + ->setName('main-header-'.$gradient) + ->setSourceFile($path) + ->setTargetCSS('.main-header-'.$gradient); + $sprite->setSourceSize(6, 44); + $sprites[] = $sprite; + } + + $sheet = $this->buildSheet('main-header', + false, + PhutilSpriteSheet::TYPE_REPEAT_X); + + foreach ($sprites as $sprite) { + $sheet->addSprite($sprite); + } + + return $sheet; + } + + public function buildAppsSheet() { return $this->buildAppsSheetVariant(1); } @@ -695,6 +722,10 @@ final class CeleritySpriteGenerator { $css .= ', .phabricator-crumb-view:hover .apps-'.$app.'-dark-large'; } + if ($color == 'white' && $variant == 1) { + $css .= ', .phui-list-item-href:hover .apps-'.$app.'-dark'; + } + $sprite = id(clone $template) ->setName('apps-'.$app.'-'.$color.$variant_short) ->setTargetCSS($css); diff --git a/src/infrastructure/celerity/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php index ccde0280e2..21ad591e87 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -39,8 +39,12 @@ final class CelerityStaticResourceResponse { * a behavior will execute only once even if it is initialized multiple times. * If $config is nonempty, the behavior will be invoked once for each config. */ - public function initBehavior($behavior, array $config = array()) { - $this->requireResource('javelin-behavior-'.$behavior); + public function initBehavior( + $behavior, + array $config = array(), + $source_name = 'phabricator') { + + $this->requireResource('javelin-behavior-'.$behavior, $source_name); if (empty($this->behaviors[$behavior])) { $this->behaviors[$behavior] = array(); @@ -53,59 +57,96 @@ final class CelerityStaticResourceResponse { return $this; } - public function requireResource($symbol) { - $this->symbols[$symbol] = true; + public function requireResource($symbol, $source_name) { + if (isset($this->symbols[$source_name][$symbol])) { + return $this; + } + + // Verify that the resource exists. + $map = CelerityResourceMap::getNamedInstance($source_name); + $name = $map->getResourceNameForSymbol($symbol); + if ($name === null) { + throw new Exception( + pht( + 'No resource with symbol "%s" exists in source "%s"!', + $symbol, + $source_name)); + } + + $this->symbols[$source_name][$symbol] = true; $this->needsResolve = true; + return $this; } private function resolveResources() { if ($this->needsResolve) { - $map = CelerityResourceMap::getInstance(); - $this->resolved = $map->resolveResources(array_keys($this->symbols)); - $this->packaged = $map->packageResources($this->resolved); + $this->packaged = array(); + foreach ($this->symbols as $source_name => $symbols_map) { + $symbols = array_keys($symbols_map); + + $map = CelerityResourceMap::getNamedInstance($source_name); + $packaged = $map->getPackagedNamesForSymbols($symbols); + + $this->packaged[$source_name] = $packaged; + } $this->needsResolve = false; } return $this; } - public function renderSingleResource($symbol) { - $map = CelerityResourceMap::getInstance(); - $resolved = $map->resolveResources(array($symbol)); - $packaged = $map->packageResources($resolved); - return $this->renderPackagedResources($packaged); + public function renderSingleResource($symbol, $source_name) { + $map = CelerityResourceMap::getNamedInstance($source_name); + $packaged = $map->getPackagedNamesForSymbols(array($symbol)); + return $this->renderPackagedResources($map, $packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); - $resources = array(); - foreach ($this->packaged as $resource) { - if ($resource['type'] == $type) { - $resources[] = $resource; + $result = array(); + foreach ($this->packaged as $source_name => $resource_names) { + $map = CelerityResourceMap::getNamedInstance($source_name); + + $resources_of_type = array(); + foreach ($resource_names as $resource_name) { + $resource_type = $map->getResourceTypeForName($resource_name); + if ($resource_type == $type) { + $resources_of_type[] = $resource_name; + } } + + $result[] = $this->renderPackagedResources($map, $resources_of_type); } - return $this->renderPackagedResources($resources); + return phutil_implode_html('', $result); } - private function renderPackagedResources(array $resources) { + private function renderPackagedResources( + CelerityResourceMap $map, + array $resources) { + $output = array(); - foreach ($resources as $resource) { - if (isset($this->hasRendered[$resource['uri']])) { + foreach ($resources as $name) { + if (isset($this->hasRendered[$name])) { continue; } - $this->hasRendered[$resource['uri']] = true; + $this->hasRendered[$name] = true; - $output[] = $this->renderResource($resource); - $output[] = "\n"; + $output[] = $this->renderResource($map, $name); } - return phutil_implode_html('', $output); + + return $output; } - private function renderResource(array $resource) { - $uri = $this->getURI($resource); - switch ($resource['type']) { + private function renderResource( + CelerityResourceMap $map, + $name) { + + $uri = $this->getURI($map, $name); + $type = $map->getResourceTypeForName($name); + + switch ($type) { case 'css': return phutil_tag( 'link', @@ -123,7 +164,12 @@ final class CelerityStaticResourceResponse { ), ''); } - throw new Exception("Unable to render resource."); + + throw new Exception( + pht( + 'Unable to render resource "%s", which has unknown type "%s".', + $name, + $type)); } public function renderHTMLFooter() { @@ -196,10 +242,11 @@ final class CelerityStaticResourceResponse { if (strpos($data, ' because it is ignored by HTML parsers. We - // would need to send the document with XHTML content type. - '', + // We don't use because it is ignored by HTML parsers. We + // would need to send the document with XHTML content type. + return phutil_tag( + 'script', + array('type' => 'text/javascript'), phutil_safe_html($data)); } @@ -221,8 +268,11 @@ final class CelerityStaticResourceResponse { $this->resolveResources(); $resources = array(); - foreach ($this->packaged as $resource) { - $resources[] = $this->getURI($resource); + foreach ($this->packaged as $source_name => $resource_names) { + $map = CelerityResourceMap::getNamedInstance($source_name); + foreach ($resource_names as $resource_name) { + $resources[] = $this->getURI($map, $resource_name); + } } if ($resources) { $response['javelin_resources'] = $resources; @@ -231,8 +281,11 @@ final class CelerityStaticResourceResponse { return $response; } - private function getURI($resource) { - $uri = $resource['uri']; + private function getURI( + CelerityResourceMap $map, + $name) { + + $uri = $map->getURIForName($name); // In developer mode, we dump file modification times into the URI. When a // page is reloaded in the browser, any resources brought in by Ajax calls @@ -241,18 +294,7 @@ final class CelerityStaticResourceResponse { // the map script). In production, we can assume the map script gets run // after changes, and safely skip this. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { - $root = dirname(phutil_get_library_root('phabricator')).'/webroot'; - if (isset($resource['disk'])) { - $mtime = (int)filemtime($root.$resource['disk']); - } else { - $mtime = 0; - foreach ($resource['symbols'] as $symbol) { - $map = CelerityResourceMap::getInstance(); - $symbol_info = $map->lookupSymbolInformation($symbol); - $mtime = max($mtime, (int)filemtime($root.$symbol_info['disk'])); - } - } - + $mtime = $map->getModifiedTimeForName($name); $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } diff --git a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php index 883e415540..f23862b8d0 100644 --- a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php +++ b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php @@ -22,11 +22,9 @@ final class CelerityResourceTransformerTestCase extends PhabricatorTestCase { ); $xformer = new CelerityResourceTransformer(); - $xformer->setRawResourceMap( + $xformer->setRawURIMap( array( - '/rsrc/example.png' => array( - 'uri' => '/res/hash/example.png', - ), + '/rsrc/example.png' => '/res/hash/example.png', )); $xformer->setMinify($options['minify']); diff --git a/src/infrastructure/celerity/api.php b/src/infrastructure/celerity/api.php index fa615f32ad..c10b7a1d40 100644 --- a/src/infrastructure/celerity/api.php +++ b/src/infrastructure/celerity/api.php @@ -15,9 +15,9 @@ * * @group celerity */ -function require_celerity_resource($symbol) { +function require_celerity_resource($symbol, $source_name = 'phabricator') { $response = CelerityAPI::getStaticResourceResponse(); - $response->requireResource($symbol); + $response->requireResource($symbol, $source_name); } @@ -49,13 +49,13 @@ function celerity_generate_unique_node_id() { * * @group celerity */ -function celerity_get_resource_uri($resource) { - $map = CelerityResourceMap::getInstance(); +function celerity_get_resource_uri($resource, $source = 'phabricator') { + $map = CelerityResourceMap::getNamedInstance($source); - $info = $map->lookupFileInformation($resource); - if ($info) { - return $info['uri']; - } else { - return $resource; + $uri = $map->getURIForName($resource); + if ($uri) { + return $uri; } + + return $resource; } diff --git a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php b/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php new file mode 100644 index 0000000000..0795ac5467 --- /dev/null +++ b/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php @@ -0,0 +1,372 @@ +setName('map') + ->setExamples('**map** [options]') + ->setSynopsis(pht('Rebuild static resource maps.')) + ->setArguments( + array()); + } + + public function execute(PhutilArgumentParser $args) { + $resources_map = CelerityPhysicalResources::getAll(); + + $this->log( + pht( + "Rebuilding %d resource source(s).", + new PhutilNumber(count($resources_map)))); + + foreach ($resources_map as $name => $resources) { + $this->rebuildResources($resources); + } + + $this->log(pht("Done.")); + + return 0; + } + + /** + * Rebuild the resource map for a resource source. + * + * @param CelerityPhysicalResources Resource source to rebuild. + * @return void + */ + private function rebuildResources(CelerityPhysicalResources $resources) { + $this->log( + pht( + 'Rebuilding resource source "%s" (%s)...', + $resources->getName(), + get_class($resources))); + + $binary_map = $this->rebuildBinaryResources($resources); + + $this->log( + pht( + 'Found %d binary resources.', + new PhutilNumber(count($binary_map)))); + + $xformer = id(new CelerityResourceTransformer()) + ->setMinify(false) + ->setRawURIMap(ipull($binary_map, 'uri')); + + $text_map = $this->rebuildTextResources($resources, $xformer); + + $this->log( + pht( + 'Found %d text resources.', + new PhutilNumber(count($text_map)))); + + $resource_graph = array(); + $requires_map = array(); + $symbol_map = array(); + foreach ($text_map as $name => $info) { + if (isset($info['provides'])) { + $symbol_map[$info['provides']] = $info['hash']; + + // We only need to check for cycles and add this to the requires map + // if it actually requires anything. + if (!empty($info['requires'])) { + $resource_graph[$info['provides']] = $info['requires']; + $requires_map[$info['hash']] = $info['requires']; + } + } + } + + $this->detectGraphCycles($resource_graph); + $name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash'); + $hash_map = array_flip($name_map); + + $package_map = $this->rebuildPackages( + $resources, + $symbol_map, + $hash_map); + + $this->log( + pht( + 'Found %d packages.', + new PhutilNumber(count($package_map)))); + + $component_map = array(); + foreach ($package_map as $package_name => $package_info) { + foreach ($package_info['symbols'] as $symbol) { + $component_map[$symbol] = $package_name; + } + } + + $name_map = $this->mergeNameMaps( + array( + array(pht('Binary'), ipull($binary_map, 'hash')), + array(pht('Text'), ipull($text_map, 'hash')), + array(pht('Package'), ipull($package_map, 'hash')), + )); + $package_map = ipull($package_map, 'symbols'); + + ksort($name_map); + ksort($symbol_map); + ksort($requires_map); + ksort($package_map); + + $map_content = $this->formatMapContent(array( + 'names' => $name_map, + 'symbols' => $symbol_map, + 'requires' => $requires_map, + 'packages' => $package_map, + )); + + $map_path = $resources->getPathToMap(); + $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path))); + Filesystem::writeFile($map_path, $map_content); + } + + + /** + * Find binary resources (like PNG and SWF) and return information about + * them. + * + * @param CelerityPhysicalResources Resource map to find binary resources for. + * @return map> Resource information map. + */ + private function rebuildBinaryResources( + CelerityPhysicalResources $resources) { + + $binary_map = $resources->findBinaryResources(); + + $result_map = array(); + foreach ($binary_map as $name => $data_hash) { + $hash = $resources->getCelerityHash($data_hash.$name); + + $result_map[$name] = array( + 'hash' => $hash, + 'uri' => $resources->getResourceURI($hash, $name), + ); + } + + return $result_map; + } + + + /** + * Find text resources (like JS and CSS) and return information about them. + * + * @param CelerityPhysicalResources Resource map to find text resources for. + * @param CelerityResourceTransformer Configured resource transformer. + * @return map> Resource information map. + */ + private function rebuildTextResources( + CelerityPhysicalResources $resources, + CelerityResourceTransformer $xformer) { + + $text_map = $resources->findTextResources(); + + $result_map = array(); + foreach ($text_map as $name => $data_hash) { + $raw_data = $resources->getResourceData($name); + $xformed_data = $xformer->transformResource($name, $raw_data); + + $data_hash = $resources->getCelerityHash($xformed_data); + $hash = $resources->getCelerityHash($data_hash.$name); + + list($provides, $requires) = $this->getProvidesAndRequires( + $name, + $raw_data); + + $result_map[$name] = array( + 'hash' => $hash, + ); + + if ($provides !== null) { + $result_map[$name] += array( + 'provides' => $provides, + 'requires' => $requires, + ); + } + } + + return $result_map; + } + + + /** + * Parse the `@provides` and `@requires` symbols out of a text resource, like + * JS or CSS. + * + * @param string Resource name. + * @param string Resource data. + * @return pair|nul> The `@provides` symbol and the + * list of `@requires` symbols. If the resource is not part of the + * dependency graph, both are null. + */ + private function getProvidesAndRequires($name, $data) { + $parser = new PhutilDocblockParser(); + + $matches = array(); + $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); + if (!$ok) { + throw new Exception( + pht( + 'Resource "%s" does not have a header doc comment. Encode '. + 'dependency data in a header docblock.', + $name)); + } + + list($description, $metadata) = $parser->parse($matches[0]); + + $provides = preg_split('/\s+/', trim(idx($metadata, 'provides'))); + $requires = preg_split('/\s+/', trim(idx($metadata, 'requires'))); + $provides = array_filter($provides); + $requires = array_filter($requires); + + if (!$provides) { + // Tests and documentation-only JS is permitted to @provide no targets. + return array(null, null); + } + + if (count($provides) > 1) { + throw new Exception( + pht( + 'Resource "%s" must @provide at most one Celerity target.', + $name)); + } + + return array(head($provides), $requires); + } + + + /** + * Check for dependency cycles in the resource graph. Raises an exception if + * a cycle is detected. + * + * @param map> Map of `@provides` symbols to their + * `@requires` symbols. + * @return void + */ + private function detectGraphCycles(array $nodes) { + $graph = id(new CelerityResourceGraph()) + ->addNodes($nodes) + ->setResourceGraph($nodes) + ->loadGraph(); + + foreach ($nodes as $provides => $requires) { + $cycle = $graph->detectCycles($provides); + if ($cycle) { + throw new Exception( + pht( + 'Cycle detected in resource graph: %s', + implode(' > ', $cycle))); + } + } + } + + /** + * Build package specifications for a given resource source. + * + * @param CelerityPhysicalResources Resource source to rebuild. + * @param list Map of `@provides` to hashes. + * @param list Map of hashes to resource names. + * @return map> Package information maps. + */ + private function rebuildPackages( + CelerityPhysicalResources $resources, + array $symbol_map, + array $reverse_map) { + + $package_map = array(); + + $package_spec = $resources->getResourcePackages(); + foreach ($package_spec as $package_name => $package_symbols) { + $type = null; + $hashes = array(); + foreach ($package_symbols as $symbol) { + $symbol_hash = idx($symbol_map, $symbol); + if ($symbol_hash === null) { + throw new Exception( + pht( + 'Package specification for "%s" includes "%s", but that symbol '. + 'is not @provided by any resource.', + $package_name, + $symbol)); + } + + $resource_name = $reverse_map[$symbol_hash]; + $resource_type = $resources->getResourceType($resource_name); + if ($type === null) { + $type = $resource_type; + } else if ($type !== $resource_type) { + throw new Exception( + pht( + 'Package specification for "%s" includes resources of multiple '. + 'types (%s, %s). Each package may only contain one type of '. + 'resource.', + $package_name, + $type, + $resource_type)); + } + + $hashes[] = $symbol.':'.$symbol_hash; + } + + $hash = $resources->getCelerityHash(implode("\n", $hashes)); + $package_map[$package_name] = array( + 'hash' => $hash, + 'symbols' => $package_symbols, + ); + } + + return $package_map; + } + + private function mergeNameMaps(array $maps) { + $result = array(); + $origin = array(); + foreach ($maps as $map) { + list($map_name, $data) = $map; + foreach ($data as $name => $hash) { + if (empty($result[$name])) { + $result[$name] = $hash; + $origin[$name] = $map_name; + } else { + $old = $origin[$name]; + $new = $map_name; + throw new Exception( + pht( + 'Resource source defines two resources with the same name, '. + '"%s". One is defined in the "%s" map; the other in the "%s" '. + 'map. Each resource must have a unique name.', + $name, + $old, + $new)); + } + } + } + return $result; + } + + private function log($message) { + $console = PhutilConsole::getConsole(); + $console->writeErr("%s\n", $message); + } + + private function formatMapContent(array $data) { + $content = var_export($data, true); + $content = preg_replace('/\s+$/m', '', $content); + $content = preg_replace('/array \(/', 'array(', $content); + + $generated = '@'.'generated'; + return <<setResourceMap($map); - $instance->setPackageMap($package_map); -} diff --git a/src/infrastructure/celerity/resources/CelerityPhabricatorResources.php b/src/infrastructure/celerity/resources/CelerityPhabricatorResources.php new file mode 100644 index 0000000000..2729e62359 --- /dev/null +++ b/src/infrastructure/celerity/resources/CelerityPhabricatorResources.php @@ -0,0 +1,28 @@ +getPhabricatorPath('webroot/'); + } + + public function getPathToMap() { + return $this->getPhabricatorPath('resources/celerity/map.php'); + } + + private function getPhabricatorPath($to_file) { + return dirname(phutil_get_library_root('phabricator')).'/'.$to_file; + } + + public function getResourcePackages() { + return include $this->getPhabricatorPath('resources/celerity/packages.php'); + } + +} diff --git a/src/infrastructure/celerity/resources/CelerityPhysicalResources.php b/src/infrastructure/celerity/resources/CelerityPhysicalResources.php new file mode 100644 index 0000000000..995b9f1a55 --- /dev/null +++ b/src/infrastructure/celerity/resources/CelerityPhysicalResources.php @@ -0,0 +1,61 @@ +map === null) { + $this->map = include $this->getPathToMap(); + } + return $this->map; + } + + public static function getAll() { + static $resources_map; + if ($resources_map === null) { + $resources_map = array(); + + $resources_list = id(new PhutilSymbolLoader()) + ->setAncestorClass('CelerityPhysicalResources') + ->loadObjects(); + + foreach ($resources_list as $resources) { + $name = $resources->getName(); + + if (!preg_match('/^[a-z0-9]+/', $name)) { + throw new Exception( + pht( + 'Resources name "%s" is not valid; it must contain only '. + 'lowercase latin letters and digits.', + $name)); + } + + if (empty($resources_map[$name])) { + $resources_map[$name] = $resources; + } else { + $old = get_class($resources_map[$name]); + $new = get_class($resources); + throw new Exception( + pht( + 'Celerity resource maps must have unique names, but maps %s and '. + '%s share the same name, "%s".', + $old, + $new, + $name)); + } + } + } + + return $resources_map; + } + +} diff --git a/src/infrastructure/celerity/resources/CelerityResources.php b/src/infrastructure/celerity/resources/CelerityResources.php new file mode 100644 index 0000000000..a9604d9442 --- /dev/null +++ b/src/infrastructure/celerity/resources/CelerityResources.php @@ -0,0 +1,40 @@ +getName(); + return "/res/{$resources}/{$hash}/{$name}"; + } + + public function getResourcePackages() { + return array(); + } + + public function loadMap() { + return array(); + } + +} diff --git a/src/infrastructure/celerity/resources/CelerityResourcesOnDisk.php b/src/infrastructure/celerity/resources/CelerityResourcesOnDisk.php new file mode 100644 index 0000000000..0aa46bb3d9 --- /dev/null +++ b/src/infrastructure/celerity/resources/CelerityResourcesOnDisk.php @@ -0,0 +1,69 @@ +getPathToResources().DIRECTORY_SEPARATOR.$name; + } + + public function getResourceData($name) { + return Filesystem::readFile($this->getPathToResource($name)); + } + + public function findBinaryResources() { + return $this->findResourcesWithSuffixes($this->getBinaryFileSuffixes()); + } + + public function findTextResources() { + return $this->findResourcesWithSuffixes($this->getTextFileSuffixes()); + } + + public function getResourceModifiedTime($name) { + return (int)filemtime($this->getPathToResource($name)); + } + + protected function getBinaryFileSuffixes() { + return array( + 'png', + 'jpg', + 'gif', + 'swf', + ); + } + + protected function getTextFileSuffixes() { + return array( + 'js', + 'css', + ); + } + + private function findResourcesWithSuffixes(array $suffixes) { + $root = $this->getPathToResources(); + + $finder = id(new FileFinder($root)) + ->withType('f') + ->withFollowSymlinks(true) + ->setGenerateChecksums(true); + + foreach ($suffixes as $suffix) { + $finder->withSuffix($suffix); + } + + $raw_files = $finder->find(); + + $results = array(); + foreach ($raw_files as $path => $hash) { + $readable = Filesystem::readablePath($path, $root); + $results[$readable] = $hash; + } + + return $results; + } + +} diff --git a/src/infrastructure/daemon/PhabricatorDaemon.php b/src/infrastructure/daemon/PhabricatorDaemon.php index b68518b07d..349e0f0ffe 100644 --- a/src/infrastructure/daemon/PhabricatorDaemon.php +++ b/src/infrastructure/daemon/PhabricatorDaemon.php @@ -19,4 +19,35 @@ abstract class PhabricatorDaemon extends PhutilDaemon { return PhabricatorUser::getOmnipotentUser(); } + + /** + * Format a command so it executes as the daemon user, if a daemon user is + * defined. This wraps the provided command in `sudo -u ...`, roughly. + * + * @param PhutilCommandString Command to execute. + * @return PhutilCommandString `sudo` version of the command. + */ + public static function sudoCommandAsDaemonUser($command) { + $user = PhabricatorEnv::getEnvConfig('phd.user'); + if (!$user) { + // No daemon user is set, so just run this as ourselves. + return $command; + } + + // Get the absolute path so we're safe against the caller wiping out + // PATH. + $sudo = Filesystem::resolveBinary('sudo'); + if (!$sudo) { + throw new Exception(pht("Unable to find 'sudo'!")); + } + + // Flags here are: + // + // -E: Preserve the environment. + // -n: Non-interactive. Exit with an error instead of prompting. + // -u: Which user to sudo to. + + return csprintf('%s -E -n -u %s -- %C', $sudo, $user, $command); + } + } diff --git a/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php b/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php deleted file mode 100644 index 4e9bfda28a..0000000000 --- a/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php +++ /dev/null @@ -1,264 +0,0 @@ -collectHeraldTranscripts(); - $n_daemon = $this->collectDaemonLogs(); - $n_parse = $this->collectParseCaches(); - $n_markup = $this->collectMarkupCaches(); - $n_tasks = $this->collectArchivedTasks(); - $n_cache_ttl = $this->collectGeneralCacheTTL(); - $n_cache = $this->collectGeneralCaches(); - $n_files = $this->collectExpiredFiles(); - $n_clogs = $this->collectExpiredConduitLogs(); - $n_ccons = $this->collectExpiredConduitConnections(); - - $collected = array( - 'Herald Transcript' => $n_herald, - 'Daemon Log' => $n_daemon, - 'Differential Parse Cache' => $n_parse, - 'Markup Cache' => $n_markup, - 'Archived Tasks' => $n_tasks, - 'General Cache TTL' => $n_cache_ttl, - 'General Cache Entries' => $n_cache, - 'Temporary Files' => $n_files, - 'Conduit Logs' => $n_clogs, - 'Conduit Connections' => $n_ccons, - ); - $collected = array_filter($collected); - - foreach ($collected as $thing => $count) { - $count = number_format($count); - $this->log("Garbage collected {$count} '{$thing}' objects."); - } - - $total = array_sum($collected); - if ($total < 100) { - // We didn't max out any of the GCs so we're basically caught up. Ease - // off the GC loop so we don't keep doing table scans just to delete - // a handful of rows; wake up in a few hours. - $this->sleep(4 * (60 * 60)); - } else { - $this->stillWorking(); - } - } while (true); - - } - - private function collectHeraldTranscripts() { - $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.herald-transcripts'); - if ($ttl <= 0) { - return 0; - } - - $table = new HeraldTranscript(); - $conn_w = $table->establishConnection('w'); - - queryfx( - $conn_w, - 'UPDATE %T SET - objectTranscript = "", - ruleTranscripts = "", - conditionTranscripts = "", - applyTranscripts = "", - garbageCollected = 1 - WHERE garbageCollected = 0 AND `time` < %d - LIMIT 100', - $table->getTableName(), - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - - private function collectDaemonLogs() { - $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.daemon-logs'); - if ($ttl <= 0) { - return 0; - } - - $table = new PhabricatorDaemonLogEvent(); - $conn_w = $table->establishConnection('w'); - - queryfx( - $conn_w, - 'DELETE FROM %T WHERE epoch < %d LIMIT 100', - $table->getTableName(), - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - - private function collectParseCaches() { - $key = 'gcdaemon.ttl.differential-parse-cache'; - $ttl = PhabricatorEnv::getEnvConfig($key); - if ($ttl <= 0) { - return 0; - } - - $table = new DifferentialChangeset(); - $conn_w = $table->establishConnection('w'); - - queryfx( - $conn_w, - 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', - DifferentialChangeset::TABLE_CACHE, - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - - private function collectMarkupCaches() { - $key = 'gcdaemon.ttl.markup-cache'; - $ttl = PhabricatorEnv::getEnvConfig($key); - if ($ttl <= 0) { - return 0; - } - - $table = new PhabricatorMarkupCache(); - $conn_w = $table->establishConnection('w'); - - queryfx( - $conn_w, - 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', - $table->getTableName(), - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - - private function collectArchivedTasks() { - $key = 'gcdaemon.ttl.task-archive'; - $ttl = PhabricatorEnv::getEnvConfig($key); - if ($ttl <= 0) { - return 0; - } - - $table = new PhabricatorWorkerArchiveTask(); - $data_table = new PhabricatorWorkerTaskData(); - $conn_w = $table->establishConnection('w'); - - $rows = queryfx_all( - $conn_w, - 'SELECT id, dataID FROM %T WHERE dateCreated < %d LIMIT 100', - $table->getTableName(), - time() - $ttl); - - if (!$rows) { - return 0; - } - - $data_ids = array_filter(ipull($rows, 'dataID')); - $task_ids = ipull($rows, 'id'); - - $table->openTransaction(); - if ($data_ids) { - queryfx( - $conn_w, - 'DELETE FROM %T WHERE id IN (%Ld)', - $data_table->getTableName(), - $data_ids); - } - queryfx( - $conn_w, - 'DELETE FROM %T WHERE id IN (%Ld)', - $table->getTableName(), - $task_ids); - $table->saveTransaction(); - - return count($task_ids); - } - - - private function collectGeneralCacheTTL() { - $cache = new PhabricatorKeyValueDatabaseCache(); - $conn_w = $cache->establishConnection('w'); - - queryfx( - $conn_w, - 'DELETE FROM %T WHERE cacheExpires < %d - ORDER BY cacheExpires ASC LIMIT 100', - $cache->getTableName(), - time()); - - return $conn_w->getAffectedRows(); - } - - - private function collectGeneralCaches() { - $key = 'gcdaemon.ttl.general-cache'; - $ttl = PhabricatorEnv::getEnvConfig($key); - if ($ttl <= 0) { - return 0; - } - - $cache = new PhabricatorKeyValueDatabaseCache(); - $conn_w = $cache->establishConnection('w'); - - queryfx( - $conn_w, - 'DELETE FROM %T WHERE cacheCreated < %d - ORDER BY cacheCreated ASC LIMIT 100', - $cache->getTableName(), - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - - private function collectExpiredFiles() { - $files = id(new PhabricatorFile())->loadAllWhere('ttl < %d LIMIT 100', - time()); - - foreach ($files as $file) { - $file->delete(); - } - - return count($files); - } - - private function collectExpiredConduitLogs() { - $key = 'gcdaemon.ttl.conduit-logs'; - $ttl = PhabricatorEnv::getEnvConfig($key); - if ($ttl <= 0) { - return 0; - } - - $table = new PhabricatorConduitMethodCallLog(); - $conn_w = $table->establishConnection('w'); - queryfx( - $conn_w, - 'DELETE FROM %T WHERE dateCreated < %d - ORDER BY dateCreated ASC LIMIT 100', - $table->getTableName(), - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - - private function collectExpiredConduitConnections() { - $key = 'gcdaemon.ttl.conduit-logs'; - $ttl = PhabricatorEnv::getEnvConfig($key); - if ($ttl <= 0) { - return 0; - } - - $table = new PhabricatorConduitConnectionLog(); - $conn_w = $table->establishConnection('w'); - queryfx( - $conn_w, - 'DELETE FROM %T WHERE dateCreated < %d - ORDER BY dateCreated ASC LIMIT 100', - $table->getTableName(), - time() - $ttl); - - return $conn_w->getAffectedRows(); - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php index 4f43b04399..9d7ac4a19f 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php @@ -83,12 +83,16 @@ final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler { $pattern = '@'. '(?getCommand()) { case 'MESSAGE': $message_body = $message->getBody(); + $now = time(); $prompt = '~what( i|\')?s new\?~i'; if (preg_match($prompt, $message_body)) { - if (time() < $this->floodblock) { + if ($now < $this->floodblock) { return; } - $this->floodblock = time() + 60; - - $this->getLatest($message); + $this->floodblock = $now + 60; + $this->reportNew($message); } break; } } - public function getLatest(PhabricatorBotMessage $message) { + public function reportNew(PhabricatorBotMessage $message) { $latest = $this->getConduit()->callMethodSynchronous( 'feed.query', array( - 'limit' => 5 + 'limit' => 5, + 'view' => 'text' )); - $phids = array(); - foreach ($latest as $action) { - if (isset($action['data']['actor_phid'])) { - $uid = $action['data']['actor_phid']; - } else { - $uid = $action['authorPHID']; + foreach ($latest as $feed_item) { + if (isset($feed_item['text'])) { + $this->replyTo($message, html_entity_decode($feed_item['text'])); } - - switch ($action['class']) { - case 'PhabricatorFeedStoryDifferential': - $phids[] = $action['data']['revision_phid']; - break; - case 'PhabricatorFeedStoryAudit': - $phids[] = $action['data']['commitPHID']; - break; - - case 'PhabricatorFeedStoryManiphest': - $phids[] = $action['data']['taskPHID']; - break; - - default: - $phids[] = $uid; - break; - } - array_push($phids, $uid); - } - - $infs = $this->getConduit()->callMethodSynchronous( - 'phid.query', - array( - 'phids'=>$phids - )); - - $cphid = 0; - foreach ($latest as $action) { - if (isset($action['data']['actor_phid'])) { - $uid = $action['data']['actor_phid']; - } else { - $uid = $action['authorPHID']; - } - switch ($action['class']) { - case 'PhabricatorFeedStoryDifferential': - $rinf = $infs[$action['data']['revision_phid']]; - break; - - case 'PhabricatorFeedStoryAudit': - $rinf = $infs[$action['data']['commitPHID']]; - break; - - case 'PhabricatorFeedStoryManiphest': - $rinf = $infs[$action['data']['taskPHID']]; - break; - - default: - $rinf = array('name'=>$action['class']); - break; - } - $uinf = $infs[$uid]; - - $action = $this->getRhetoric($action['data']['action']); - $user = $uinf['name']; - $title = $rinf['fullName']; - $uri = $rinf['uri']; - $color = chr(3); - $blue = $color.'12'; - $gray = $color.'15'; - $bold = chr(2); - $reset = chr(15); - // Disabling irc-specific styling, at least for now - // $content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}". - // "{$title}{$reset} - {$gray}{$uri}{$reset}"; - $content = "{$user} {$action} {$title} - {$uri}"; - $this->replyTo($message, $content); - } - return; - } - - public function getRhetoric($input) { - switch ($input) { - case 'comment': - case 'none': - return 'commented on'; - break; - case 'update': - return 'updated'; - break; - case 'commit': - return 'closed'; - break; - case 'create': - return 'created'; - break; - case 'concern': - return 'raised concern for'; - break; - case 'abandon': - return 'abandonned'; - break; - case 'accept': - return 'accepted'; - break; - default: - return $input; - break; } } + } diff --git a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php new file mode 100644 index 0000000000..5aea82c845 --- /dev/null +++ b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php @@ -0,0 +1,12 @@ +setAncestorClass('PhabricatorGarbageCollector') + ->loadObjects(); + + do { + foreach ($collectors as $name => $collector) { + $more_garbage = false; + do { + if ($more_garbage) { + $this->log(pht('Collecting more garbage with "%s".', $name)); + } else { + $this->log(pht('Collecting garbage with "%s".', $name)); + } + + $more_garbage = $collector->collectGarbage(); + $this->stillWorking(); + } while ($more_garbage); + } + + // We made it to the end of the run cycle of every GC, so we're more or + // less caught up. Ease off the GC loop so we don't keep doing table + // scans just to delete a handful of rows; wake up in a few hours. + $this->log(pht('All caught up, waiting for more garbage.')); + $this->sleep(4 * (60 * 60)); + } while (true); + + } + +} diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php index a6096919f0..4a25cf9f8c 100644 --- a/src/infrastructure/daemon/workers/PhabricatorWorker.php +++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php @@ -85,14 +85,28 @@ abstract class PhabricatorWorker { } final public static function scheduleTask($task_class, $data) { + $task = id(new PhabricatorWorkerActiveTask()) + ->setTaskClass($task_class) + ->setData($data); + if (self::$runAllTasksInProcess) { + // Do the work in-process. $worker = newv($task_class, array($data)); $worker->doWork(); + + // Now, save a task row and immediately archive it so we can return an + // object with a valid ID. + $task->openTransaction(); + $task->save(); + $archived = $task->archiveTask( + PhabricatorWorkerArchiveTask::RESULT_SUCCESS, + 0); + $task->saveTransaction(); + + return $archived; } else { - return id(new PhabricatorWorkerActiveTask()) - ->setTaskClass($task_class) - ->setData($data) - ->save(); + $task->save(); + return $task; } } @@ -105,6 +119,10 @@ abstract class PhabricatorWorker { * @return void */ final public static function waitForTasks(array $task_ids) { + if (!$task_ids) { + return; + } + $task_table = new PhabricatorWorkerActiveTask(); $waiting = array_fuse($task_ids); @@ -149,12 +167,13 @@ abstract class PhabricatorWorker { foreach ($tasks as $task) { if ($task->getResult() != PhabricatorWorkerArchiveTask::RESULT_SUCCESS) { - throw new Exception("Task ".$task->getID()." failed!"); + throw new Exception( + pht("Task %d failed!", $task->getID())); } } } - public function renderForDisplay() { + public function renderForDisplay(PhabricatorUser $viewer) { $data = PhutilReadableSerializer::printableValue($this->data); return phutil_tag('pre', array(), $data); } diff --git a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php index e58650371f..7e5c5f9416 100644 --- a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php +++ b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php @@ -49,7 +49,6 @@ final class PhabricatorWorkerTestCase extends PhabricatorTestCase { $this->expectNextLease($task1); } - public function testExecuteTask() { $task = $this->scheduleAndExecuteTask(); @@ -155,6 +154,24 @@ final class PhabricatorWorkerTestCase extends PhabricatorTestCase { $this->assertEqual(true, ($task->getLeaseExpires() - time()) > 1000); } + public function testLeasedIsOldestFirst() { + // Tasks which expired earlier should lease first, all else being equal. + + $task1 = $this->scheduleTask(); + $task2 = $this->scheduleTask(); + + $task1->setLeaseOwner('test'); + $task1->setLeaseExpires(time() - 100000); + $task1->forceSaveWithoutLease(); + + $task2->setLeaseOwner('test'); + $task2->setLeaseExpires(time() - 200000); + $task2->forceSaveWithoutLease(); + + $this->expectNextLease($task2); + $this->expectNextLease($task1); + } + private function expectNextLease($task) { $leased = id(new PhabricatorWorkerLeaseQuery()) ->setLimit(1) diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php index e7b07ba844..507a8b6c44 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php @@ -60,7 +60,7 @@ final class PhabricatorWorkerLeaseQuery extends PhabricatorQuery { 'SELECT id, leaseOwner FROM %T %Q %Q %Q', $task_table->getTableName(), $this->buildWhereClause($conn_w, $phase), - $this->buildOrderClause($conn_w), + $this->buildOrderClause($conn_w, $phase), $this->buildLimitClause($conn_w, $limit - $leased)); // NOTE: Sometimes, we'll race with another worker and they'll grab @@ -102,7 +102,7 @@ final class PhabricatorWorkerLeaseQuery extends PhabricatorQuery { $task_table->getTableName(), $taskdata_table->getTableName(), $lease_ownership_name, - $this->buildOrderClause($conn_w), + $this->buildOrderClause($conn_w, $phase), $this->buildLimitClause($conn_w, $limit)); $tasks = $task_table->loadAllFromArray($data); @@ -188,8 +188,26 @@ final class PhabricatorWorkerLeaseQuery extends PhabricatorQuery { } - private function buildOrderClause(AphrontDatabaseConnection $conn_w) { - return qsprintf($conn_w, 'ORDER BY id ASC'); + private function buildOrderClause(AphrontDatabaseConnection $conn_w, $phase) { + switch ($phase) { + case self::PHASE_UNLEASED: + // When selecting new tasks, we want to consume them in roughly + // FIFO order, so we order by the task ID. + return qsprintf($conn_w, 'ORDER BY id ASC'); + case self::PHASE_EXPIRED: + // When selecting failed tasks, we want to consume them in roughly + // FIFO order of their failures, which is not necessarily their original + // queue order. + + // Particularly, this is important for tasks which use soft failures to + // indicate that they are waiting on other tasks to complete: we need to + // push them to the end of the queue after they fail, at least on + // average, so we don't deadlock retrying the same blocked task over + // and over again. + return qsprintf($conn_w, 'ORDER BY leaseExpires ASC'); + default: + throw new Exception(pht('Unknown phase "%s"!', $phase)); + } } private function buildLimitClause(AphrontDatabaseConnection $conn_w, $limit) { diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php index 3a8ad2f1ad..22550936cf 100644 --- a/src/infrastructure/diff/PhabricatorInlineCommentController.php +++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php @@ -71,7 +71,8 @@ abstract class PhabricatorInlineCommentController $dialog->setTitle('Really delete this comment?'); $dialog->addHiddenInput('id', $this->getCommentID()); $dialog->addHiddenInput('op', 'delete'); - $dialog->appendChild(hsprintf('

    Delete this inline comment?

    ')); + $dialog->appendChild( + phutil_tag('p', array(), pht('Delete this inline comment?'))); $dialog->addCancelButton('#'); $dialog->addSubmitButton('Delete'); @@ -234,14 +235,11 @@ abstract class PhabricatorInlineCommentController } private function renderTextArea($text) { - return javelin_tag( - 'textarea', - array( - 'class' => 'differential-inline-comment-edit-textarea', - 'sigil' => 'differential-inline-comment-edit-textarea', - 'name' => 'text', - ), - $text); + return id(new PhabricatorRemarkupControl()) + ->setUser($this->getRequest()->getUser()) + ->setSigil('differential-inline-comment-edit-textarea') + ->setName('text') + ->setValue($text); } } diff --git a/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php b/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php index f8686ebba0..9e05640b61 100644 --- a/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php +++ b/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php @@ -19,12 +19,7 @@ final class PhabricatorInlineSummaryView extends AphrontView { } private function renderHeader() { - return phutil_tag( - 'div', - array( - 'class' => 'phabricator-inline-summary', - ), - 'Inline Comments'); + return phutil_tag_div('phabricator-inline-summary', pht('Inline Comments')); } private function renderTable() { @@ -39,7 +34,10 @@ final class PhabricatorInlineSummaryView extends AphrontView { } } - $rows[] = hsprintf('%s', $group); + $rows[] = phutil_tag( + 'tr', + array(), + phutil_tag('th', array('colspan' => 3), $group)); foreach ($items as $item) { @@ -80,25 +78,21 @@ final class PhabricatorInlineSummaryView extends AphrontView { $where = idx($item, 'where'); $colspan = ($has_where ? null : 2); - $rows[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '', - $lines, - ($has_where - ? hsprintf('%s', $where) - : null), - phutil_tag( - 'td', - array( - 'class' => 'inline-summary-content', - 'colspan' => $colspan, - ), - hsprintf( - '
    %s
    ', - $item['content']))); + $rows[] = phutil_tag( + 'tr', + array(), + array( + phutil_tag('td', array('class' => 'inline-line-number'), $lines), + ($has_where + ? phutil_tag('td', array('class' => 'inline-which-diff'), $where) + : null), + phutil_tag( + 'td', + array( + 'class' => 'inline-summary-content', + 'colspan' => $colspan, + ), + phutil_tag_div('phabricator-remarkup', $item['content'])))); } } diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index 6929837304..8614a38372 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -60,6 +60,15 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { const TYPE_MOCK_HAS_TASK = 37; const TYPE_TASK_HAS_MOCK = 38; + const TYPE_OBJECT_USES_CREDENTIAL = 39; + const TYPE_CREDENTIAL_USED_BY_OBJECT = 40; + + const TYPE_OBJECT_HAS_PROJECT = 41; + const TYPE_PROJECT_HAS_OBJECT = 42; + + const TYPE_OBJECT_HAS_COLUMN = 43; + const TYPE_COLUMN_HAS_OBJECT = 44; + const TYPE_TEST_NO_CYCLE = 9000; const TYPE_PHOB_HAS_ASANATASK = 80001; @@ -134,7 +143,16 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { self::TYPE_REVIEWER_FOR_DREV => self::TYPE_DREV_HAS_REVIEWER, self::TYPE_PHOB_HAS_JIRAISSUE => self::TYPE_JIRAISSUE_HAS_PHOB, - self:: TYPE_JIRAISSUE_HAS_PHOB => self::TYPE_PHOB_HAS_JIRAISSUE + self::TYPE_JIRAISSUE_HAS_PHOB => self::TYPE_PHOB_HAS_JIRAISSUE, + + self::TYPE_OBJECT_USES_CREDENTIAL => self::TYPE_CREDENTIAL_USED_BY_OBJECT, + self::TYPE_CREDENTIAL_USED_BY_OBJECT => self::TYPE_OBJECT_USES_CREDENTIAL, + + self::TYPE_OBJECT_HAS_PROJECT => self::TYPE_PROJECT_HAS_OBJECT, + self::TYPE_PROJECT_HAS_OBJECT => self::TYPE_OBJECT_HAS_PROJECT, + + self::TYPE_OBJECT_HAS_COLUMN => self::TYPE_COLUMN_HAS_OBJECT, + self::TYPE_COLUMN_HAS_OBJECT => self::TYPE_OBJECT_HAS_COLUMN, ); return idx($map, $edge_type); @@ -206,6 +224,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { return '%s edited member(s), added %d: %s; removed %d: %s.'; case self::TYPE_MEMBER_OF_PROJ: case self::TYPE_COMMIT_HAS_PROJECT: + case self::TYPE_OBJECT_HAS_PROJECT: return '%s edited project(s), added %d: %s; removed %d: %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -220,6 +239,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_UNSUBSCRIBED_FROM_OBJECT: case self::TYPE_FILE_HAS_OBJECT: case self::TYPE_CONTRIBUTED_TO_OBJECT: + case self::TYPE_PROJECT_HAS_OBJECT: return '%s edited object(s), added %d: %s; removed %d: %s.'; case self::TYPE_OBJECT_HAS_UNSUBSCRIBER: return '%s edited unsubcriber(s), added %d: %s; removed %d: %s.'; @@ -280,6 +300,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { return '%s added %d member(s): %s.'; case self::TYPE_MEMBER_OF_PROJ: case self::TYPE_COMMIT_HAS_PROJECT: + case self::TYPE_OBJECT_HAS_PROJECT: return '%s added %d project(s): %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -312,6 +333,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_UNSUBSCRIBED_FROM_OBJECT: case self::TYPE_FILE_HAS_OBJECT: case self::TYPE_CONTRIBUTED_TO_OBJECT: + case self::TYPE_PROJECT_HAS_OBJECT: default: return '%s added %d object(s): %s.'; @@ -349,6 +371,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { return '%s removed %d member(s): %s.'; case self::TYPE_MEMBER_OF_PROJ: case self::TYPE_COMMIT_HAS_PROJECT: + case self::TYPE_OBJECT_HAS_PROJECT: return '%s removed %d project(s): %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -381,6 +404,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_UNSUBSCRIBED_FROM_OBJECT: case self::TYPE_FILE_HAS_OBJECT: case self::TYPE_CONTRIBUTED_TO_OBJECT: + case self::TYPE_PROJECT_HAS_OBJECT: default: return '%s removed %d object(s): %s.'; @@ -416,6 +440,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { return '%s updated members of %s.'; case self::TYPE_MEMBER_OF_PROJ: case self::TYPE_COMMIT_HAS_PROJECT: + case self::TYPE_OBJECT_HAS_PROJECT: return '%s updated projects of %s.'; case self::TYPE_QUESTION_HAS_VOTING_USER: case self::TYPE_ANSWER_HAS_VOTING_USER: @@ -448,6 +473,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { case self::TYPE_UNSUBSCRIBED_FROM_OBJECT: case self::TYPE_FILE_HAS_OBJECT: case self::TYPE_CONTRIBUTED_TO_OBJECT: + case self::TYPE_PROJECT_HAS_OBJECT: default: return '%s updated objects of %s.'; diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 44a43e62ec..f4882c0be1 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -108,6 +108,10 @@ final class PhabricatorEnv { } putenv('PATH='.$env_path); + // Write this back into $_ENV, too, so ExecFuture picks it up when creating + // subprocess environments. + $_ENV['PATH'] = $env_path; + PhabricatorEventEngine::initialize(); $translation = PhabricatorEnv::newObjectFromConfig('translation.provider'); diff --git a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php index f49c69ecaf..09a2cce336 100644 --- a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php +++ b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php @@ -810,7 +810,23 @@ abstract class PhabricatorBaseEnglishTranslation ), ), + '%d User(s) Need Approval' => array( + '%d User Needs Approval', + '%d Users Need Approval', + ), + + 'Warning: there are %d signature(s) already for this document. '. + 'Updating the title or text will invalidate these signatures and users '. + 'will need to sign again. Proceed carefully.' => array( + 'Warning: there is %d signature already for this document. '. + 'Updating the title or text will invalidate this signature and the '. + 'user will need to sign again. Proceed carefully.', + 'Warning: there are %d signatures already for this document. '. + 'Updating the title or text will invalidate these signatures and '. + 'users will need to sign again. Proceed carefully.', + ), + ); } -} + } diff --git a/src/infrastructure/javelin/markup.php b/src/infrastructure/javelin/markup.php index 5477f5687b..b845b33ae8 100644 --- a/src/infrastructure/javelin/markup.php +++ b/src/infrastructure/javelin/markup.php @@ -38,23 +38,48 @@ function javelin_tag( function phabricator_form(PhabricatorUser $user, $attributes, $content) { $body = array(); - if (strcasecmp(idx($attributes, 'method'), 'POST') == 0 && - !preg_match('#^(https?:|//)#', idx($attributes, 'action'))) { - $body[] = phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => AphrontRequest::getCSRFTokenName(), - 'value' => $user->getCSRFToken(), - )); + $http_method = idx($attributes, 'method'); + $is_post = (strcasecmp($http_method, 'POST') === 0); - $body[] = phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => '__form__', - 'value' => true, - )); + $http_action = idx($attributes, 'action'); + $is_absolute_uri = preg_match('#^(https?:|//)#', $http_action); + + if ($is_post) { + if ($is_absolute_uri) { + $is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); + if ($is_dev) { + $form_domain = id(new PhutilURI($http_action)) + ->getDomain(); + $host_domain = id(new PhutilURI(PhabricatorEnv::getURI('/'))) + ->getDomain(); + + if (strtolower($form_domain) == strtolower($host_domain)) { + throw new Exception( + pht( + "You are building a
    that submits to Phabricator, but ". + "has an absolute URI in its 'action' attribute ('%s'). To avoid ". + "leaking CSRF tokens, Phabricator does not add CSRF information ". + "to forms with absolute URIs. Instead, use a relative URI.", + $http_action)); + } + } + } else { + $body[] = phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => AphrontRequest::getCSRFTokenName(), + 'value' => $user->getCSRFToken(), + )); + + $body[] = phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => '__form__', + 'value' => true, + )); + } } if (is_array($content)) { diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php index a1f9cd9974..4c73e10d8e 100644 --- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php +++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php @@ -139,7 +139,7 @@ final class PhabricatorJavelinLinter extends ArcanistLinter { } } - $celerity = CelerityResourceMap::getInstance(); + $celerity = CelerityResourceMap::getNamedInstance('phabricator'); $path = preg_replace( '@^externals/javelinjs/src/@', @@ -147,31 +147,30 @@ final class PhabricatorJavelinLinter extends ArcanistLinter { $path); $need = $external_classes; - $info = $celerity->lookupFileInformation(substr($path, strlen('webroot'))); - if (!$info) { - $info = array(); + $resource_name = substr($path, strlen('webroot/')); + $requires = $celerity->getRequiredSymbolsForName($resource_name); + if (!$requires) { + $requires = array(); } - $requires = idx($info, 'requires', array()); - - foreach ($requires as $key => $name) { - $symbol_info = $celerity->lookupSymbolInformation($name); - if (!$symbol_info) { + foreach ($requires as $key => $requires_symbol) { + $requires_name = $celerity->getResourceNameForSymbol($requires_symbol); + if ($requires_name === null) { $this->raiseLintAtLine( 0, 0, self::LINT_UNKNOWN_DEPENDENCY, - "This file @requires component '{$name}', but it does not ". - "exist. You may need to rebuild the Celerity map."); + "This file @requires component '{$requires_symbol}', but it does ". + "not exist. You may need to rebuild the Celerity map."); unset($requires[$key]); continue; } - if (preg_match('/\\.css$/', $symbol_info['disk'])) { + if (preg_match('/\\.css$/', $requires_name)) { // If JS requires CSS, just assume everything is fine. unset($requires[$key]); } else { - $symbol_path = 'webroot'.$symbol_info['disk']; + $symbol_path = 'webroot/'.$requires_name; list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath( $symbol_path); if (array_intersect_key($req_install, $external_classes)) { diff --git a/src/infrastructure/PhabricatorAccessLog.php b/src/infrastructure/log/PhabricatorAccessLog.php similarity index 100% rename from src/infrastructure/PhabricatorAccessLog.php rename to src/infrastructure/log/PhabricatorAccessLog.php diff --git a/src/infrastructure/log/PhabricatorSSHLog.php b/src/infrastructure/log/PhabricatorSSHLog.php new file mode 100644 index 0000000000..12b8001b5a --- /dev/null +++ b/src/infrastructure/log/PhabricatorSSHLog.php @@ -0,0 +1,52 @@ + date('r'), + 'h' => php_uname('n'), + 'p' => getmypid(), + 'e' => time(), + ); + + $sudo_user = PhabricatorEnv::getEnvConfig('phd.user'); + if (strlen($sudo_user)) { + $data['S'] = $sudo_user; + } + + if (function_exists('posix_geteuid')) { + $system_uid = posix_geteuid(); + $system_info = posix_getpwuid($system_uid); + $data['s'] = idx($system_info, 'name'); + } + + $client = getenv('SSH_CLIENT'); + if (strlen($client)) { + $remote_address = head(explode(' ', $client)); + $data['r'] = $remote_address; + } + + $log = id(new PhutilDeferredLog($path, $format)) + ->setFailQuietly(true) + ->setData($data); + + self::$log = $log; + } + + return self::$log; + } + +} diff --git a/src/infrastructure/management/PhabricatorManagementWorkflow.php b/src/infrastructure/management/PhabricatorManagementWorkflow.php new file mode 100644 index 0000000000..18deb5916e --- /dev/null +++ b/src/infrastructure/management/PhabricatorManagementWorkflow.php @@ -0,0 +1,16 @@ +getMarkupFieldKey($field).'@'.$this->version.'@'.$custom; } @@ -239,7 +242,9 @@ final class PhabricatorMarkupEngine { } foreach ($objects as $key => $info) { - if (isset($blocks[$key])) { + // False check in case MySQL doesn't support unicode characters + // in the string (T1191), resulting in unserialize returning false. + if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) { // If we already have a preprocessing cache, we don't need to rebuild // it. continue; @@ -364,6 +369,10 @@ final class PhabricatorMarkupEngine { case 'default': $engine = self::newMarkupEngine(array()); break; + case 'nolinebreaks': + $engine = self::newMarkupEngine(array()); + $engine->setConfig('preserve-linebreaks', false); + break; case 'diviner': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); @@ -446,6 +455,7 @@ final class PhabricatorMarkupEngine { $rules[] = new PhutilRemarkupRuleBold(); $rules[] = new PhutilRemarkupRuleItalic(); $rules[] = new PhutilRemarkupRuleDel(); + $rules[] = new PhutilRemarkupRuleUnderline(); foreach (self::loadCustomInlineRules() as $rule) { $rules[] = $rule; diff --git a/src/infrastructure/markup/PhabricatorMarkupOneOff.php b/src/infrastructure/markup/PhabricatorMarkupOneOff.php index f0f213fc12..ec112e5c00 100644 --- a/src/infrastructure/markup/PhabricatorMarkupOneOff.php +++ b/src/infrastructure/markup/PhabricatorMarkupOneOff.php @@ -36,10 +36,11 @@ final class PhabricatorMarkupOneOff implements PhabricatorMarkupInterface { } public function newMarkupEngine($field) { - return PhabricatorMarkupEngine::newMarkupEngine( - array( - 'preserve-linebreaks' => $this->preserveLinebreaks, - )); + if ($this->preserveLinebreaks) { + return PhabricatorMarkupEngine::getEngine(); + } else { + return PhabricatorMarkupEngine::getEngine('nolinebreaks'); + } } public function getMarkupText($field) { diff --git a/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterCowsay.php b/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterCowsay.php index 07e6ca611b..e4c5be4c30 100644 --- a/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterCowsay.php +++ b/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterCowsay.php @@ -29,6 +29,7 @@ final class PhabricatorRemarkupBlockInterpreterCowsay $tongue, $cow); + $future->setTimeout(15); $future->write($content); list($err, $stdout, $stderr) = $future->resolve(); diff --git a/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterFiglet.php b/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterFiglet.php index 35370e2aa2..9bf5a132ee 100644 --- a/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterFiglet.php +++ b/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterFiglet.php @@ -13,7 +13,10 @@ final class PhabricatorRemarkupBlockInterpreterFiglet pht('Unable to locate the `figlet` binary. Install figlet.')); } - $future = id(new ExecFuture('figlet')) + $font = idx($argv, 'font', 'standard'); + $safe_font = preg_replace('/[^0-9a-zA-Z-_.]/', '', $font); + $future = id(new ExecFuture('figlet -f %s', $safe_font)) + ->setTimeout(15) ->write(trim($content, "\n")); list($err, $stdout, $stderr) = $future->resolve(); diff --git a/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterGraphviz.php b/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterGraphviz.php index 79ec668f8c..e165f17532 100644 --- a/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterGraphviz.php +++ b/src/infrastructure/markup/interpreter/PhabricatorRemarkupBlockInterpreterGraphviz.php @@ -14,6 +14,7 @@ final class PhabricatorRemarkupBlockInterpreterGraphviz } $future = id(new ExecFuture('dot -T%s', 'png')) + ->setTimeout(15) ->write(trim($content)); list($err, $stdout, $stderr) = $future->resolve(); @@ -21,7 +22,9 @@ final class PhabricatorRemarkupBlockInterpreterGraphviz if ($err) { return $this->markupError( pht( - 'Execution of `dot` failed, check your syntax: %s', $stderr)); + 'Execution of `dot` failed (#%d), check your syntax: %s', + $err, + $stderr)); } $file = PhabricatorFile::buildFromFileDataOrHash( diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php index 35e95ee4ba..252195c7b4 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php @@ -85,10 +85,10 @@ abstract class PhabricatorRemarkupRuleObject } protected function renderHovertag($name, $href, array $attr = array()) { - return id(new PhabricatorTagView()) + return id(new PHUITagView()) ->setName($name) ->setHref($href) - ->setType(PhabricatorTagView::TYPE_OBJECT) + ->setType(PHUITagView::TYPE_OBJECT) ->setPHID(idx($attr, 'phid')) ->setClosed(idx($attr, 'closed')) ->render(); diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php index 6ea88db6ca..ba3b0c92dc 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php @@ -30,8 +30,8 @@ final class PhabricatorRemarkupRuleYoutube } $youtube_src = 'https://www.youtube.com/embed/'.$v; - $iframe = hsprintf( - '
    %s
    ', + $iframe = phutil_tag_div( + 'embedded-youtube-video', phutil_tag( 'iframe', array( diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 75d325d5fe..3e13fb3d33 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -19,6 +19,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } protected function getPagingValue($result) { + if (!is_object($result)) { + // This interface can't be typehinted and PHP gets really angry if we + // call a method on a non-object, so add an explicit check here. + throw new Exception(pht('Expected object, got "%s"!', gettype($result))); + } return $result->getID(); } diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 55c6edc277..d2fd056e7f 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -109,7 +109,7 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { * @task config */ final public function shouldRaisePolicyExceptions() { - return (bool) $this->raisePolicyExceptions; + return (bool)$this->raisePolicyExceptions; } diff --git a/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php b/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php new file mode 100644 index 0000000000..e05e0c7fae --- /dev/null +++ b/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php @@ -0,0 +1,212 @@ + stdin + * stdout <- stdout + * stderr <- stderr + * + * You can provide **read and write callbacks** which are invoked as data + * is passed through this class. They allow you to inspect and modify traffic. + * + * IO Channel Passthru Command Channel + * stdout -> willWrite -> stdin + * stdin <- willRead <- stdout + * stderr <- (identity) <- stderr + * + * Primarily, this means: + * + * - the **IO Channel** can be a @{class:PhutilProtocolChannel} if the + * **write callback** can convert protocol messages into strings; and + * - the **write callback** can inspect and reject requests over the channel, + * e.g. to enforce policies. + * + * In practice, this is used when serving repositories to check each command + * issued over SSH and determine if it is a read command or a write command. + * Writes can then be checked for appropriate permissions. + */ +final class PhabricatorSSHPassthruCommand extends Phobject { + + private $commandChannel; + private $ioChannel; + private $errorChannel; + private $execFuture; + private $willWriteCallback; + private $willReadCallback; + + public function setCommandChannelFromExecFuture(ExecFuture $exec_future) { + $exec_channel = new PhutilExecChannel($exec_future); + $exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback')); + + $this->execFuture = $exec_future; + $this->commandChannel = $exec_channel; + + return $this; + } + + public function setIOChannel(PhutilChannel $io_channel) { + $this->ioChannel = $io_channel; + return $this; + } + + public function setErrorChannel(PhutilChannel $error_channel) { + $this->errorChannel = $error_channel; + return $this; + } + + public function setWillReadCallback($will_read_callback) { + $this->willReadCallback = $will_read_callback; + return $this; + } + + public function setWillWriteCallback($will_write_callback) { + $this->willWriteCallback = $will_write_callback; + return $this; + } + + public function writeErrorIOCallback(PhutilChannel $channel, $data) { + $this->errorChannel->write($data); + } + + public function execute() { + $command_channel = $this->commandChannel; + $io_channel = $this->ioChannel; + $error_channel = $this->errorChannel; + + if (!$command_channel) { + throw new Exception("Set a command channel before calling execute()!"); + } + + if (!$io_channel) { + throw new Exception("Set an IO channel before calling execute()!"); + } + + if (!$error_channel) { + throw new Exception("Set an error channel before calling execute()!"); + } + + $channels = array($command_channel, $io_channel, $error_channel); + + // We want to limit the amount of data we'll hold in memory for this + // process. See T4241 for a discussion of this issue in general. + + $buffer_size = (1024 * 1024); // 1MB + $io_channel->setReadBufferSize($buffer_size); + $command_channel->setReadBufferSize($buffer_size); + + // TODO: This just makes us throw away stderr after the first 1MB, but we + // don't currently have the support infrastructure to buffer it correctly. + // It's difficult to imagine this causing problems in practice, though. + $this->execFuture->getStderrSizeLimit($buffer_size); + + while (true) { + PhutilChannel::waitForAny($channels); + + $io_channel->update(); + $command_channel->update(); + $error_channel->update(); + + // If any channel is blocked on the other end, wait for it to flush before + // we continue reading. For example, if a user is running `git clone` on + // a 1GB repository, the underlying `git-upload-pack` may + // be able to produce data much more quickly than we can send it over + // the network. If we don't throttle the reads, we may only send a few + // MB over the I/O channel in the time it takes to read the entire 1GB off + // the command channel. That leaves us with 1GB of data in memory. + + while ($command_channel->isOpen() && + $io_channel->isOpenForWriting() && + ($command_channel->getWriteBufferSize() >= $buffer_size || + $io_channel->getWriteBufferSize() >= $buffer_size || + $error_channel->getWriteBufferSize() >= $buffer_size)) { + PhutilChannel::waitForActivity(array(), $channels); + $io_channel->update(); + $command_channel->update(); + $error_channel->update(); + } + + // If the subprocess has exited and we've read everything from it, + // we're all done. + $done = !$command_channel->isOpenForReading() && + $command_channel->isReadBufferEmpty(); + + $in_message = $io_channel->read(); + if ($in_message !== null) { + $in_message = $this->willWriteData($in_message); + if ($in_message !== null) { + $command_channel->write($in_message); + } + } + + $out_message = $command_channel->read(); + if ($out_message !== null) { + $out_message = $this->willReadData($out_message); + if ($out_message !== null) { + $io_channel->write($out_message); + } + } + + // If we have nothing left on stdin, close stdin on the subprocess. + if (!$io_channel->isOpenForReading()) { + $command_channel->closeWriteChannel(); + } + + if ($done) { + break; + } + + // If the client has disconnected, kill the subprocess and bail. + if (!$io_channel->isOpenForWriting()) { + $this->execFuture + ->setStdoutSizeLimit(0) + ->setStderrSizeLimit(0) + ->setReadBufferSize(null) + ->resolveKill(); + break; + } + } + + list($err) = $this->execFuture + ->setStdoutSizeLimit(0) + ->setStderrSizeLimit(0) + ->setReadBufferSize(null) + ->resolve(); + + return $err; + } + + public function willWriteData($message) { + if ($this->willWriteCallback) { + return call_user_func($this->willWriteCallback, $this, $message); + } else { + if (strlen($message)) { + return $message; + } else { + return null; + } + } + } + + public function willReadData($message) { + if ($this->willReadCallback) { + return call_user_func($this->willReadCallback, $this, $message); + } else { + if (strlen($message)) { + return $message; + } else { + return null; + } + } + } + +} diff --git a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php index 60a9f03fcd..8bcd289054 100644 --- a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php +++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php @@ -1,11 +1,15 @@ errorChannel = $error_channel; return $this; @@ -24,10 +28,6 @@ abstract class PhabricatorSSHWorkflow extends PhutilArgumentWorkflow { return $this->user; } - final public function isExecutable() { - return false; - } - public function setIOChannel(PhutilChannel $channel) { $this->iochannel = $channel; return $this; @@ -37,50 +37,6 @@ abstract class PhabricatorSSHWorkflow extends PhutilArgumentWorkflow { return $this->iochannel; } - public function passthruIO(ExecFuture $future) { - $exec_channel = new PhutilExecChannel($future); - $exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback')); - - $io_channel = $this->getIOChannel(); - $error_channel = $this->getErrorChannel(); - - $channels = array($exec_channel, $io_channel, $error_channel); - - while (true) { - PhutilChannel::waitForAny($channels); - - $io_channel->update(); - $exec_channel->update(); - $error_channel->update(); - - $done = !$exec_channel->isOpen(); - - $data = $io_channel->read(); - if (strlen($data)) { - $exec_channel->write($data); - } - - $data = $exec_channel->read(); - if (strlen($data)) { - $io_channel->write($data); - } - - // If we have nothing left on stdin, close stdin on the subprocess. - if (!$io_channel->isOpenForReading()) { - // TODO: This should probably be part of PhutilExecChannel? - $future->write(''); - } - - if ($done) { - break; - } - } - - list($err) = $future->resolve(); - - return $err; - } - public function readAllInput() { $channel = $this->getIOChannel(); while ($channel->update()) { @@ -102,8 +58,9 @@ abstract class PhabricatorSSHWorkflow extends PhutilArgumentWorkflow { return $this; } - public function writeErrorIOCallback(PhutilChannel $channel, $data) { - $this->writeErrorIO($data); + protected function newPassthruCommand() { + return id(new PhabricatorSSHPassthruCommand()) + ->setErrorChannel($this->getErrorChannel()); } } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index a8667a1266..531de3aa6e 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1257,7 +1257,17 @@ abstract class LiskDAO { $columns = array_keys($data); foreach ($data as $key => $value) { - $data[$key] = qsprintf($conn, '%ns', $value); + try { + $data[$key] = qsprintf($conn, '%ns', $value); + } catch (AphrontQueryParameterException $parameter_exception) { + throw new PhutilProxyException( + pht( + "Unable to insert or update object of class %s, field '%s' ". + "has a nonscalar value.", + get_class($this), + $key), + $parameter_exception); + } } $data = implode(', ', $data); diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index cca5c3accf..2fb458bbb4 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -175,4 +175,15 @@ abstract class PhabricatorLiskDAO extends LiskDAO { return $value[$key]; } + protected function detectEncodingForStorage($string) { + return phutil_is_utf8($string) ? 'utf8' : null; + } + + protected function getUTF8StringFromStorage($string, $encoding) { + if ($encoding == 'utf8') { + return $string; + } + return phutil_utf8ize($string); + } + } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index 39767de50c..ccb96b0c54 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -1,7 +1,7 @@ api; } - public function isExecutable() { - return true; - } - } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 6f5e699c98..70ee75e1d3 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -17,1733 +17,769 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { } public function getPatches() { + $patches = array(); + + foreach ($this->getOldPatches() as $old_name => $old_patch) { + if (preg_match('/^db\./', $old_name)) { + $old_patch['name'] = substr($old_name, 3); + $old_patch['type'] = 'db'; + } else { + if (empty($old_patch['name'])) { + $old_patch['name'] = $this->getPatchPath($old_name); + } + if (empty($old_patch['type'])) { + $matches = null; + preg_match('/\.(sql|php)$/', $old_name, $matches); + $old_patch['type'] = $matches[1]; + } + } + + $patches[$old_name] = $old_patch; + } + + $root = dirname(phutil_get_library_root('phabricator')); + $auto_root = $root.'/resources/sql/autopatches/'; + $auto_list = Filesystem::listDirectory($auto_root, $include_hidden = false); + sort($auto_list); + + foreach ($auto_list as $auto_patch) { + $matches = null; + if (!preg_match('/\.(sql|php)$/', $auto_patch, $matches)) { + throw new Exception( + pht( + 'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.', + $auto_patch, + $auto_root)); + } + + $patches[$auto_patch] = array( + 'type' => $matches[1], + 'name' => $auto_root.$auto_patch, + ); + } + + return $patches; + } + + public function getOldPatches() { return array( 'db.audit' => array( - 'type' => 'db', - 'name' => 'audit', 'after' => array( /* First Patch */ ), ), - 'db.calendar' => array( - 'type' => 'db', - 'name' => 'calendar', - ), - 'db.chatlog' => array( - 'type' => 'db', - 'name' => 'chatlog', - ), - 'db.conduit' => array( - 'type' => 'db', - 'name' => 'conduit', - ), - 'db.countdown' => array( - 'type' => 'db', - 'name' => 'countdown', - ), - 'db.daemon' => array( - 'type' => 'db', - 'name' => 'daemon', - ), - 'db.differential' => array( - 'type' => 'db', - 'name' => 'differential', - ), - 'db.draft' => array( - 'type' => 'db', - 'name' => 'draft', - ), - 'db.drydock' => array( - 'type' => 'db', - 'name' => 'drydock', - ), - 'db.feed' => array( - 'type' => 'db', - 'name' => 'feed', - ), - 'db.file' => array( - 'type' => 'db', - 'name' => 'file', - ), - 'db.flag' => array( - 'type' => 'db', - 'name' => 'flag', - ), - 'db.harbormaster' => array( - 'type' => 'db', - 'name' => 'harbormaster', - ), - 'db.herald' => array( - 'type' => 'db', - 'name' => 'herald', - ), - 'db.maniphest' => array( - 'type' => 'db', - 'name' => 'maniphest', - ), - 'db.meta_data' => array( - 'type' => 'db', - 'name' => 'meta_data', - ), - 'db.metamta' => array( - 'type' => 'db', - 'name' => 'metamta', - ), - 'db.oauth_server' => array( - 'type' => 'db', - 'name' => 'oauth_server', - ), - 'db.owners' => array( - 'type' => 'db', - 'name' => 'owners', - ), - 'db.pastebin' => array( - 'type' => 'db', - 'name' => 'pastebin', - ), - 'db.phame' => array( - 'type' => 'db', - 'name' => 'phame', - ), - 'db.phriction' => array( - 'type' => 'db', - 'name' => 'phriction', - ), - 'db.project' => array( - 'type' => 'db', - 'name' => 'project', - ), - 'db.repository' => array( - 'type' => 'db', - 'name' => 'repository', - ), - 'db.search' => array( - 'type' => 'db', - 'name' => 'search', - ), - 'db.slowvote' => array( - 'type' => 'db', - 'name' => 'slowvote', - ), + 'db.calendar' => array(), + 'db.chatlog' => array(), + 'db.conduit' => array(), + 'db.countdown' => array(), + 'db.daemon' => array(), + 'db.differential' => array(), + 'db.draft' => array(), + 'db.drydock' => array(), + 'db.feed' => array(), + 'db.file' => array(), + 'db.flag' => array(), + 'db.harbormaster' => array(), + 'db.herald' => array(), + 'db.maniphest' => array(), + 'db.meta_data' => array(), + 'db.metamta' => array(), + 'db.oauth_server' => array(), + 'db.owners' => array(), + 'db.pastebin' => array(), + 'db.phame' => array(), + 'db.phriction' => array(), + 'db.project' => array(), + 'db.repository' => array(), + 'db.search' => array(), + 'db.slowvote' => array(), 'db.timeline' => array( - 'type' => 'db', - 'name' => 'timeline', - 'dead' => true, - ), - 'db.user' => array( - 'type' => 'db', - 'name' => 'user', - ), - 'db.worker' => array( - 'type' => 'db', - 'name' => 'worker', - ), - 'db.xhpastview' => array( - 'type' => 'db', - 'name' => 'xhpastview', - ), - 'db.cache' => array( - 'type' => 'db', - 'name' => 'cache', - ), - 'db.fact' => array( - 'type' => 'db', - 'name' => 'fact', - ), - 'db.ponder' => array( - 'type' => 'db', - 'name' => 'ponder', - ), - 'db.xhprof' => array( - 'type' => 'db', - 'name' => 'xhprof', - ), - 'db.pholio' => array( - 'type' => 'db', - 'name' => 'pholio', - ), - 'db.conpherence' => array( - 'type' => 'db', - 'name' => 'conpherence', - ), - 'db.config' => array( - 'type' => 'db', - 'name' => 'config', - ), - 'db.token' => array( - 'type' => 'db', - 'name' => 'token', - ), - 'db.releeph' => array( - 'type' => 'db', - 'name' => 'releeph', - ), - 'db.phlux' => array( - 'type' => 'db', - 'name' => 'phlux', - ), - 'db.phortune' => array( - 'type' => 'db', - 'name' => 'phortune', - ), - 'db.phrequent' => array( - 'type' => 'db', - 'name' => 'phrequent', - ), - 'db.diviner' => array( - 'type' => 'db', - 'name' => 'diviner', - ), - 'db.auth' => array( - 'type' => 'db', - 'name' => 'auth', - ), - 'db.doorkeeper' => array( - 'type' => 'db', - 'name' => 'doorkeeper', - ), - 'db.legalpad' => array( - 'type' => 'db', - 'name' => 'legalpad', - ), - 'db.policy' => array( - 'type' => 'db', - 'name' => 'policy', - ), - 'db.nuance' => array( - 'type' => 'db', - 'name' => 'nuance', + 'dead' => true, ), + 'db.user' => array(), + 'db.worker' => array(), + 'db.xhpastview' => array(), + 'db.cache' => array(), + 'db.fact' => array(), + 'db.ponder' => array(), + 'db.xhprof' => array(), + 'db.pholio' => array(), + 'db.conpherence' => array(), + 'db.config' => array(), + 'db.token' => array(), + 'db.releeph' => array(), + 'db.phlux' => array(), + 'db.phortune' => array(), + 'db.phrequent' => array(), + 'db.diviner' => array(), + 'db.auth' => array(), + 'db.doorkeeper' => array(), + 'db.legalpad' => array(), + 'db.policy' => array(), + 'db.nuance' => array(), + 'db.passphrase' => array(), + 'db.phragment' => array(), + 'db.dashboard' => array(), '0000.legacy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('0000.legacy.sql'), - 'legacy' => 0, + 'legacy' => 0, ), '000.project.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('000.project.sql'), - 'legacy' => 0, + 'legacy' => 0, ), '001.maniphest_projects.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('001.maniphest_projects.sql'), - 'legacy' => 1, + 'legacy' => 1, ), '002.oauth.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('002.oauth.sql'), - 'legacy' => 2, + 'legacy' => 2, ), '003.more_oauth.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('003.more_oauth.sql'), - 'legacy' => 3, + 'legacy' => 3, ), '004.daemonrepos.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('004.daemonrepos.sql'), - 'legacy' => 4, + 'legacy' => 4, ), '005.workers.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('005.workers.sql'), - 'legacy' => 5, + 'legacy' => 5, ), '006.repository.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('006.repository.sql'), - 'legacy' => 6, + 'legacy' => 6, ), '007.daemonlog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('007.daemonlog.sql'), - 'legacy' => 7, + 'legacy' => 7, ), '008.repoopt.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('008.repoopt.sql'), - 'legacy' => 8, + 'legacy' => 8, ), '009.repo_summary.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('009.repo_summary.sql'), - 'legacy' => 9, + 'legacy' => 9, ), '010.herald.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('010.herald.sql'), - 'legacy' => 10, + 'legacy' => 10, ), '011.badcommit.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('011.badcommit.sql'), - 'legacy' => 11, + 'legacy' => 11, ), '012.dropphidtype.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('012.dropphidtype.sql'), - 'legacy' => 12, + 'legacy' => 12, ), '013.commitdetail.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('013.commitdetail.sql'), - 'legacy' => 13, + 'legacy' => 13, ), '014.shortcuts.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('014.shortcuts.sql'), - 'legacy' => 14, + 'legacy' => 14, ), '015.preferences.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('015.preferences.sql'), - 'legacy' => 15, + 'legacy' => 15, ), '016.userrealnameindex.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('016.userrealnameindex.sql'), - 'legacy' => 16, + 'legacy' => 16, ), '017.sessionkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('017.sessionkeys.sql'), - 'legacy' => 17, + 'legacy' => 17, ), '018.owners.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('018.owners.sql'), - 'legacy' => 18, + 'legacy' => 18, ), '019.arcprojects.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('019.arcprojects.sql'), - 'legacy' => 19, + 'legacy' => 19, ), '020.pathcapital.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('020.pathcapital.sql'), - 'legacy' => 20, + 'legacy' => 20, ), '021.xhpastview.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('021.xhpastview.sql'), - 'legacy' => 21, + 'legacy' => 21, ), '022.differentialcommit.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('022.differentialcommit.sql'), - 'legacy' => 22, + 'legacy' => 22, ), '023.dxkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('023.dxkeys.sql'), - 'legacy' => 23, + 'legacy' => 23, ), '024.mlistkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('024.mlistkeys.sql'), - 'legacy' => 24, + 'legacy' => 24, ), '025.commentopt.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('025.commentopt.sql'), - 'legacy' => 25, + 'legacy' => 25, ), '026.diffpropkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('026.diffpropkey.sql'), - 'legacy' => 26, + 'legacy' => 26, ), '027.metamtakeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('027.metamtakeys.sql'), - 'legacy' => 27, + 'legacy' => 27, ), '028.systemagent.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('028.systemagent.sql'), - 'legacy' => 28, + 'legacy' => 28, ), '029.cursors.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('029.cursors.sql'), - 'legacy' => 29, + 'legacy' => 29, ), '030.imagemacro.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('030.imagemacro.sql'), - 'legacy' => 30, + 'legacy' => 30, ), '031.workerrace.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('031.workerrace.sql'), - 'legacy' => 31, + 'legacy' => 31, ), '032.viewtime.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('032.viewtime.sql'), - 'legacy' => 32, + 'legacy' => 32, ), '033.privtest.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('033.privtest.sql'), - 'legacy' => 33, + 'legacy' => 33, ), '034.savedheader.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('034.savedheader.sql'), - 'legacy' => 34, + 'legacy' => 34, ), '035.proxyimage.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('035.proxyimage.sql'), - 'legacy' => 35, + 'legacy' => 35, ), '036.mailkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('036.mailkey.sql'), - 'legacy' => 36, + 'legacy' => 36, ), '037.setuptest.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('037.setuptest.sql'), - 'legacy' => 37, + 'legacy' => 37, ), '038.admin.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('038.admin.sql'), - 'legacy' => 38, + 'legacy' => 38, ), '039.userlog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('039.userlog.sql'), - 'legacy' => 39, + 'legacy' => 39, ), '040.transform.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('040.transform.sql'), - 'legacy' => 40, + 'legacy' => 40, ), '041.heraldrepetition.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('041.heraldrepetition.sql'), - 'legacy' => 41, + 'legacy' => 41, ), '042.commentmetadata.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('042.commentmetadata.sql'), - 'legacy' => 42, + 'legacy' => 42, ), '043.pastebin.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('043.pastebin.sql'), - 'legacy' => 43, + 'legacy' => 43, ), '044.countdown.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('044.countdown.sql'), - 'legacy' => 44, + 'legacy' => 44, ), '045.timezone.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('045.timezone.sql'), - 'legacy' => 45, + 'legacy' => 45, ), '046.conduittoken.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('046.conduittoken.sql'), - 'legacy' => 46, + 'legacy' => 46, ), '047.projectstatus.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('047.projectstatus.sql'), - 'legacy' => 47, + 'legacy' => 47, ), '048.relationshipkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('048.relationshipkeys.sql'), - 'legacy' => 48, + 'legacy' => 48, ), '049.projectowner.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('049.projectowner.sql'), - 'legacy' => 49, + 'legacy' => 49, ), '050.taskdenormal.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('050.taskdenormal.sql'), - 'legacy' => 50, + 'legacy' => 50, ), '051.projectfilter.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('051.projectfilter.sql'), - 'legacy' => 51, + 'legacy' => 51, ), '052.pastelanguage.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('052.pastelanguage.sql'), - 'legacy' => 52, + 'legacy' => 52, ), '053.feed.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('053.feed.sql'), - 'legacy' => 53, + 'legacy' => 53, ), '054.subscribers.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('054.subscribers.sql'), - 'legacy' => 54, + 'legacy' => 54, ), '055.add_author_to_files.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('055.add_author_to_files.sql'), - 'legacy' => 55, + 'legacy' => 55, ), '056.slowvote.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('056.slowvote.sql'), - 'legacy' => 56, + 'legacy' => 56, ), '057.parsecache.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('057.parsecache.sql'), - 'legacy' => 57, + 'legacy' => 57, ), '058.missingkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('058.missingkeys.sql'), - 'legacy' => 58, + 'legacy' => 58, ), '059.engines.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('059.engines.php'), - 'legacy' => 59, + 'legacy' => 59, ), '060.phriction.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('060.phriction.sql'), - 'legacy' => 60, + 'legacy' => 60, ), '061.phrictioncontent.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('061.phrictioncontent.sql'), - 'legacy' => 61, + 'legacy' => 61, ), '062.phrictionmenu.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('062.phrictionmenu.sql'), - 'legacy' => 62, + 'legacy' => 62, ), '063.pasteforks.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('063.pasteforks.sql'), - 'legacy' => 63, + 'legacy' => 63, ), '064.subprojects.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('064.subprojects.sql'), - 'legacy' => 64, + 'legacy' => 64, ), '065.sshkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('065.sshkeys.sql'), - 'legacy' => 65, + 'legacy' => 65, ), '066.phrictioncontent.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('066.phrictioncontent.sql'), - 'legacy' => 66, + 'legacy' => 66, ), '067.preferences.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('067.preferences.sql'), - 'legacy' => 67, + 'legacy' => 67, ), '068.maniphestauxiliarystorage.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('068.maniphestauxiliarystorage.sql'), - 'legacy' => 68, + 'legacy' => 68, ), '069.heraldxscript.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('069.heraldxscript.sql'), - 'legacy' => 69, + 'legacy' => 69, ), '070.differentialaux.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('070.differentialaux.sql'), - 'legacy' => 70, + 'legacy' => 70, ), '071.contentsource.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('071.contentsource.sql'), - 'legacy' => 71, + 'legacy' => 71, ), '072.blamerevert.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('072.blamerevert.sql'), - 'legacy' => 72, + 'legacy' => 72, ), '073.reposymbols.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('073.reposymbols.sql'), - 'legacy' => 73, + 'legacy' => 73, ), '074.affectedpath.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('074.affectedpath.sql'), - 'legacy' => 74, + 'legacy' => 74, ), '075.revisionhash.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('075.revisionhash.sql'), - 'legacy' => 75, + 'legacy' => 75, ), '076.indexedlanguages.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('076.indexedlanguages.sql'), - 'legacy' => 76, + 'legacy' => 76, ), '077.originalemail.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('077.originalemail.sql'), - 'legacy' => 77, + 'legacy' => 77, ), '078.nametoken.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('078.nametoken.sql'), - 'legacy' => 78, + 'legacy' => 78, ), '079.nametokenindex.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('079.nametokenindex.php'), - 'legacy' => 79, + 'legacy' => 79, ), '080.filekeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('080.filekeys.sql'), - 'legacy' => 80, + 'legacy' => 80, ), '081.filekeys.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('081.filekeys.php'), - 'legacy' => 81, + 'legacy' => 81, ), '082.xactionkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('082.xactionkey.sql'), - 'legacy' => 82, + 'legacy' => 82, ), '083.dxviewtime.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('083.dxviewtime.sql'), - 'legacy' => 83, + 'legacy' => 83, ), '084.pasteauthorkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('084.pasteauthorkey.sql'), - 'legacy' => 84, + 'legacy' => 84, ), '085.packagecommitrelationship.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('085.packagecommitrelationship.sql'), - 'legacy' => 85, + 'legacy' => 85, ), '086.formeraffil.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('086.formeraffil.sql'), - 'legacy' => 86, + 'legacy' => 86, ), '087.phrictiondelete.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('087.phrictiondelete.sql'), - 'legacy' => 87, + 'legacy' => 87, ), '088.audit.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('088.audit.sql'), - 'legacy' => 88, + 'legacy' => 88, ), '089.projectwiki.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('089.projectwiki.sql'), - 'legacy' => 89, + 'legacy' => 89, ), '090.forceuniqueprojectnames.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('090.forceuniqueprojectnames.php'), - 'legacy' => 90, + 'legacy' => 90, ), '091.uniqueslugkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('091.uniqueslugkey.sql'), - 'legacy' => 91, + 'legacy' => 91, ), '092.dropgithubnotification.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('092.dropgithubnotification.sql'), - 'legacy' => 92, + 'legacy' => 92, ), '093.gitremotes.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('093.gitremotes.php'), - 'legacy' => 93, + 'legacy' => 93, ), '094.phrictioncolumn.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('094.phrictioncolumn.sql'), - 'legacy' => 94, + 'legacy' => 94, ), '095.directory.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('095.directory.sql'), - 'legacy' => 95, + 'legacy' => 95, ), '096.filename.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('096.filename.sql'), - 'legacy' => 96, + 'legacy' => 96, ), '097.heraldruletypes.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('097.heraldruletypes.sql'), - 'legacy' => 97, + 'legacy' => 97, ), '098.heraldruletypemigration.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('098.heraldruletypemigration.php'), - 'legacy' => 98, + 'legacy' => 98, ), '099.drydock.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('099.drydock.sql'), - 'legacy' => 99, + 'legacy' => 99, ), '100.projectxaction.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('100.projectxaction.sql'), - 'legacy' => 100, + 'legacy' => 100, ), '101.heraldruleapplied.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('101.heraldruleapplied.sql'), - 'legacy' => 101, + 'legacy' => 101, ), '102.heraldcleanup.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('102.heraldcleanup.php'), - 'legacy' => 102, + 'legacy' => 102, ), '103.heraldedithistory.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('103.heraldedithistory.sql'), - 'legacy' => 103, + 'legacy' => 103, ), '104.searchkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('104.searchkey.sql'), - 'legacy' => 104, + 'legacy' => 104, ), '105.mimetype.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('105.mimetype.sql'), - 'legacy' => 105, + 'legacy' => 105, ), '106.chatlog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('106.chatlog.sql'), - 'legacy' => 106, + 'legacy' => 106, ), '107.oauthserver.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('107.oauthserver.sql'), - 'legacy' => 107, + 'legacy' => 107, ), '108.oauthscope.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('108.oauthscope.sql'), - 'legacy' => 108, + 'legacy' => 108, ), '109.oauthclientphidkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('109.oauthclientphidkey.sql'), - 'legacy' => 109, + 'legacy' => 109, ), '110.commitaudit.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('110.commitaudit.sql'), - 'legacy' => 110, + 'legacy' => 110, ), '111.commitauditmigration.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('111.commitauditmigration.php'), - 'legacy' => 111, + 'legacy' => 111, ), '112.oauthaccesscoderedirecturi.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('112.oauthaccesscoderedirecturi.sql'), - 'legacy' => 112, + 'legacy' => 112, ), '113.lastreviewer.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('113.lastreviewer.sql'), - 'legacy' => 113, + 'legacy' => 113, ), '114.auditrequest.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('114.auditrequest.sql'), - 'legacy' => 114, + 'legacy' => 114, ), '115.prepareutf8.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('115.prepareutf8.sql'), - 'legacy' => 115, + 'legacy' => 115, ), '116.utf8-backup-first-expect-wait.sql' => array( - 'type' => 'sql', - 'name' => - $this->getPatchPath('116.utf8-backup-first-expect-wait.sql'), - 'legacy' => 116, + 'legacy' => 116, ), '117.repositorydescription.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('117.repositorydescription.php'), - 'legacy' => 117, + 'legacy' => 117, ), '118.auditinline.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('118.auditinline.sql'), - 'legacy' => 118, + 'legacy' => 118, ), '119.filehash.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('119.filehash.sql'), - 'legacy' => 119, + 'legacy' => 119, ), '120.noop.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('120.noop.sql'), - 'legacy' => 120, + 'legacy' => 120, ), '121.drydocklog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('121.drydocklog.sql'), - 'legacy' => 121, + 'legacy' => 121, ), '122.flag.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('122.flag.sql'), - 'legacy' => 122, + 'legacy' => 122, ), '123.heraldrulelog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('123.heraldrulelog.sql'), - 'legacy' => 123, + 'legacy' => 123, ), '124.subpriority.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('124.subpriority.sql'), - 'legacy' => 124, + 'legacy' => 124, ), '125.ipv6.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('125.ipv6.sql'), - 'legacy' => 125, + 'legacy' => 125, ), '126.edges.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('126.edges.sql'), - 'legacy' => 126, + 'legacy' => 126, ), '127.userkeybody.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('127.userkeybody.sql'), - 'legacy' => 127, + 'legacy' => 127, ), '128.phabricatorcom.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('128.phabricatorcom.sql'), - 'legacy' => 128, + 'legacy' => 128, ), '129.savedquery.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('129.savedquery.sql'), - 'legacy' => 129, + 'legacy' => 129, ), '130.denormalrevisionquery.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('130.denormalrevisionquery.sql'), - 'legacy' => 130, + 'legacy' => 130, ), '131.migraterevisionquery.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('131.migraterevisionquery.php'), - 'legacy' => 131, + 'legacy' => 131, ), '132.phame.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('132.phame.sql'), - 'legacy' => 132, + 'legacy' => 132, ), '133.imagemacro.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('133.imagemacro.sql'), - 'legacy' => 133, + 'legacy' => 133, ), '134.emptysearch.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('134.emptysearch.sql'), - 'legacy' => 134, + 'legacy' => 134, ), '135.datecommitted.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('135.datecommitted.sql'), - 'legacy' => 135, + 'legacy' => 135, ), '136.sex.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('136.sex.sql'), - 'legacy' => 136, + 'legacy' => 136, ), '137.auditmetadata.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('137.auditmetadata.sql'), - 'legacy' => 137, - ), - '138.notification.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('138.notification.sql'), - ), - 'holidays.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('holidays.sql'), - ), - 'userstatus.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('userstatus.sql'), - ), - 'emailtable.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('emailtable.sql'), + 'legacy' => 137, ), + '138.notification.sql' => array(), + 'holidays.sql' => array(), + 'userstatus.sql' => array(), + 'emailtable.sql' => array(), 'emailtableport.sql' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('emailtableport.php'), - ), - 'emailtableremove.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('emailtableremove.sql'), - ), - 'phiddrop.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('phiddrop.sql'), - ), - 'testdatabase.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('testdatabase.sql'), - ), - 'ldapinfo.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('ldapinfo.sql'), - ), - 'threadtopic.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('threadtopic.sql'), - ), - 'usertranslation.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('usertranslation.sql'), - ), - 'differentialbookmarks.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('differentialbookmarks.sql'), - ), - 'harbormasterobject.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('harbormasterobject.sql'), - ), - 'markupcache.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('markupcache.sql'), - ), - 'maniphestxcache.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('maniphestxcache.sql'), - ), - 'migrate-maniphest-dependencies.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('migrate-maniphest-dependencies.php'), - ), - 'migrate-differential-dependencies.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath( - 'migrate-differential-dependencies.php'), - ), - 'phameblog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('phameblog.sql'), - ), - 'migrate-maniphest-revisions.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('migrate-maniphest-revisions.php'), - ), - 'daemonstatus.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('daemonstatus.sql'), - ), - 'symbolcontexts.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('symbolcontexts.sql'), - ), - 'migrate-project-edges.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('migrate-project-edges.php'), - ), - 'fact-raw.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('fact-raw.sql'), - ), - 'ponder.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('ponder.sql') - ), - 'policy-project.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('policy-project.sql'), - ), - 'daemonstatuskey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('daemonstatuskey.sql'), - ), - 'edgetype.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('edgetype.sql'), - ), - 'ponder-comments.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('ponder-comments.sql'), - ), - 'pastepolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('pastepolicy.sql'), - ), - 'xhprof.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('xhprof.sql'), - ), - 'draft-metadata.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('draft-metadata.sql'), - ), - 'phamedomain.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('phamedomain.sql'), - ), - 'ponder-mailkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('ponder-mailkey.sql'), - ), - 'ponder-mailkey-populate.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('ponder-mailkey-populate.php'), - ), - 'phamepolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('phamepolicy.sql'), - ), - 'phameoneblog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('phameoneblog.sql'), - ), - 'statustxt.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('statustxt.sql'), - ), - 'daemontaskarchive.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('daemontaskarchive.sql'), - ), - 'drydocktaskid.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('drydocktaskid.sql'), + // NOTE: This is a ".php" patch, but the key is ".sql". + 'type' => 'php', + 'name' => $this->getPatchPath('emailtableport.php'), ), + 'emailtableremove.sql' => array(), + 'phiddrop.sql' => array(), + 'testdatabase.sql' => array(), + 'ldapinfo.sql' => array(), + 'threadtopic.sql' => array(), + 'usertranslation.sql' => array(), + 'differentialbookmarks.sql' => array(), + 'harbormasterobject.sql' => array(), + 'markupcache.sql' => array(), + 'maniphestxcache.sql' => array(), + 'migrate-maniphest-dependencies.php' => array(), + 'migrate-differential-dependencies.php' => array(), + 'phameblog.sql' => array(), + 'migrate-maniphest-revisions.php' => array(), + 'daemonstatus.sql' => array(), + 'symbolcontexts.sql' => array(), + 'migrate-project-edges.php' => array(), + 'fact-raw.sql' => array(), + 'ponder.sql' => array(), + 'policy-project.sql' => array(), + 'daemonstatuskey.sql' => array(), + 'edgetype.sql' => array(), + 'ponder-comments.sql' => array(), + 'pastepolicy.sql' => array(), + 'xhprof.sql' => array(), + 'draft-metadata.sql' => array(), + 'phamedomain.sql' => array(), + 'ponder-mailkey.sql' => array(), + 'ponder-mailkey-populate.php' => array(), + 'phamepolicy.sql' => array(), + 'phameoneblog.sql' => array(), + 'statustxt.sql' => array(), + 'daemontaskarchive.sql' => array(), + 'drydocktaskid.sql' => array(), 'drydockresoucetype.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('drydockresourcetype.sql'), - ), - 'liskcounters.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('liskcounters.sql'), - ), - 'liskcounters.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('liskcounters.php'), - ), - 'dropfileproxyimage.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('dropfileproxyimage.sql'), - ), - 'repository-lint.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('repository-lint.sql'), - ), - 'liskcounters-task.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('liskcounters-task.sql'), - ), - 'pholio.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('pholio.sql'), - ), - 'owners-exclude.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('owners-exclude.sql'), - ), - '20121209.pholioxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20121209.pholioxactions.sql'), - ), - '20121209.xmacroadd.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20121209.xmacroadd.sql'), - ), - '20121209.xmacromigrate.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20121209.xmacromigrate.php'), - ), - '20121209.xmacromigratekey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20121209.xmacromigratekey.sql'), - ), - '20121220.generalcache.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20121220.generalcache.sql'), - ), - '20121226.config.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20121226.config.sql'), - ), - '20130101.confxaction.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130101.confxaction.sql'), - ), - '20130102.metamtareceivedmailmessageidhash.sql' => array( - 'type' => 'sql', - 'name' => - $this->getPatchPath('20130102.metamtareceivedmailmessageidhash.sql'), - ), - '20130103.filemetadata.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130103.filemetadata.sql'), - ), - '20130111.conpherence.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130111.conpherence.sql'), - ), - '20130127.altheraldtranscript.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130127.altheraldtranscript.sql'), - ), - '20130201.revisionunsubscribed.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130201.revisionunsubscribed.php'), - ), - '20130201.revisionunsubscribed.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130201.revisionunsubscribed.sql'), - ), - '20130131.conpherencepics.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130131.conpherencepics.sql'), - ), - '20130214.chatlogchannel.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130214.chatlogchannel.sql'), - ), - '20130214.chatlogchannelid.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130214.chatlogchannelid.sql'), - ), - '20130214.token.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130214.token.sql'), - ), - '20130215.phabricatorfileaddttl.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130215.phabricatorfileaddttl.sql'), - ), - '20130217.cachettl.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130217.cachettl.sql'), - ), - '20130218.updatechannelid.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130218.updatechannelid.php'), - ), - '20130218.longdaemon.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130218.longdaemon.sql'), - ), - '20130219.commitsummary.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130219.commitsummary.sql'), - ), - '20130219.commitsummarymig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130219.commitsummarymig.php'), - ), - '20130222.dropchannel.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130222.dropchannel.sql'), - ), - '20130226.commitkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130226.commitkey.sql'), - ), - '20131302.maniphestvalue.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131302.maniphestvalue.sql'), - ), - '20130304.lintauthor.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130304.lintauthor.sql'), - ), - 'releeph.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('releeph.sql'), - ), - '20130319.phabricatorfileexplicitupload.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath( - '20130319.phabricatorfileexplicitupload.sql') - ), - '20130319.conpherence.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130319.conpherence.sql'), - ), - '20130320.phlux.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130320.phlux.sql'), - ), - '20130317.phrictionedge.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130317.phrictionedge.sql'), - ), - '20130321.token.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130321.token.sql'), - ), - '20130310.xactionmeta.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130310.xactionmeta.sql'), - ), - '20130322.phortune.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130322.phortune.sql'), - ), - '20130323.phortunepayment.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130323.phortunepayment.sql'), - ), - '20130324.phortuneproduct.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130324.phortuneproduct.sql'), - ), - '20130330.phrequent.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130330.phrequent.sql'), - ), - '20130403.conpherencecache.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130403.conpherencecache.sql'), - ), - '20130403.conpherencecachemig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130403.conpherencecachemig.php'), - ), - '20130409.commitdrev.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130409.commitdrev.php'), - ), - '20130417.externalaccount.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130417.externalaccount.sql'), - ), - '20130423.updateexternalaccount.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130423.updateexternalaccount.sql'), - ), - '20130423.phortunepaymentrevised.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'), - ), - '20130423.conpherenceindices.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130423.conpherenceindices.sql'), - ), - '20130426.search_savedquery.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130426.search_savedquery.sql'), - ), - '20130502.countdownrevamp1.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130502.countdownrevamp1.sql'), - ), - '20130502.countdownrevamp2.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130502.countdownrevamp2.php'), - ), - '20130502.countdownrevamp3.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130502.countdownrevamp3.sql'), - ), - '20130507.releephrqsimplifycols.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130507.releephrqsimplifycols.sql'), - ), - '20130507.releephrqmailkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130507.releephrqmailkey.sql'), - ), - '20130507.releephrqmailkeypop.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130507.releephrqmailkeypop.php'), - ), - '20130508.search_namedquery.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130508.search_namedquery.sql'), - ), - '20130508.releephtransactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130508.releephtransactions.sql'), - ), - '20130508.releephtransactionsmig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130508.releephtransactionsmig.php'), - ), - '20130513.receviedmailstatus.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130513.receviedmailstatus.sql'), - ), - '20130519.diviner.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130519.diviner.sql'), - ), - '20130521.dropconphimages.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130521.dropconphimages.sql'), - ), - '20130523.maniphest_owners.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130523.maniphest_owners.sql'), - ), - '20130524.repoxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130524.repoxactions.sql'), - ), - '20130529.macroauthor.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130529.macroauthor.sql'), - ), - '20130529.macroauthormig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130529.macroauthormig.php'), - ), - '20130530.sessionhash.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130530.sessionhash.php'), - ), - '20130530.macrodatekey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130530.macrodatekey.sql'), - ), - '20130530.pastekeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130530.pastekeys.sql'), - ), - '20130531.filekeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130531.filekeys.sql'), - ), - '20130602.morediviner.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130602.morediviner.sql'), - ), - '20130602.namedqueries.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130602.namedqueries.sql'), - ), - '20130606.userxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130606.userxactions.sql'), - ), - '20130607.xaccount.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130607.xaccount.sql'), - ), - '20130611.migrateoauth.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130611.migrateoauth.php'), - ), - '20130611.nukeldap.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130611.nukeldap.php'), - ), - '20130613.authdb.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130613.authdb.sql'), - ), - '20130619.authconf.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130619.authconf.php'), - ), - '20130620.diffxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130620.diffxactions.sql'), - ), - '20130621.diffcommentphid.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130621.diffcommentphid.sql'), - ), - '20130621.diffcommentphidmig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130621.diffcommentphidmig.php'), - ), - '20130621.diffcommentunphid.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130621.diffcommentunphid.sql'), - ), - '20130622.doorkeeper.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130622.doorkeeper.sql'), - ), - '20130628.legalpadv0.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130628.legalpadv0.sql'), - ), - '20130701.conduitlog.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130701.conduitlog.sql'), - ), - 'legalpad-mailkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('legalpad-mailkey.sql'), - ), - 'legalpad-mailkey-populate.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('legalpad-mailkey-populate.php'), - ), - '20130703.legalpaddocdenorm.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130703.legalpaddocdenorm.sql'), - ), - '20130703.legalpaddocdenorm.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130703.legalpaddocdenorm.php'), - ), - '20130709.legalpadsignature.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130709.legalpadsignature.sql'), - ), - '20130709.droptimeline.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130709.droptimeline.sql'), - ), - '20130711.trimrealnames.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130711.trimrealnames.php'), - ), - '20130714.votexactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130714.votexactions.sql'), - ), - '20130715.votecomments.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130715.votecomments.php'), - ), - '20130715.voteedges.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130715.voteedges.sql'), - ), - '20130711.pholioimageobsolete.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130711.pholioimageobsolete.sql'), - ), - '20130711.pholioimageobsolete.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130711.pholioimageobsolete.php'), - ), - '20130711.pholioimageobsolete2.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130711.pholioimageobsolete2.sql'), - ), - '20130716.archivememberlessprojects.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130716.archivememberlessprojects.php'), - ), - '20130722.pholioreplace.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130722.pholioreplace.sql'), - ), - '20130723.taskstarttime.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130723.taskstarttime.sql'), - ), - '20130727.ponderquestionstatus.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130727.ponderquestionstatus.sql'), - ), - '20130726.ponderxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130726.ponderxactions.sql'), - ), - '20130728.ponderunique.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130728.ponderunique.php'), - ), - '20130728.ponderuniquekey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130728.ponderuniquekey.sql'), - ), - '20130728.ponderxcomment.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130728.ponderxcomment.php'), - ), - '20130801.pastexactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130801.pastexactions.sql'), - ), - '20130801.pastexactions.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130801.pastexactions.php'), - ), - '20130805.pastemailkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130805.pastemailkey.sql'), - ), - '20130805.pasteedges.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130805.pasteedges.sql'), - ), - '20130805.pastemailkeypop.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130805.pastemailkeypop.php'), - ), - '20130802.heraldphid.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130802.heraldphid.sql'), - ), - '20130802.heraldphids.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130802.heraldphids.php'), - ), - '20130802.heraldphidukey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130802.heraldphidukey.sql'), - ), - '20130802.heraldxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130802.heraldxactions.sql'), - ), - '20130731.releephrepoid.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130731.releephrepoid.sql'), - ), - '20130731.releephproject.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130731.releephproject.sql'), - ), - '20130731.releephcutpointidentifier.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130731.releephcutpointidentifier.sql'), - ), - '20130814.usercustom.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130814.usercustom.sql'), - ), - '20130820.releephxactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130820.releephxactions.sql'), - ), - '20130826.divinernode.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130826.divinernode.sql'), - ), - '20130820.filexactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130820.filexactions.sql'), - ), - '20130820.filemailkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130820.filemailkey.sql'), - ), - '20130820.file-mailkey-populate.php' => array( - 'type' => 'php', - 'name' => - $this->getPatchPath('20130820.file-mailkey-populate.php'), - ), - '20130912.maniphest.1.touch.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130912.maniphest.1.touch.sql'), - ), - '20130912.maniphest.2.created.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130912.maniphest.2.created.sql'), - ), - '20130912.maniphest.3.nameindex.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130912.maniphest.3.nameindex.sql'), - ), - '20130912.maniphest.4.fillindex.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130912.maniphest.4.fillindex.php'), - ), - '20130913.maniphest.1.migratesearch.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130913.maniphest.1.migratesearch.php'), - ), - '20130914.usercustom.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130914.usercustom.sql'), - ), - '20130915.maniphestcustom.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130915.maniphestcustom.sql'), - ), - '20130915.maniphestmigrate.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130915.maniphestmigrate.php'), - ), - '20130919.mfieldconf.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130919.mfieldconf.php'), - ), - '20130920.repokeyspolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130920.repokeyspolicy.sql'), - ), - '20130921.mtransactions.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130921.mtransactions.sql'), - ), - '20130921.xmigratemaniphest.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130921.xmigratemaniphest.php'), - ), - '20130923.mrename.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130923.mrename.sql'), - ), - '20130924.mdraftkey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130924.mdraftkey.sql'), - ), - '20130925.mpolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130925.mpolicy.sql'), - ), - '20130925.xpolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130925.xpolicy.sql'), - ), - '20130926.dcustom.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130926.dcustom.sql'), - ), - '20130926.dinkeys.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130926.dinkeys.sql'), - ), - '20130927.audiomacro.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130927.audiomacro.sql'), - ), - '20130929.filepolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130929.filepolicy.sql'), - ), - '20131004.dxedgekey.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131004.dxedgekey.sql'), - ), - '20131004.dxreviewers.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20131004.dxreviewers.php'), - ), - '20131006.hdisable.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131006.hdisable.sql'), - ), - '20131010.pstorage.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131010.pstorage.sql'), - ), - '20131015.cpolicy.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131015.cpolicy.sql'), - ), - '20130915.maniphestqdrop.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20130915.maniphestqdrop.sql'), - ), - '20130926.dinline.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20130926.dinline.php'), - ), - '20131020.pcustom.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131020.pcustom.sql'), - ), - '20131020.col1.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131020.col1.sql'), - ), - '20131020.pxaction.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131020.pxaction.sql'), - ), - '20131020.pxactionmig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20131020.pxactionmig.php'), - ), - '20131020.harbormaster.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131020.harbormaster.sql'), - ), - '20131025.repopush.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131025.repopush.sql'), - ), - '20131026.commitstatus.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131026.commitstatus.sql'), - ), - '20131030.repostatusmessage.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131030.repostatusmessage.sql'), - ), - '20131031.vcspassword.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131031.vcspassword.sql'), - ), - '20131105.buildstep.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131105.buildstep.sql'), - ), - '20131106.diffphid.1.col.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131106.diffphid.1.col.sql'), - ), - '20131106.diffphid.2.mig.php' => array( - 'type' => 'php', - 'name' => $this->getPatchPath('20131106.diffphid.2.mig.php'), - ), - '20131106.diffphid.3.key.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131106.diffphid.3.key.sql'), - ), - '20131106.nuance-v0.sql' => array( - 'type' => 'sql', - 'name' => $this->getPatchPath('20131106.nuance-v0.sql'), - ), + // NOTE: The key for this patch misspells "resource" as "resouce". + 'name' => $this->getPatchPath('drydockresourcetype.sql'), + ), + 'liskcounters.sql' => array(), + 'liskcounters.php' => array(), + 'dropfileproxyimage.sql' => array(), + 'repository-lint.sql' => array(), + 'liskcounters-task.sql' => array(), + 'pholio.sql' => array(), + 'owners-exclude.sql' => array(), + '20121209.pholioxactions.sql' => array(), + '20121209.xmacroadd.sql' => array(), + '20121209.xmacromigrate.php' => array(), + '20121209.xmacromigratekey.sql' => array(), + '20121220.generalcache.sql' => array(), + '20121226.config.sql' => array(), + '20130101.confxaction.sql' => array(), + '20130102.metamtareceivedmailmessageidhash.sql' => array(), + '20130103.filemetadata.sql' => array(), + '20130111.conpherence.sql' => array(), + '20130127.altheraldtranscript.sql' => array(), + '20130201.revisionunsubscribed.php' => array(), + '20130201.revisionunsubscribed.sql' => array(), + '20130131.conpherencepics.sql' => array(), + '20130214.chatlogchannel.sql' => array(), + '20130214.chatlogchannelid.sql' => array(), + '20130214.token.sql' => array(), + '20130215.phabricatorfileaddttl.sql' => array(), + '20130217.cachettl.sql' => array(), + '20130218.updatechannelid.php' => array(), + '20130218.longdaemon.sql' => array(), + '20130219.commitsummary.sql' => array(), + '20130219.commitsummarymig.php' => array(), + '20130222.dropchannel.sql' => array(), + '20130226.commitkey.sql' => array(), + '20131302.maniphestvalue.sql' => array(), + '20130304.lintauthor.sql' => array(), + 'releeph.sql' => array(), + '20130319.phabricatorfileexplicitupload.sql' => array(), + '20130319.conpherence.sql' => array(), + '20130320.phlux.sql' => array(), + '20130317.phrictionedge.sql' => array(), + '20130321.token.sql' => array(), + '20130310.xactionmeta.sql' => array(), + '20130322.phortune.sql' => array(), + '20130323.phortunepayment.sql' => array(), + '20130324.phortuneproduct.sql' => array(), + '20130330.phrequent.sql' => array(), + '20130403.conpherencecache.sql' => array(), + '20130403.conpherencecachemig.php' => array(), + '20130409.commitdrev.php' => array(), + '20130417.externalaccount.sql' => array(), + '20130423.updateexternalaccount.sql' => array(), + '20130423.phortunepaymentrevised.sql' => array(), + '20130423.conpherenceindices.sql' => array(), + '20130426.search_savedquery.sql' => array(), + '20130502.countdownrevamp1.sql' => array(), + '20130502.countdownrevamp2.php' => array(), + '20130502.countdownrevamp3.sql' => array(), + '20130507.releephrqsimplifycols.sql' => array(), + '20130507.releephrqmailkey.sql' => array(), + '20130507.releephrqmailkeypop.php' => array(), + '20130508.search_namedquery.sql' => array(), + '20130508.releephtransactions.sql' => array(), + '20130508.releephtransactionsmig.php' => array(), + '20130513.receviedmailstatus.sql' => array(), + '20130519.diviner.sql' => array(), + '20130521.dropconphimages.sql' => array(), + '20130523.maniphest_owners.sql' => array(), + '20130524.repoxactions.sql' => array(), + '20130529.macroauthor.sql' => array(), + '20130529.macroauthormig.php' => array(), + '20130530.sessionhash.php' => array(), + '20130530.macrodatekey.sql' => array(), + '20130530.pastekeys.sql' => array(), + '20130531.filekeys.sql' => array(), + '20130602.morediviner.sql' => array(), + '20130602.namedqueries.sql' => array(), + '20130606.userxactions.sql' => array(), + '20130607.xaccount.sql' => array(), + '20130611.migrateoauth.php' => array(), + '20130611.nukeldap.php' => array(), + '20130613.authdb.sql' => array(), + '20130619.authconf.php' => array(), + '20130620.diffxactions.sql' => array(), + '20130621.diffcommentphid.sql' => array(), + '20130621.diffcommentphidmig.php' => array(), + '20130621.diffcommentunphid.sql' => array(), + '20130622.doorkeeper.sql' => array(), + '20130628.legalpadv0.sql' => array(), + '20130701.conduitlog.sql' => array(), + 'legalpad-mailkey.sql' => array(), + 'legalpad-mailkey-populate.php' => array(), + '20130703.legalpaddocdenorm.sql' => array(), + '20130703.legalpaddocdenorm.php' => array(), + '20130709.legalpadsignature.sql' => array(), + '20130709.droptimeline.sql' => array(), + '20130711.trimrealnames.php' => array(), + '20130714.votexactions.sql' => array(), + '20130715.votecomments.php' => array(), + '20130715.voteedges.sql' => array(), + '20130711.pholioimageobsolete.sql' => array(), + '20130711.pholioimageobsolete.php' => array(), + '20130711.pholioimageobsolete2.sql' => array(), + '20130716.archivememberlessprojects.php' => array(), + '20130722.pholioreplace.sql' => array(), + '20130723.taskstarttime.sql' => array(), + '20130727.ponderquestionstatus.sql' => array(), + '20130726.ponderxactions.sql' => array(), + '20130728.ponderunique.php' => array(), + '20130728.ponderuniquekey.sql' => array(), + '20130728.ponderxcomment.php' => array(), + '20130801.pastexactions.sql' => array(), + '20130801.pastexactions.php' => array(), + '20130805.pastemailkey.sql' => array(), + '20130805.pasteedges.sql' => array(), + '20130805.pastemailkeypop.php' => array(), + '20130802.heraldphid.sql' => array(), + '20130802.heraldphids.php' => array(), + '20130802.heraldphidukey.sql' => array(), + '20130802.heraldxactions.sql' => array(), + '20130731.releephrepoid.sql' => array(), + '20130731.releephproject.sql' => array(), + '20130731.releephcutpointidentifier.sql' => array(), + '20130814.usercustom.sql' => array(), + '20130820.releephxactions.sql' => array(), + '20130826.divinernode.sql' => array(), + '20130820.filexactions.sql' => array(), + '20130820.filemailkey.sql' => array(), + '20130820.file-mailkey-populate.php' => array(), + '20130912.maniphest.1.touch.sql' => array(), + '20130912.maniphest.2.created.sql' => array(), + '20130912.maniphest.3.nameindex.sql' => array(), + '20130912.maniphest.4.fillindex.php' => array(), + '20130913.maniphest.1.migratesearch.php' => array(), + '20130914.usercustom.sql' => array(), + '20130915.maniphestcustom.sql' => array(), + '20130915.maniphestmigrate.php' => array(), + '20130919.mfieldconf.php' => array(), + '20130920.repokeyspolicy.sql' => array(), + '20130921.mtransactions.sql' => array(), + '20130921.xmigratemaniphest.php' => array(), + '20130923.mrename.sql' => array(), + '20130924.mdraftkey.sql' => array(), + '20130925.mpolicy.sql' => array(), + '20130925.xpolicy.sql' => array(), + '20130926.dcustom.sql' => array(), + '20130926.dinkeys.sql' => array(), + '20130927.audiomacro.sql' => array(), + '20130929.filepolicy.sql' => array(), + '20131004.dxedgekey.sql' => array(), + '20131004.dxreviewers.php' => array(), + '20131006.hdisable.sql' => array(), + '20131010.pstorage.sql' => array(), + '20131015.cpolicy.sql' => array(), + '20130915.maniphestqdrop.sql' => array(), + '20130926.dinline.php' => array(), + '20131020.pcustom.sql' => array(), + '20131020.col1.sql' => array(), + '20131020.pxaction.sql' => array(), + '20131020.pxactionmig.php' => array(), + '20131020.harbormaster.sql' => array(), + '20131025.repopush.sql' => array(), + '20131026.commitstatus.sql' => array(), + '20131030.repostatusmessage.sql' => array(), + '20131031.vcspassword.sql' => array(), + '20131105.buildstep.sql' => array(), + '20131106.diffphid.1.col.sql' => array(), + '20131106.diffphid.2.mig.php' => array(), + '20131106.diffphid.3.key.sql' => array(), + '20131106.nuance-v0.sql' => array(), + '20131107.buildlog.sql' => array(), + '20131112.userverified.1.col.sql' => array(), + '20131112.userverified.2.mig.php' => array(), + '20131118.ownerorder.php' => array(), + '20131119.passphrase.sql' => array(), + '20131120.nuancesourcetype.sql' => array(), + '20131121.passphraseedge.sql' => array(), + '20131121.repocredentials.1.col.sql' => array(), + '20131121.repocredentials.2.mig.php' => array(), + '20131122.repomirror.sql' => array(), + '20131123.drydockblueprintpolicy.sql' => array(), + '20131129.drydockresourceblueprint.sql' => array(), + '20131205.buildtargets.sql' => array(), + '20131204.pushlog.sql' => array(), + '20131205.buildsteporder.sql' => array(), + '20131205.buildstepordermig.php' => array(), + '20131206.phragment.sql' => array(), + '20131206.phragmentnull.sql' => array(), + '20131208.phragmentsnapshot.sql' => array(), + '20131211.phragmentedges.sql' => array(), + '20131217.pushlogphid.1.col.sql' => array(), + '20131217.pushlogphid.2.mig.php' => array(), + '20131217.pushlogphid.3.key.sql' => array(), + '20131219.pxdrop.sql' => array(), + '20131224.harbormanual.sql' => array(), + '20131227.heraldobject.sql' => array(), + '20131231.dropshortcut.sql' => array(), ); + + // NOTE: STOP! Don't add new patches here. + // Use 'resources/sql/autopatches/' instead! } } diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php index 18c0ed7dcf..e4d5868461 100644 --- a/src/infrastructure/testing/PhabricatorTestCase.php +++ b/src/infrastructure/testing/PhabricatorTestCase.php @@ -105,6 +105,11 @@ abstract class PhabricatorTestCase extends ArcanistPhutilTestCase { 'phabricator.show-beta-applications', true); + // Reset application settings to defaults, particularly policies. + $this->env->overrideEnvConfig( + 'phabricator.application-settings', + array()); + // TODO: Remove this when we remove "releeph.installed". $this->env->overrideEnvConfig('releeph.installed', true); } @@ -176,7 +181,8 @@ abstract class PhabricatorTestCase extends ArcanistPhutilTestCase { $user = id(new PhabricatorUser()) ->setRealName("Test User {$seed}}") - ->setUserName("test{$seed}"); + ->setUserName("test{$seed}") + ->setIsApproved(1); $email = id(new PhabricatorUserEmail()) ->setAddress("testuser{$seed}@example.com") diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 1a590c4b6c..4076106c95 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -111,6 +111,16 @@ final class AphrontDialogView extends AphrontView { return $this; } + public function appendParagraph($paragraph) { + return $this->appendChild( + phutil_tag( + 'p', + array( + 'class' => 'aphront-dialog-view-paragraph', + ), + $paragraph)); + } + final public function render() { require_celerity_resource('aphront-dialog-view-css'); diff --git a/src/view/AphrontView.php b/src/view/AphrontView.php index 33bddb701e..788ea50ab3 100644 --- a/src/view/AphrontView.php +++ b/src/view/AphrontView.php @@ -128,6 +128,23 @@ abstract class AphrontView extends Phobject return $children; } + public function getDefaultResourceSource() { + return 'phabricator'; + } + + public function requireResource($symbol) { + $response = CelerityAPI::getStaticResourceResponse(); + $response->requireResource($symbol, $this->getDefaultResourceSource()); + return $this; + } + + public function initBehavior($name, $config = array()) { + Javelin::initBehavior( + $name, + $config, + $this->getDefaultResourceSource()); + } + /* -( Rendering )---------------------------------------------------------- */ diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php index ede387b65a..17fca10d5a 100644 --- a/src/view/control/AphrontTableView.php +++ b/src/view/control/AphrontTableView.php @@ -272,10 +272,13 @@ final class AphrontTableView extends AphrontView { } } else { $colspan = max(count(array_filter($visibility)), 1); - $table[] = hsprintf( - '%s', - $colspan, - coalesce($this->noDataString, pht('No data available.'))); + $table[] = phutil_tag( + 'tr', + array('class' => 'no-data'), + phutil_tag( + 'td', + array('colspan' => $colspan), + coalesce($this->noDataString, pht('No data available.')))); } $table_class = 'aphront-table-view'; @@ -287,7 +290,7 @@ final class AphrontTableView extends AphrontView { } $html = phutil_tag('table', array('class' => $table_class), $table); - return hsprintf('
    %s
    ', $html); + return phutil_tag_div('aphront-table-wrap', $html); } public static function renderSingleDisplayLine($line) { diff --git a/src/view/control/PhabricatorObjectSelectorDialog.php b/src/view/control/PhabricatorObjectSelectorDialog.php index 0d98861dff..f6e107a99c 100644 --- a/src/view/control/PhabricatorObjectSelectorDialog.php +++ b/src/view/control/PhabricatorObjectSelectorDialog.php @@ -117,22 +117,19 @@ final class PhabricatorObjectSelectorDialog { 'action' => $this->submitURI, 'id' => $search_id, ), - hsprintf( - ' - - - - - ', - phutil_tag( - 'select', - array('id' => $filter_id), - $options), - phutil_tag( - 'input', - array( - 'id' => $query_id, - 'type' => 'text')))); + phutil_tag( + 'table', + array('class' => 'phabricator-object-selector-search'), + phutil_tag('tr', array(), array( + phutil_tag( + 'td', + array('class' => 'phabricator-object-selector-search-filter'), + phutil_tag('select', array('id' => $filter_id), $options)), + phutil_tag( + 'td', + array('class' => 'phabricator-object-selector-search-text'), + phutil_tag('input', array('id' => $query_id, 'type' => 'text'))), + )))); $result_box = phutil_tag( 'div', @@ -142,17 +139,15 @@ final class PhabricatorObjectSelectorDialog { ), ''); - $attached_box = hsprintf( - '
    '. - '
    '. - '
    %s
    '. - '
    '. - '%s'. - '
    '. - '
    ', - $this->header, - $current_id, - $instructions); + $attached_box = phutil_tag_div( + 'phabricator-object-selector-current', + phutil_tag_div( + 'phabricator-object-selector-currently-attached', + array( + phutil_tag_div('phabricator-object-selector-header', $this->header), + phutil_tag('div', array('id' => $current_id)), + $instructions, + ))); $dialog = new AphrontDialogView(); $dialog diff --git a/src/view/form/PHUIFormLayoutView.php b/src/view/form/PHUIFormLayoutView.php index 15347b12f5..3ff5f02670 100644 --- a/src/view/form/PHUIFormLayoutView.php +++ b/src/view/form/PHUIFormLayoutView.php @@ -14,6 +14,29 @@ final class PHUIFormLayoutView extends AphrontView { return $this; } + public function appendInstructions($text) { + return $this->appendChild( + phutil_tag( + 'div', + array( + 'class' => 'aphront-form-instructions', + ), + $text)); + } + + public function appendRemarkupInstructions($remarkup) { + if ($this->getUser() === null) { + throw new Exception( + "Call `setUser` before appending Remarkup to PHUIFormLayoutView."); + } + + return $this->appendInstructions( + PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($remarkup), + 'default', + $this->getUser())); + } + public function render() { $classes = array('phui-form-view'); diff --git a/src/view/form/PHUIFormPageView.php b/src/view/form/PHUIFormPageView.php index ae17e08bd8..c945094957 100644 --- a/src/view/form/PHUIFormPageView.php +++ b/src/view/form/PHUIFormPageView.php @@ -13,7 +13,7 @@ class PHUIFormPageView extends AphrontView { private $isValid; private $validateFormPageCallback; private $adjustFormPageCallback; - private $pageErrors; + private $pageErrors = array(); private $pageName; @@ -191,7 +191,9 @@ class PHUIFormPageView extends AphrontView { public function readFromObject($object) { foreach ($this->getControls() as $name => $control) { - $control->readValueFromDictionary($object); + if (is_array($object)) { + $control->readValueFromDictionary($object); + } } return $this; diff --git a/src/view/form/PHUIPagedFormView.php b/src/view/form/PHUIPagedFormView.php index a088980275..034c86a1eb 100644 --- a/src/view/form/PHUIPagedFormView.php +++ b/src/view/form/PHUIPagedFormView.php @@ -262,20 +262,17 @@ final class PHUIPagedFormView extends AphrontTagView { $form->appendChild($selected_page); $form->appendChild($submit); - if ($errors) { - $errors = id(new AphrontErrorView())->setErrors($errors); - } + $box = id(new PHUIObjectBoxView()) + ->setFormErrors($errors) + ->setForm($form); - $header = null; if ($selected_page->getPageName()) { $header = id(new PHUIHeaderView()) ->setHeader($selected_page->getPageName()); + $box->setHeader($header); } - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setFormError($errors) - ->setForm($form); + return $box; } } diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php index 4d452c448e..6e575fd533 100644 --- a/src/view/form/control/AphrontFormCheckboxControl.php +++ b/src/view/form/control/AphrontFormCheckboxControl.php @@ -38,10 +38,10 @@ final class AphrontFormCheckboxControl extends AphrontFormControl { 'for' => $id, ), $box['label']); - $rows[] = hsprintf( - '%s%s', - $checkbox, - $label); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag('td', array(), $checkbox), + phutil_tag('th', array(), $label) + )); } return phutil_tag( 'table', diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 8f9a7b7441..b926f8b4f3 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -103,6 +103,7 @@ final class AphrontFormPolicyControl extends AphrontFormControl { $options, array( PhabricatorPolicyType::TYPE_GLOBAL, + PhabricatorPolicyType::TYPE_USER, PhabricatorPolicyType::TYPE_CUSTOM, PhabricatorPolicyType::TYPE_PROJECT, )); @@ -123,8 +124,10 @@ final class AphrontFormPolicyControl extends AphrontFormControl { // TODO: Make this configurable. $policy = PhabricatorPolicies::POLICY_USER; } - $this->setValue($policy); + if (!$this->getValue()) { + $this->setValue($policy); + } $control_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); diff --git a/src/view/form/control/AphrontFormRadioButtonControl.php b/src/view/form/control/AphrontFormRadioButtonControl.php index 589968af13..5764275151 100644 --- a/src/view/form/control/AphrontFormRadioButtonControl.php +++ b/src/view/form/control/AphrontFormRadioButtonControl.php @@ -51,15 +51,15 @@ final class AphrontFormRadioButtonControl extends AphrontFormControl { $button['label']); if ($button['caption']) { - $label = hsprintf( - '%s
    %s
    ', + $label = array( $label, - $button['caption']); + phutil_tag_div('aphront-form-radio-caption', $button['caption']), + ); } - $rows[] = hsprintf( - '%s%s', - $radio, - $label); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag('td', array(), $radio), + phutil_tag('th', array(), $label), + )); } return phutil_tag( diff --git a/src/view/form/control/AphrontFormTextWithSubmitControl.php b/src/view/form/control/AphrontFormTextWithSubmitControl.php new file mode 100644 index 0000000000..eeaa597c89 --- /dev/null +++ b/src/view/form/control/AphrontFormTextWithSubmitControl.php @@ -0,0 +1,57 @@ +submitLabel = $submit_label; + return $this; + } + + public function getSubmitLabel() { + return $this->submitLabel; + } + + protected function getCustomControlClass() { + return 'aphront-form-control-text-with-submit'; + } + + protected function renderInput() { + return phutil_tag( + 'div', + array( + 'class' => 'text-with-submit-control-outer-bounds', + ), + array( + phutil_tag( + 'div', + array( + 'class' => 'text-with-submit-control-text-bounds', + ), + javelin_tag( + 'input', + array( + 'type' => 'text', + 'class' => 'text-with-submit-control-text', + 'name' => $this->getName(), + 'value' => $this->getValue(), + 'disabled' => $this->getDisabled() ? 'disabled' : null, + 'id' => $this->getID(), + ))), + phutil_tag( + 'div', + array( + 'class' => 'text-with-submit-control-submit-bounds', + ), + javelin_tag( + 'input', + array( + 'type' => 'submit', + 'class' => 'text-with-submit-control-submit grey', + 'value' => coalesce($this->getSubmitLabel(), pht('Submit')) + ))), + )); + } + +} diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index bf1773ef28..c43fd68405 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -97,6 +97,7 @@ final class AphrontFormTokenizerControl extends AphrontFormControl { 'packages' => pht('Type a package name...'), 'arcanistproject' => pht('Type an arc project name...'), 'accountsorprojects' => pht('Type a user or project name...'), + 'macros' => pht('Type a macro name...'), ); return idx($map, $request); diff --git a/src/view/form/control/PHUIFormMultiSubmitControl.php b/src/view/form/control/PHUIFormMultiSubmitControl.php index ae632c549c..71433e6647 100644 --- a/src/view/form/control/PHUIFormMultiSubmitControl.php +++ b/src/view/form/control/PHUIFormMultiSubmitControl.php @@ -41,6 +41,7 @@ final class PHUIFormMultiSubmitControl extends AphrontFormControl { 'class' => $class, 'disabled' => $this->getDisabled() ? 'disabled' : null, )); + return $this; } protected function getCustomControlClass() { diff --git a/src/view/layout/AphrontContextBarView.php b/src/view/layout/AphrontContextBarView.php index 07c640fbb5..2d5a6d3637 100644 --- a/src/view/layout/AphrontContextBarView.php +++ b/src/view/layout/AphrontContextBarView.php @@ -15,16 +15,15 @@ final class AphrontContextBarView extends AphrontView { require_celerity_resource('aphront-contextbar-view-css'); - return hsprintf( - '
    '. - '
    '. - '
    %s
    '. - '
    %s
    '. - '
    '. - '
    '. - '
    ', - $view->render(), - $this->renderChildren()); + return phutil_tag_div( + 'aphront-contextbar-view', + array( + phutil_tag_div('aphront-contextbar-core', array( + phutil_tag_div('aphront-contextbar-buttons', $view->render()), + phutil_tag_div('aphront-contextbar-content', $this->renderChildren()), + )), + phutil_tag('div', array('style' => 'clear: both;')), + )); } } diff --git a/src/view/layout/AphrontPanelView.php b/src/view/layout/AphrontPanelView.php index 0ae35ef0df..066d538de6 100644 --- a/src/view/layout/AphrontPanelView.php +++ b/src/view/layout/AphrontPanelView.php @@ -69,25 +69,20 @@ final class AphrontPanelView extends AphrontView { } if ($this->caption !== null) { - $caption = phutil_tag( - 'div', - array('class' => 'aphront-panel-view-caption'), - $this->caption); + $caption = phutil_tag_div('aphront-panel-view-caption', $this->caption); } else { $caption = null; } $buttons = null; if ($this->buttons) { - $buttons = hsprintf( - '
    %s
    ', + $buttons = phutil_tag_div( + 'aphront-panel-view-buttons', phutil_implode_html(" ", $this->buttons)); } - $header_elements = hsprintf( - '
    %s%s%s
    ', - $buttons, - $header, - $caption); + $header_elements = phutil_tag_div( + 'aphront-panel-header', + array($buttons, $header, $caption)); $table = phutil_implode_html('', $this->renderChildren()); diff --git a/src/view/layout/PhabricatorActionHeaderView.php b/src/view/layout/PhabricatorActionHeaderView.php index 4ce655df4f..eda01d392e 100644 --- a/src/view/layout/PhabricatorActionHeaderView.php +++ b/src/view/layout/PhabricatorActionHeaderView.php @@ -32,7 +32,7 @@ final class PhabricatorActionHeaderView extends AphrontView { return $this; } - public function setTag(PhabricatorTagView $tag) { + public function setTag(PHUITagView $tag) { $this->actions[] = $tag; return $this; } diff --git a/src/view/layout/PhabricatorCrumbsView.php b/src/view/layout/PhabricatorCrumbsView.php index f9c378655e..ec411ca6e6 100644 --- a/src/view/layout/PhabricatorCrumbsView.php +++ b/src/view/layout/PhabricatorCrumbsView.php @@ -10,6 +10,22 @@ final class PhabricatorCrumbsView extends AphrontView { return false; } + + /** + * Convenience method for adding a simple crumb with just text, or text and + * a link. + * + * @param string Text of the crumb. + * @param string? Optional href for the crumb. + * @return this + */ + public function addTextCrumb($text, $href = null) { + return $this->addCrumb( + id(new PhabricatorCrumbView()) + ->setName($text) + ->setHref($href)); + } + public function addCrumb(PhabricatorCrumbView $crumb) { $this->crumbs[] = $crumb; return $this; diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php index b8396b78b2..7c2b64d660 100644 --- a/src/view/layout/PhabricatorSourceCodeView.php +++ b/src/view/layout/PhabricatorSourceCodeView.php @@ -116,11 +116,8 @@ final class PhabricatorSourceCodeView extends AphrontView { $classes[] = 'remarkup-code'; $classes[] = 'PhabricatorMonospaced'; - return phutil_tag( - 'div', - array( - 'class' => 'phabricator-source-code-container', - ), + return phutil_tag_div( + 'phabricator-source-code-container', javelin_tag( 'table', array( diff --git a/src/view/layout/PhabricatorTimelineEventView.php b/src/view/layout/PhabricatorTimelineEventView.php index e190c8f73b..54f226dbbb 100644 --- a/src/view/layout/PhabricatorTimelineEventView.php +++ b/src/view/layout/PhabricatorTimelineEventView.php @@ -109,23 +109,41 @@ final class PhabricatorTimelineEventView extends AphrontView { return $this; } - - public function renderEventTitle() { + protected function renderEventTitle($is_first_event, $force_icon) { $title = $this->title; if (($title === null) && !$this->hasChildren()) { $title = ''; } - $extra = $this->renderExtra(); + if ($is_first_event) { + $extra = array(); + $is_first_extra = true; + foreach ($this->getEventGroup() as $event) { + $extra[] = $event->renderExtra($is_first_extra); + $is_first_extra = false; + } + $extra = array_reverse($extra); + $extra = array_mergev($extra); + $extra = phutil_tag( + 'span', + array( + 'class' => 'phabricator-timeline-extra', + ), + phutil_implode_html(" \xC2\xB7 ", $extra)); + } else { + $extra = null; + } - if ($title !== null || $extra !== null) { + if ($title !== null || $extra) { $title_classes = array(); $title_classes[] = 'phabricator-timeline-title'; $icon = null; - if ($this->icon) { + if ($this->icon || $force_icon) { $title_classes[] = 'phabricator-timeline-title-with-icon'; + } + if ($this->icon) { $fill_classes = array(); $fill_classes[] = 'phabricator-timeline-icon-fill'; if ($this->color) { @@ -159,10 +177,24 @@ final class PhabricatorTimelineEventView extends AphrontView { public function render() { + $events = $this->getEventGroup(); + + // Move events with icons first. + $icon_keys = array(); + foreach ($this->getEventGroup() as $key => $event) { + if ($event->icon) { + $icon_keys[] = $key; + } + } + $events = array_select_keys($events, $icon_keys) + $events; + $force_icon = (bool)$icon_keys; + $group_titles = array(); $group_children = array(); - foreach ($this->getEventGroup() as $event) { - $group_titles[] = $event->renderEventTitle(); + $is_first_event = true; + foreach ($events as $event) { + $group_titles[] = $event->renderEventTitle($is_first_event, $force_icon); + $is_first_event = false; if ($event->hasChildren()) { $group_children[] = $event->renderChildren(); } @@ -264,7 +296,7 @@ final class PhabricatorTimelineEventView extends AphrontView { $content)); } - private function renderExtra() { + private function renderExtra($is_first_extra) { $extra = array(); if ($this->getIsPreview()) { @@ -272,7 +304,6 @@ final class PhabricatorTimelineEventView extends AphrontView { } else { $xaction_phid = $this->getTransactionPHID(); - if ($this->getIsEdited()) { $extra[] = javelin_tag( 'a', @@ -293,46 +324,50 @@ final class PhabricatorTimelineEventView extends AphrontView { pht('Edit')); } - $source = $this->getContentSource(); - if ($source) { - $extra[] = id(new PhabricatorContentSourceView()) - ->setContentSource($source) - ->setUser($this->getUser()) - ->render(); - } - - if ($this->getDateCreated()) { - $date = phabricator_datetime( - $this->getDateCreated(), - $this->getUser()); - if ($this->anchor) { - Javelin::initBehavior('phabricator-watch-anchor'); - - $anchor = id(new PhabricatorAnchorView()) - ->setAnchorName($this->anchor) + if ($is_first_extra) { + $source = $this->getContentSource(); + if ($source) { + $extra[] = id(new PhabricatorContentSourceView()) + ->setContentSource($source) + ->setUser($this->getUser()) ->render(); - - $date = array( - $anchor, - phutil_tag( - 'a', - array( - 'href' => '#'.$this->anchor, - ), - $date), - ); } - $extra[] = $date; - } - } - if ($extra) { - $extra = phutil_tag( - 'span', - array( - 'class' => 'phabricator-timeline-extra', - ), - phutil_implode_html(" \xC2\xB7 ", $extra)); + $date_created = null; + foreach ($this->getEventGroup() as $event) { + if ($event->getDateCreated()) { + if ($date_created === null) { + $date_created = $event->getDateCreated(); + } else { + $date_created = min($event->getDateCreated(), $date_created); + } + } + } + + if ($date_created) { + $date = phabricator_datetime( + $this->getDateCreated(), + $this->getUser()); + if ($this->anchor) { + Javelin::initBehavior('phabricator-watch-anchor'); + + $anchor = id(new PhabricatorAnchorView()) + ->setAnchorName($this->anchor) + ->render(); + + $date = array( + $anchor, + phutil_tag( + 'a', + array( + 'href' => '#'.$this->anchor, + ), + $date), + ); + } + $extra[] = $date; + } + } } return $extra; diff --git a/src/view/layout/PhabricatorTransactionView.php b/src/view/layout/PhabricatorTransactionView.php index 7ceb3023bc..f87a1f3df9 100644 --- a/src/view/layout/PhabricatorTransactionView.php +++ b/src/view/layout/PhabricatorTransactionView.php @@ -62,6 +62,10 @@ final class PhabricatorTransactionView extends AphrontView { $transaction_id = $this->anchorName ? 'anchor-'.$this->anchorName : null; + $header = phutil_tag_div( + 'phabricator-transaction-header', + array($info, $actions)); + return phutil_tag( 'div', array( @@ -69,15 +73,9 @@ final class PhabricatorTransactionView extends AphrontView { 'id' => $transaction_id, 'style' => $style, ), - hsprintf( - '
    '. - '
    %s%s
    '. - '%s'. - '
    ', - $classes, - $info, - $actions, - $content)); + phutil_tag_div( + 'phabricator-transaction-detail '.$classes, + array($header, $content))); } @@ -118,8 +116,9 @@ final class PhabricatorTransactionView extends AphrontView { $info = phutil_implode_html(" \xC2\xB7 ", $info); - return hsprintf( - '%s', + return phutil_tag( + 'span', + array('class' => 'phabricator-transaction-info'), $info); } @@ -139,9 +138,8 @@ final class PhabricatorTransactionView extends AphrontView { if (!$this->hasChildren()) { return null; } - return phutil_tag( - 'div', - array('class' => 'phabricator-transaction-content'), + return phutil_tag_div( + 'phabricator-transaction-content', $this->renderChildren()); } diff --git a/src/view/page/AphrontRequestFailureView.php b/src/view/page/AphrontRequestFailureView.php index 1965340dd5..5910b4468f 100644 --- a/src/view/page/AphrontRequestFailureView.php +++ b/src/view/page/AphrontRequestFailureView.php @@ -13,15 +13,15 @@ final class AphrontRequestFailureView extends AphrontView { final public function render() { require_celerity_resource('aphront-request-failure-view-css'); - return hsprintf( - '
    '. - '
    '. - '

    %s

    '. - '
    '. - '
    %s
    '. - '
    ', - $this->header, + $head = phutil_tag_div( + 'aphront-request-failure-head', + phutil_tag('h1', array(), $this->header)); + + $body = phutil_tag_div( + 'aphront-request-failure-body', $this->renderChildren()); + + return phutil_tag_div('aphront-request-failure-view', array($head, $body)); } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index affcc3bd88..d31c3567b8 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -185,6 +185,9 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } + if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { + $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; + } Javelin::initBehavior( 'dark-console', @@ -252,7 +255,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { parent::getHead(), phutil_safe_html($monospaced), phutil_safe_html($monospaced_win), - $response->renderSingleResource('javelin-magical-init')); + $response->renderSingleResource('javelin-magical-init', 'phabricator')); } public function setGlyph($glyph) { @@ -298,11 +301,8 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { $developer_warning = null; if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && DarkConsoleErrorLogPluginAPI::getErrors()) { - $developer_warning = phutil_tag( - 'div', - array( - 'class' => 'aphront-developer-error-callout', - ), + $developer_warning = phutil_tag_div( + 'aphront-developer-error-callout', pht( 'This page raised PHP errors. Find them in DarkConsole '. 'or the error log.')); @@ -313,11 +313,8 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { if ($user && $user->getIsAdmin()) { $open = PhabricatorSetupCheck::getOpenSetupIssueCount(); if ($open) { - $setup_warning = phutil_tag( - 'div', - array( - 'class' => 'setup-warning-callout', - ), + $setup_warning = phutil_tag_div( + 'setup-warning-callout', phutil_tag( 'a', array( @@ -334,16 +331,16 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { 'id' => 'base-page', 'class' => 'phabricator-standard-page', ), - hsprintf( - '%s%s%s'. - '
    '. - '%s%s
    '. - '
    ', - $developer_warning, - $setup_warning, - $header_chrome, - ($console ? hsprintf('') : null), - parent::getBody())); + array( + $developer_warning, + $setup_warning, + $header_chrome, + phutil_tag_div('phabricator-standard-page-body', array( + ($console ? hsprintf('') : null), + parent::getBody(), + phutil_tag('div', array('style' => 'clear: both;')), + )), + )); } protected function getTail() { diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php index 1f416a1f94..4d2cd11045 100644 --- a/src/view/page/menu/PhabricatorMainMenuSearchView.php +++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php @@ -66,13 +66,12 @@ final class PhabricatorMainMenuSearchView extends AphrontView { 'action' => '/search/', 'method' => 'POST', ), - hsprintf( - '
    '. - '%s%s%s'. - '
    ', + phutil_tag_div('phabricator-main-menu-search-container', array( $input, + phutil_tag('button', array(), pht('Search')), $scope_input, - $target)); + $target, + ))); return $form; } diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index 9d8b25228d..9f656e2c78 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -37,6 +37,7 @@ final class PhabricatorMainMenuView extends AphrontView { $user = $this->user; require_celerity_resource('phabricator-main-menu-view'); + require_celerity_resource('sprite-main-header-css'); $header_id = celerity_generate_unique_node_id(); $menus = array(); @@ -44,7 +45,7 @@ final class PhabricatorMainMenuView extends AphrontView { $search_button = ''; $app_button = ''; - if ($user->isLoggedIn()) { + if ($user->isLoggedIn() && $user->isUserActivated()) { list($menu, $dropdowns) = $this->renderNotificationMenu(); $alerts[] = $menu; $menus = array_merge($menus, $dropdowns); @@ -64,11 +65,15 @@ final class PhabricatorMainMenuView extends AphrontView { } $application_menu = $this->renderApplicationMenu(); + $classes = array(); + $classes[] = 'phabricator-main-menu'; + $classes[] = 'sprite-main-header'; + $classes[] = 'main-header-'.PhabricatorEnv::getEnvConfig('ui.header-color'); return phutil_tag( 'div', array( - 'class' => 'phabricator-main-menu', + 'class' => implode(' ', $classes), 'id' => $header_id, ), array( @@ -91,8 +96,11 @@ final class PhabricatorMainMenuView extends AphrontView { 'helpURI' => '/help/keyboardshortcut/', ); - $show_search = ($user->isLoggedIn()) || - (PhabricatorEnv::getEnvConfig('policy.allow-public')); + if ($user->isLoggedIn()) { + $show_search = $user->isUserActivated(); + } else { + $show_search = PhabricatorEnv::getEnvConfig('policy.allow-public'); + } if ($show_search) { $search = new PhabricatorMainMenuSearchView(); @@ -155,6 +163,8 @@ final class PhabricatorMainMenuView extends AphrontView { } } + $actions = msort($actions, 'getOrder'); + $view = $this->getApplicationMenu(); if (!$view) { @@ -224,6 +234,8 @@ final class PhabricatorMainMenuView extends AphrontView { } private function renderPhabricatorLogo() { + $class = 'phabricator-main-menu-logo-image'; + return phutil_tag( 'a', array( @@ -233,7 +245,7 @@ final class PhabricatorMainMenuView extends AphrontView { phutil_tag( 'span', array( - 'class' => 'sprite-menu phabricator-main-menu-logo-image', + 'class' => 'sprite-menu menu-logo-image '.$class, ), '')); } @@ -383,6 +395,13 @@ final class PhabricatorMainMenuView extends AphrontView { $notification_dropdown, $message_notification_dropdown); + $applications = PhabricatorApplication::getAllInstalledApplications(); + foreach ($applications as $application) { + $dropdowns[] = $application->buildMainMenuExtraNodes( + $this->getUser(), + $this->getController()); + } + return array( hsprintf('%s%s', $bubble_tag, $message_tag), $dropdowns diff --git a/src/view/phui/PHUIButtonView.php b/src/view/phui/PHUIButtonView.php index 3970b882f2..e3c8e8ec06 100644 --- a/src/view/phui/PHUIButtonView.php +++ b/src/view/phui/PHUIButtonView.php @@ -6,6 +6,7 @@ final class PHUIButtonView extends AphrontTagView { const GREY = 'grey'; const BLACK = 'black'; const DISABLED = 'disabled'; + const SIMPLE = 'simple'; const SMALL = 'small'; const BIG = 'big'; @@ -17,12 +18,18 @@ final class PHUIButtonView extends AphrontTagView { private $tag = 'button'; private $dropdown; private $icon; + private $href = null; public function setText($text) { $this->text = $text; return $this; } + public function setHref($href) { + $this->href = $href; + return $this; + } + public function setSubtext($subtext) { $this->subtext = $subtext; return $this; @@ -80,7 +87,8 @@ final class PHUIButtonView extends AphrontTagView { $classes[] = 'has-icon'; } - return array('class' => $classes); + return array('class' => $classes, + 'href' => $this->href); } protected function getTagContent() { diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index 008c286e49..3b9183dab9 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -99,7 +99,7 @@ final class PHUIFeedStoryView extends AphrontView { return $this->href; } - public function renderNotification() { + public function renderNotification($user) { $classes = array( 'phabricator-notification', ); @@ -107,6 +107,20 @@ final class PHUIFeedStoryView extends AphrontView { if (!$this->viewed) { $classes[] = 'phabricator-notification-unread'; } + if ($this->epoch) { + if ($user) { + $foot = phabricator_datetime($this->epoch, $user); + $foot = phutil_tag( + 'span', + array( + 'class' => 'phabricator-notification-date'), + $foot); + } else { + $foot = null; + } + } else { + $foot = pht('No time specified.'); + } return javelin_tag( 'div', @@ -117,7 +131,7 @@ final class PHUIFeedStoryView extends AphrontView { 'href' => $this->getHref(), ), ), - $this->title); + array($this->title, $foot)); } public function render() { diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 5bfb58785a..5d17059db6 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -13,6 +13,7 @@ final class PHUIHeaderView extends AphrontView { private $noBackground; private $bleedHeader; private $properties = array(); + private $actionLinks = array(); private $policyObject; public function setHeader($header) { @@ -30,7 +31,7 @@ final class PHUIHeaderView extends AphrontView { return $this; } - public function addTag(PhabricatorTagView $tag) { + public function addTag(PHUITagView $tag) { $this->tags[] = $tag; return $this; } @@ -65,6 +66,11 @@ final class PHUIHeaderView extends AphrontView { return $this; } + public function addActionLink(PHUIButtonView $button) { + $this->actionLinks[] = $button; + return $this; + } + public function setStatus($icon, $color, $name) { $header_class = 'phui-header-status'; @@ -184,6 +190,22 @@ final class PHUIHeaderView extends AphrontView { $property_list); } + if ($this->actionLinks) { + $actions = array(); + foreach ($this->actionLinks as $button) { + $button->setColor(PHUIButtonView::SIMPLE); + $button->addClass(PHUI::MARGIN_SMALL_LEFT); + $button->addClass('phui-header-action-link'); + $actions[] = $button; + } + $header[] = phutil_tag( + 'div', + array( + 'class' => 'phui-header-action-links', + ), + $actions); + } + return phutil_tag( 'div', array( diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php index 9af36ab589..ef8b8da880 100644 --- a/src/view/phui/PHUIListItemView.php +++ b/src/view/phui/PHUIListItemView.php @@ -19,10 +19,21 @@ final class PHUIListItemView extends AphrontTagView { private $isExternal; private $key; private $icon; + private $appIcon; private $selected; private $disabled; private $renderNameAsTooltip; private $statusColor; + private $order; + + public function setOrder($order) { + $this->order = $order; + return $this; + } + + public function getOrder() { + return $this->order; + } public function setRenderNameAsTooltip($render_name_as_tooltip) { $this->renderNameAsTooltip = $render_name_as_tooltip; @@ -47,6 +58,11 @@ final class PHUIListItemView extends AphrontTagView { return $this; } + public function setAppIcon($icon) { + $this->appIcon = $icon; + return $this; + } + public function getIcon() { return $this->icon; } @@ -110,7 +126,7 @@ final class PHUIListItemView extends AphrontTagView { $classes[] = 'phui-list-item-view'; $classes[] = 'phui-list-item-'.$this->type; - if ($this->icon) { + if ($this->icon || $this->appIcon) { $classes[] = 'phui-list-item-has-icon'; } @@ -178,6 +194,13 @@ final class PHUIListItemView extends AphrontTagView { ->setSpriteIcon($icon_name); } + if ($this->appIcon) { + $icon = id(new PHUIIconView()) + ->addClass('phui-list-item-icon') + ->setSpriteSheet(PHUIIconView::SPRITE_APPS) + ->setSpriteIcon($this->appIcon); + } + return javelin_tag( $this->href ? 'a' : 'div', array( diff --git a/src/view/phui/PHUIObjectBoxView.php b/src/view/phui/PHUIObjectBoxView.php index 1325d89bef..a419d91fb4 100644 --- a/src/view/phui/PHUIObjectBoxView.php +++ b/src/view/phui/PHUIObjectBoxView.php @@ -3,11 +3,14 @@ final class PHUIObjectBoxView extends AphrontView { private $headerText; - private $formError = null; + private $formErrors = null; + private $formSaved = false; + private $errorView; private $form; private $validationException; private $header; private $flush; + private $id; private $tabs = array(); private $propertyLists = array(); @@ -63,8 +66,30 @@ final class PHUIObjectBoxView extends AphrontView { return $this; } - public function setFormError($error) { - $this->formError = $error; + public function setFormErrors(array $errors, $title = null) { + if (nonempty($errors)) { + $this->formErrors = id(new AphrontErrorView()) + ->setTitle($title) + ->setErrors($errors); + } + return $this; + } + + public function setFormSaved($saved, $text = null) { + if (!$text) { + $text = pht('Changes saved.'); + } + if ($saved) { + $save = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->appendChild($text); + $this->formSaved = $save; + } + return $this; + } + + public function setErrorView(AphrontErrorView $view) { + $this->errorView = $view; return $this; } @@ -73,6 +98,11 @@ final class PHUIObjectBoxView extends AphrontView { return $this; } + public function setID($id) { + $this->id = $id; + return $this; + } + public function setHeader(PHUIHeaderView $header) { $this->header = $header; return $this; @@ -187,7 +217,9 @@ final class PHUIObjectBoxView extends AphrontView { ->appendChild( array( $header, - $this->formError, + $this->errorView, + $this->formErrors, + $this->formSaved, $exception_errors, $this->form, $tabs, @@ -196,6 +228,7 @@ final class PHUIObjectBoxView extends AphrontView { $this->renderChildren(), )) ->setBorder(true) + ->setID($this->id) ->addMargin(PHUI::MARGIN_LARGE_TOP) ->addMargin(PHUI::MARGIN_LARGE_LEFT) ->addMargin(PHUI::MARGIN_LARGE_RIGHT) diff --git a/src/view/phui/PHUIObjectItemListView.php b/src/view/phui/PHUIObjectItemListView.php index 64cc6dc84d..b57d01a04b 100644 --- a/src/view/phui/PHUIObjectItemListView.php +++ b/src/view/phui/PHUIObjectItemListView.php @@ -10,6 +10,17 @@ final class PHUIObjectItemListView extends AphrontTagView { private $noDataString; private $flush; private $plain; + private $allowEmptyList; + + + public function setAllowEmptyList($allow_empty_list) { + $this->allowEmptyList = $allow_empty_list; + return $this; + } + + public function getAllowEmptyList() { + return $this->allowEmptyList; + } public function setFlush($flush) { $this->flush = $flush; @@ -92,6 +103,8 @@ final class PHUIObjectItemListView extends AphrontTagView { if ($this->items) { $items = $this->items; + } else if ($this->allowEmptyList) { + $items = null; } else { $string = nonempty($this->noDataString, pht('No data.')); $items = id(new AphrontErrorView()) diff --git a/src/view/phui/PHUIStatusListView.php b/src/view/phui/PHUIStatusListView.php index 64c115ecf0..9200da4531 100644 --- a/src/view/phui/PHUIStatusListView.php +++ b/src/view/phui/PHUIStatusListView.php @@ -6,6 +6,7 @@ final class PHUIStatusListView extends AphrontTagView { public function addItem(PHUIStatusItemView $item) { $this->items[] = $item; + return $this; } protected function canAppendChild() { diff --git a/src/view/layout/PhabricatorTagView.php b/src/view/phui/PHUITagView.php similarity index 80% rename from src/view/layout/PhabricatorTagView.php rename to src/view/phui/PHUITagView.php index 1607fb9ad8..d31a06656a 100644 --- a/src/view/layout/PhabricatorTagView.php +++ b/src/view/phui/PHUITagView.php @@ -1,6 +1,6 @@ id = $id; @@ -53,11 +53,6 @@ final class PhabricatorTagView extends AphrontView { return $this; } - public function setBarColor($bar_color) { - $this->barColor = $bar_color; - return $this; - } - public function setDotColor($dot_color) { $this->dotColor = $dot_color; return $this; @@ -88,58 +83,60 @@ final class PhabricatorTagView extends AphrontView { return $this; } + public function setIcon($icon) { + $icon_view = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) + ->setSpriteIcon($icon); + $this->icon = $icon_view; + return $this; + } + public function render() { if (!$this->type) { throw new Exception(pht("You must call setType() before render()!")); } - require_celerity_resource('phabricator-tag-view-css'); + require_celerity_resource('phui-tag-view-css'); $classes = array( - 'phabricator-tag-view', - 'phabricator-tag-type-'.$this->type, + 'phui-tag-view', + 'phui-tag-type-'.$this->type, ); if ($this->closed) { - $classes[] = 'phabricator-tag-state-closed'; + $classes[] = 'phui-tag-state-closed'; } $color = null; if ($this->backgroundColor) { - $color = 'phabricator-tag-color-'.$this->backgroundColor; + $color = 'phui-tag-color-'.$this->backgroundColor; } if ($this->dotColor) { - $dotcolor = 'phabricator-tag-color-'.$this->dotColor; + $dotcolor = 'phui-tag-color-'.$this->dotColor; $dot = phutil_tag( 'span', array( - 'class' => 'phabricator-tag-dot '.$dotcolor, + 'class' => 'phui-tag-dot '.$dotcolor, ), ''); } else { $dot = null; } + if ($this->icon) { + $icon = $this->icon; + $classes[] = 'phui-tag-icon-view'; + } else { + $icon = null; + } + $content = phutil_tag( 'span', array( - 'class' => 'phabricator-tag-core '.$color, + 'class' => 'phui-tag-core '.$color, ), array($dot, $this->name)); - if ($this->barColor) { - $barcolor = 'phabricator-tag-color-'.$this->barColor; - $bar = phutil_tag( - 'span', - array( - 'class' => 'phabricator-tag-bar '.$barcolor, - ), - ''); - $classes[] = 'phabricator-tag-view-has-bar'; - } else { - $bar = null; - } - if ($this->phid) { Javelin::initBehavior('phabricator-hovercards'); @@ -155,7 +152,7 @@ final class PhabricatorTagView extends AphrontView { ), 'target' => $this->external ? '_blank' : null, ), - array($bar, $content)); + array($icon, $content)); } else { return phutil_tag( $this->href ? 'a' : 'span', @@ -165,7 +162,7 @@ final class PhabricatorTagView extends AphrontView { 'class' => implode(' ', $classes), 'target' => $this->external ? '_blank' : null, ), - array($bar, $content)); + array($icon, $content)); } } diff --git a/src/view/phui/PHUIWorkboardView.php b/src/view/phui/PHUIWorkboardView.php index 80a059f348..06ee373b6a 100644 --- a/src/view/phui/PHUIWorkboardView.php +++ b/src/view/phui/PHUIWorkboardView.php @@ -1,6 +1,6 @@ 'phui-workboard-view', + ); + } + + public function getTagContent() { require_celerity_resource('phui-workboard-view-css'); $action_list = null; @@ -68,14 +74,9 @@ final class PHUIWorkboardView extends AphrontView { ), $view); - return phutil_tag( - 'div', - array( - 'class' => 'phui-workboard-view' - ), - array( - $action_list, - $board - )); + return array( + $action_list, + $board, + ); } } diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index cedcc653a1..2d4b946c1a 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -1,11 +1,12 @@ cards[] = $cards; @@ -17,9 +18,8 @@ final class PHUIWorkpanelView extends AphrontView { return $this; } - public function setHeaderAction($header_action) { - // TODO: This doesn't do anything? - $this->headerAction = $header_action; + public function setEditURI($edit_uri) { + $this->editURI = $edit_uri; return $this; } @@ -28,7 +28,18 @@ final class PHUIWorkpanelView extends AphrontView { return $this; } - public function render() { + public function setHeaderColor($header_color) { + $this->headerColor = $header_color; + return $this; + } + + public function getTagAttributes() { + return array( + 'class' => 'phui-workpanel-view', + ); + } + + public function getTagContent() { require_celerity_resource('phui-workpanel-view-css'); $footer = ''; @@ -42,9 +53,15 @@ final class PHUIWorkpanelView extends AphrontView { $footer_tag); } + $header_edit = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) + ->setSpriteIcon('settings-grey') + ->setHref($this->editURI); + $header = id(new PhabricatorActionHeaderView()) ->setHeaderTitle($this->header) - ->setHeaderColor(PhabricatorActionHeaderView::HEADER_GREY); + ->setHeaderColor($this->headerColor) + ->addAction($header_edit); $body = phutil_tag( 'div', @@ -64,11 +81,6 @@ final class PHUIWorkpanelView extends AphrontView { $footer, )); - return phutil_tag( - 'div', - array( - 'class' => 'phui-workpanel-view' - ), - $view); + return $view; } } diff --git a/src/view/widget/AphrontStackTraceView.php b/src/view/widget/AphrontStackTraceView.php new file mode 100644 index 0000000000..61811a28e9 --- /dev/null +++ b/src/view/widget/AphrontStackTraceView.php @@ -0,0 +1,120 @@ +trace = $trace; + return $this; + } + + public function render() { + $user = $this->getUser(); + $trace = $this->trace; + + $libraries = PhutilBootloader::getInstance()->getAllLibraries(); + + // TODO: Make this configurable? + $path = 'https://secure.phabricator.com/diffusion/%s/browse/master/src/'; + + $callsigns = array( + 'arcanist' => 'ARC', + 'phutil' => 'PHU', + 'phabricator' => 'P', + ); + + $rows = array(); + $depth = count($trace); + foreach ($trace as $part) { + $lib = null; + $file = idx($part, 'file'); + $relative = $file; + foreach ($libraries as $library) { + $root = phutil_get_library_root($library); + if (Filesystem::isDescendant($file, $root)) { + $lib = $library; + $relative = Filesystem::readablePath($file, $root); + break; + } + } + + $where = ''; + if (isset($part['class'])) { + $where .= $part['class'].'::'; + } + if (isset($part['function'])) { + $where .= $part['function'].'()'; + } + + if ($file) { + if (isset($callsigns[$lib])) { + $attrs = array('title' => $file); + try { + $attrs['href'] = $user->loadEditorLink( + '/src/'.$relative, + $part['line'], + $callsigns[$lib]); + } catch (Exception $ex) { + // The database can be inaccessible. + } + if (empty($attrs['href'])) { + $attrs['href'] = sprintf($path, $callsigns[$lib]). + str_replace(DIRECTORY_SEPARATOR, '/', $relative). + '$'.$part['line']; + $attrs['target'] = '_blank'; + } + $file_name = phutil_tag( + 'a', + $attrs, + $relative); + } else { + $file_name = phutil_tag( + 'span', + array( + 'title' => $file, + ), + $relative); + } + $file_name = hsprintf('%s : %d', $file_name, $part['line']); + } else { + $file_name = phutil_tag('em', array(), '(Internal)'); + } + + + $rows[] = array( + $depth--, + $lib, + $file_name, + $where, + ); + } + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'Depth', + 'Library', + 'File', + 'Where', + )); + $table->setColumnClasses( + array( + 'n', + '', + '', + 'wide', + )); + + return phutil_tag( + 'div', + array('class' => 'exception-trace'), + array( + phutil_tag( + 'div', + array('class' => 'exception-trace-header'), + pht('Stack Trace')), + $table->render(), + )); + } + +} diff --git a/src/view/widget/bars/AphrontProgressBarView.php b/src/view/widget/bars/AphrontProgressBarView.php index 4584c08c8f..ef63cd0ccd 100644 --- a/src/view/widget/bars/AphrontProgressBarView.php +++ b/src/view/widget/bars/AphrontProgressBarView.php @@ -38,18 +38,15 @@ final class AphrontProgressBarView extends AphrontBarView { $color = $this->getColor(); - return phutil_tag( - 'div', - array( - 'class' => "aphront-bar progress color-{$color}", - ), + return phutil_tag_div( + "aphront-bar progress color-{$color}", array( phutil_tag( 'div', array('title' => $this->alt), phutil_tag( 'div', - array('style' => hsprintf("width: %dpx;", $width)), + array('style' => "width: {$width}px;"), '')), phutil_tag( 'span', diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php index 7b68046bb1..5f0dea8fa4 100644 --- a/src/view/widget/hovercard/PhabricatorHovercardView.php +++ b/src/view/widget/hovercard/PhabricatorHovercardView.php @@ -51,7 +51,7 @@ final class PhabricatorHovercardView extends AphrontView { return $this; } - public function addTag(PhabricatorTagView $tag) { + public function addTag(PHUITagView $tag) { $this->tags[] = $tag; return $this; } @@ -92,22 +92,15 @@ final class PhabricatorHovercardView extends AphrontView { $body_title = $handle->getFullName(); } - $body[] = phutil_tag( - 'div', - array( - 'class' => 'phabricator-hovercard-body-header' - ), - $body_title); + $body[] = phutil_tag_div('phabricator-hovercard-body-header', $body_title); foreach ($this->fields as $field) { - $item = hsprintf('%s: %s', - $field['label'], $field['value']); - $body[] = phutil_tag( - 'div', - array( - 'class' => 'phabricator-hovercard-body-item' - ), - $item); + $item = array( + phutil_tag('strong', array(), $field['label']), + ' ', + phutil_tag('span', array(), $field['value']), + ); + $body[] = phutil_tag_div('phabricator-hovercard-body-item', $item); } if ($handle->getImageURI()) { @@ -147,40 +140,22 @@ final class PhabricatorHovercardView extends AphrontView { $tail = null; if ($buttons) { - $tail = phutil_tag('div', - array('class' => 'phabricator-hovercard-tail'), - $buttons); + $tail = phutil_tag_div('phabricator-hovercard-tail', $buttons); } // Assemble container // TODO: Add color support - $content = hsprintf( - '%s%s%s', - phutil_tag('div', - array( - 'class' => 'phabricator-hovercard-head' - ), - $header), - phutil_tag('div', - array( - 'class' => 'phabricator-hovercard-body' - ), - $body), - $tail); - - $hovercard = phutil_tag("div", + $hovercard = phutil_tag_div( + 'phabricator-hovercard-container', array( - "class" => "phabricator-hovercard-container", - ), - $content); + phutil_tag_div('phabricator-hovercard-head', $header), + phutil_tag_div('phabricator-hovercard-body', $body), + $tail, + )); // Wrap for thick border // and later the tip at the bottom - return phutil_tag('div', - array( - 'class' => 'phabricator-hovercard-wrapper', - ), - $hovercard); + return phutil_tag_div('phabricator-hovercard-wrapper', $hovercard); } } diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index 79b2131def..47b902ee84 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -198,9 +198,54 @@ final class PhabricatorStartup { /** + * Fatal the request completely in response to an exception, sending a plain + * text message to the client. Calls @{method:didFatal} internally. + * + * @param string Brief description of the exception context, like + * `"Rendering Exception"`. + * @param Exception The exception itself. + * @param bool True if it's okay to show the exception's stack trace + * to the user. The trace will always be logged. + * @return exit This method **does not return**. + * * @task apocalypse */ - public static function didFatal($message) { + public static function didEncounterFatalException( + $note, + Exception $ex, + $show_trace) { + + $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage(); + + $full_message = $message; + $full_message .= "\n\n"; + $full_message .= $ex->getTraceAsString(); + + if ($show_trace) { + $message = $full_message; + } + + self::didFatal($message, $full_message); + } + + + /** + * Fatal the request completely, sending a plain text message to the client. + * + * @param string Plain text message to send to the client. + * @param string Plain text message to send to the error log. If not + * provided, the client message is used. You can pass a more + * detailed message here (e.g., with stack traces) to avoid + * showing it to users. + * @return exit This method **does not return**. + * + * @task apocalypse + */ + public static function didFatal($message, $log_message = null) { + if ($log_message === null) { + $log_message = $message; + } + self::endOutputCapture(); $access_log = self::getGlobal('log.access'); @@ -219,7 +264,7 @@ final class PhabricatorStartup { $replace = true, $http_error = 500); - error_log($message); + error_log($log_message); echo $message; exit(1); @@ -235,6 +280,11 @@ final class PhabricatorStartup { private static function setupPHP() { error_reporting(E_ALL | E_STRICT); ini_set('memory_limit', -1); + + // If we have libxml, disable the incredibly dangerous entity loader. + if (function_exists('libxml_disable_entity_loader')) { + libxml_disable_entity_loader(true); + } } /** diff --git a/webroot/index.php b/webroot/index.php index 92a41bce8e..f880a77b90 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -3,10 +3,13 @@ require_once dirname(dirname(__FILE__)).'/support/PhabricatorStartup.php'; PhabricatorStartup::didStartup(); +$show_unexpected_traces = false; try { PhabricatorStartup::loadCoreLibraries(); PhabricatorEnv::initializeWebEnvironment(); + $show_unexpected_traces = PhabricatorEnv::getEnvConfig( + 'phabricator.developer-mode'); // This is the earliest we can get away with this, we need env config first. PhabricatorAccessLog::init(); @@ -99,10 +102,15 @@ try { phlog($unexpected_output); if ($response instanceof AphrontWebpageResponse) { - echo hsprintf( - '
    %s
    ', + echo phutil_tag( + 'div', + array('style' => + 'background: #eeddff;'. + 'white-space: pre-wrap;'. + 'z-index: 200000;'. + 'position: relative;'. + 'padding: 8px;'. + 'font-family: monospace'), $unexpected_output); } } @@ -119,7 +127,10 @@ try { $ex, )); } - PhabricatorStartup::didFatal('[Rendering Exception] '.$ex->getMessage()); + PhabricatorStartup::didEncounterFatalException( + 'Rendering Exception', + $ex, + $show_unexpected_traces); } $write_guard->dispose(); @@ -132,6 +143,9 @@ try { DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); } catch (Exception $ex) { - PhabricatorStartup::didFatal("[Exception] ".$ex->getMessage()); + PhabricatorStartup::didEncounterFatalException( + 'Core Exception', + $ex, + $show_unexpected_traces); } diff --git a/webroot/rsrc/css/aphront/context-bar.css b/webroot/rsrc/css/aphront/context-bar.css index 374e19b740..b130f3d700 100644 --- a/webroot/rsrc/css/aphront/context-bar.css +++ b/webroot/rsrc/css/aphront/context-bar.css @@ -8,6 +8,10 @@ border-bottom: 1px solid {$lightblueborder}; } +.device-phone .aphront-contextbar-view { + padding: 12px 8px; +} + .aphront-contextbar-content { padding-top: 5px; font-size: 13px; @@ -18,11 +22,24 @@ float: right; } +.device-phone .aphront-contextbar-buttons { + float: none; +} + .aphront-contextbar-buttons label { font-weight: bold; color: {$bluetext}; } -.aphront-contextbar-buttons a { - margin: 0 0 0 .5em; +.device-phone .aphront-contextbar-buttons label { + display: block; + margin-bottom: 4px; +} + +.aphront-contextbar-buttons a { + margin: 0 0 0 8px; +} + +.device-phone .aphront-contextbar-buttons a { + margin: 0 8px 0 0; } diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 0462c83017..ea77b9852c 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -124,3 +124,7 @@ .aphront-capability-details { margin: 20px 0 4px; } + +.aphront-dialog-view-paragraph + .aphront-dialog-view-paragraph { + margin-top: 16px; +} diff --git a/webroot/rsrc/css/aphront/error-view.css b/webroot/rsrc/css/aphront/error-view.css index 48cbc8bcdf..f6568c75d5 100644 --- a/webroot/rsrc/css/aphront/error-view.css +++ b/webroot/rsrc/css/aphront/error-view.css @@ -9,20 +9,24 @@ } form.aphront-dialog-view .aphront-error-view { - margin: 8px 0; + margin: 0 0 12px 0; } .aphront-error-view { margin: 16px; } .aphront-error-view-dialogue { - margin: 0px 0px 16px 0px; + margin: 0 0 16px 0; } .device-phone .aphront-error-view { margin: 8px; } +.aphront-error-view .phui-form-view { + padding: 0; +} + .aphront-error-view-body { padding: 12px; } @@ -33,8 +37,9 @@ form.aphront-dialog-view .aphront-error-view { } h1.aphront-error-view-head { - padding: 10px 10px 0 10px; + padding: 12px 8px 0 12px; font-weight: bold; + font-size: 15px; color: {$darkgreytext}; } diff --git a/webroot/rsrc/css/aphront/list-filter-view.css b/webroot/rsrc/css/aphront/list-filter-view.css index ddec174473..6d432cda57 100644 --- a/webroot/rsrc/css/aphront/list-filter-view.css +++ b/webroot/rsrc/css/aphront/list-filter-view.css @@ -3,10 +3,10 @@ */ .aphront-list-filter-wrap { - border-left: 1px solid {$lightgreyborder}; - border-right: 1px solid {$lightgreyborder}; - border-bottom: 1px solid {$greyborder}; - margin: 0 20px; + border-left: 1px solid {$lightblueborder}; + border-right: 1px solid {$lightblueborder}; + border-bottom: 1px solid {$blueborder}; + margin: 0 16px; } .aphront-list-filter-view { @@ -24,6 +24,14 @@ padding: 12px 0 6px; } +/* When a list filter view contains two consecuitive forms, lay them out + without much white space in between them so they look more contiugous. At + the time of writing, this is used only in the Diffusion repository search + UI. */ +.aphront-list-filter-view-content form + form .phui-form-view { + margin-top: -18px; +} + .aphront-list-filter-view-content .phui-form-view .aphront-form-label { width: 12%; } diff --git a/webroot/rsrc/css/aphront/pager-view.css b/webroot/rsrc/css/aphront/pager-view.css index 40f06fe72b..e63fbf7f8e 100644 --- a/webroot/rsrc/css/aphront/pager-view.css +++ b/webroot/rsrc/css/aphront/pager-view.css @@ -14,9 +14,26 @@ } .aphront-pager-view a { - padding: 2px 3px; + padding: 3px 7px; + border: 1px solid {$lightblueborder}; + border-bottom: 1px solid {$blueborder}; + margin: 0 2px; + background: #fff; + color: {$lightbluetext}; + display: inline-block; + border-radius: 2px; +} + +.aphront-pager-view a:hover { + text-decoration: none; + border: 1px solid {$greyborder}; + border-bottom: 1px solid {$darkgreyborder}; + color: {$darkgreytext}; } .aphront-pager-view a.current { - border-bottom: 2px solid; + background: {$greybackground}; + border: 1px solid {$greyborder}; + border-bottom: 1px solid {$darkgreyborder}; + color: {$darkgreytext}; } diff --git a/webroot/rsrc/css/aphront/panel-view.css b/webroot/rsrc/css/aphront/panel-view.css index 10e737645a..987751fbd7 100644 --- a/webroot/rsrc/css/aphront/panel-view.css +++ b/webroot/rsrc/css/aphront/panel-view.css @@ -7,12 +7,12 @@ background: {$lightgreybackground}; border: 1px solid #c0c0c0; border-width: 1px 0 0; - padding: 1em 2em; - margin: 20px 20px 40px 20px; + padding: 8px 16px; + margin: 20px 16px 32px 16px; } .device-phone .aphront-panel-view { - margin: 20px 10px 40px 10px; + margin: 20px 8px 40px 8px; } .aphront-unpadded-panel-view { @@ -32,20 +32,29 @@ .aphront-panel-view h1 { font-size: 16px; font-weight: bold; - color: {$darkgreytext}; + color: {$darkbluetext}; text-shadow: 0 1px 1px #fff; padding: 4px 0 0 0; } +.aphront-panel-view h1 a { + color: {$darkbluetext}; + text-decoration: none; +} + +.aphront-panel-view h1 a:hover { + text-decoration: underline; +} + .aphront-panel-plain h1 { - padding: 0 0 4px 0; + padding: 0; } .aphront-panel-view-caption { font-size: 11px; color: {$greytext}; - margin-top: -0.1em; - margin-bottom: 0.75em; + margin-top: 4px; + margin-bottom: 4px; } .aphront-panel-view-buttons { @@ -113,7 +122,7 @@ .aphront-mini-panel-view { background: #fff; padding: 10px; - margin: 5px 20px; + margin: 4px 16px; border: 1px solid {$lightgreyborder}; border-bottom: 1px solid {$greyborder}; } diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 5f520df2fb..32cde749ea 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -11,32 +11,33 @@ width: 100%; border-collapse: collapse; background: #fff; - border: 1px solid #d5dae0; + border: 1px solid {$lightblueborder}; + border-bottom: 1px solid {$blueborder}; } .aphront-table-view tr.alt { - background: #f1f3f8; + background: {$lightgreybackground}; } .aphront-table-view th { font-weight: bold; + font-size: 13px; white-space: nowrap; - color: #525252; + color: {$bluetext}; text-shadow: 0 1px 0 white; font-weight: bold; - background-color: #d9dde2; - background-image: url(/rsrc/image/texture/table_header.png); - background-repeat: repeat-x; + border-bottom: 1px solid {$thinblueborder}; + background-color: {$lightbluebackground}; } th.aphront-table-view-sortable-selected { - background-image: url(/rsrc/image/texture/table_header_hover.png); + background-color: {$greybackground}; } .aphront-table-view th a, .aphront-table-view th a:hover, .aphront-table-view th a:link { - color: #525252; + color: {$bluetext}; text-shadow: 0 1px 0 white; display: block; text-decoration: none; @@ -44,13 +45,14 @@ th.aphront-table-view-sortable-selected { .aphront-table-view th a:hover { text-decoration: underline; + color: {$darkbluetext}; } .aphront-table-view td.header { padding: 4px 8px; white-space: nowrap; text-align: right; - color: #525252; + color: {$bluetext}; font-weight: bold; } @@ -65,7 +67,7 @@ th.aphront-table-view-sortable-selected { width: 0; height: 0; vertical-align: top; - border-top: 4px solid black; + border-top: 4px solid {$bluetext}; border-right: 4px solid transparent; border-left: 4px solid transparent; content: ""; @@ -77,7 +79,7 @@ th.aphront-table-view-sortable-selected { width: 0; height: 0; vertical-align: top; - border-bottom: 4px solid black; + border-bottom: 4px solid {$bluetext}; border-right: 4px solid transparent; border-left: 4px solid transparent; content: ""; @@ -94,7 +96,7 @@ th.aphront-table-view-sortable-selected { .aphront-table-view th { padding: 8px 10px; - font-size: 12px; + font-size: 13px; } .aphront-table-view td { @@ -124,11 +126,11 @@ th.aphront-table-view-sortable-selected { } .aphront-table-view td.sorted-column { - background: #f1f3f8; + background: {$lightbluebackground}; } .aphront-table-view tr.alt td.sorted-column { - background: #e4e6eb; + background: {$greybackground}; } .aphront-table-view td.action { @@ -175,7 +177,7 @@ th.aphront-table-view-sortable-selected { } .aphront-table-view td.narrow { - width: 1px; + width: 1px; } div.single-display-line-bounds { @@ -189,6 +191,11 @@ span.single-display-line-content { position: absolute; } +.device-phone span.single-display-line-content { + white-space: nowrap; + position: static; +} + .aphront-table-view tr.highlighted { background: #fcf8e2; } @@ -198,9 +205,9 @@ span.single-display-line-content { } .aphront-table-view tr.no-data td { - padding: 1em; + padding: 12px; text-align: center; - color: {$lightgreytext}; + color: {$lightgreytext}; font-style: italic; } @@ -247,3 +254,7 @@ span.single-display-line-content { padding: 6px 8px; font-weight: bold; } + +.phui-object-box .aphront-table-view { + border: none; +} diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css index cf73e30682..219c8795a3 100644 --- a/webroot/rsrc/css/application/auth/auth.css +++ b/webroot/rsrc/css/application/auth/auth.css @@ -24,13 +24,13 @@ } .auth-account-view { - border: 1px solid #aaaaaa; + background-color: #fff; + border: 1px solid {$lightblueborder}; background-repeat: no-repeat; background-position: 4px 4px; padding: 4px 4px 4px 62px; min-height: 50px; border-radius: 2px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); } .auth-account-view-name { diff --git a/webroot/rsrc/css/application/base/main-menu-view.css b/webroot/rsrc/css/application/base/main-menu-view.css index fb52d762ac..476a963bf9 100644 --- a/webroot/rsrc/css/application/base/main-menu-view.css +++ b/webroot/rsrc/css/application/base/main-menu-view.css @@ -12,7 +12,6 @@ .phabricator-main-menu { background: #2d3236; - background-image: url(/rsrc/image/texture/phlnx-bg.png); background-repeat: repeat-x; position: relative; box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25); @@ -21,6 +20,7 @@ .device-desktop .phabricator-main-menu { height: 44px; + padding-right: 4px; } .phabricator-main-menu a:hover { @@ -49,11 +49,7 @@ padding-left: 6px; } -.device-desktop .phabricator-main-menu-logo:hover { - background-color: rgba(0,0,0,.6); -} - -.phabricator-main-menu-logo-image { +.menu-logo-image { position: absolute; width: 124px; height: 26px; @@ -146,13 +142,18 @@ font-size: 13px; border: 1px solid #333; border-radius: 12px; - background-color: #555; + background-color: #222; + opacity: .8; height: 26px; line-height: 12px; box-shadow: 0px 1px 1px rgba(128, 128, 128, 0.25); padding: 6px 32px 6px 10px; } +.phabricator-main-menu.main-header-dark .phabricator-main-menu-search input { + background-color: #555; +} + .device .phabricator-main-menu-search input { height: 30px; font-size: 15px; @@ -161,6 +162,7 @@ .phabricator-main-menu .phabricator-main-menu-search input:focus { background: #fff; + opacity: 1; color: #333; border-color: #e7e7e7; box-shadow: none; @@ -169,13 +171,13 @@ } .phabricator-main-menu-search input.jx-typeahead-placeholder { - color: {$lightgreytext}; + color: #fff; } .phabricator-main-menu-search button { position: absolute; color: transparent; - background: transparent 5px 6px url(/rsrc/image/search.png) no-repeat; + background: transparent 5px 6px url(/rsrc/image/search-white.png) no-repeat; border: none; outline: none; box-shadow: none; @@ -353,21 +355,31 @@ display: block; } -.device-desktop .phabricator-application-menu - .core-menu-item.phui-list-item-view:hover { +.device-desktop .main-header-dark .phabricator-application-menu + .core-menu-item.phui-list-item-view:hover, +.device-desktop .main-header-dark .phabricator-main-menu-logo:hover { background-color: rgba(0,0,0,.6); } +.device-desktop .phabricator-application-menu + .core-menu-item.phui-list-item-view:hover, +.device-desktop .phabricator-main-menu-logo:hover { + background-color: rgba(0,0,0,.3); + box-shadow: inset 0 5px 10px rgba(0,0,0,0.5); + border-radius: 3px; +} + .device-desktop .phabricator-application-menu .phui-list-item-view { float: left; position: relative; - min-width: 40px; - height: 44px; + min-width: 36px; + height: 36px; + margin-top: 4px; } .device-desktop .phabricator-core-menu-icon { - top: 9px; - left: 6px; + top: 5px; + left: 4px; } .device .phabricator-core-menu-icon { @@ -381,6 +393,17 @@ height: 28px; } +.phabricator-main-menu-dropdown { + position: absolute; + background: #fff; + top: 38px; + padding: 2px; + border: 1px solid {$lightblueborder}; + border-bottom: 1px solid {$blueborder}; + box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.3); +} + + /* - Application Menu ---------------------------------------------------------- Styles unique to the application menu (right button on mobile). diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css index 7f4e4e858a..fe3f77ad62 100644 --- a/webroot/rsrc/css/application/base/notification-menu.css +++ b/webroot/rsrc/css/application/base/notification-menu.css @@ -13,6 +13,11 @@ border-radius: 3px; } +.phabricator-notification .phabricator-notification-date { + margin-left: 8px; + color: {$lightgreytext}; +} + .phabricator-notification-menu-loading { text-align: center; padding: 10px 0; @@ -38,6 +43,9 @@ .phabricator-notification { padding: 6px; +} + +.phabricator-notification-menu .phabricator-notification { cursor: pointer; } @@ -53,10 +61,6 @@ color: {$lightgreytext}; } -.phabricator-notification-list { - font-size: 11px; -} - .phabricator-notification-list .phabricator-notification-unread, .phabricator-notification-menu .phabricator-notification-unread { background: #eceff5; diff --git a/webroot/rsrc/css/application/base/phabricator-application-launch-view.css b/webroot/rsrc/css/application/base/phabricator-application-launch-view.css index 9f5899c42c..54952c2e3d 100644 --- a/webroot/rsrc/css/application/base/phabricator-application-launch-view.css +++ b/webroot/rsrc/css/application/base/phabricator-application-launch-view.css @@ -51,28 +51,6 @@ a.application-tile-full.application-tile-create { border-right: none; } -.application-tile-full + .phabricator-application-launch-create { - float: right; - width: 12%; - height: 52px; - position: relative; - border-top: 1px solid #000; - border-bottom: none; - display: inline-block; - text-align: center; -} - -.device-desktop a.phabricator-application-launch-create:hover { - background-color: #1e2225; - color: #fff; - text-decoration: none; -} - -.device-desktop a.phabricator-application-launch-container:hover { - background-color: #1e2225; - text-decoration: none; -} - .phabricator-application-launch-icon { display: block; position: absolute; @@ -82,18 +60,16 @@ a.application-tile-full.application-tile-create { height: 28px; } -.phabricator-application-create-icon { - display: inline-block; - margin: 20px auto; - width: 14px; - height: 14px; -} - .application-tile-full .phabricator-application-launch-icon { top: 12px; left: 10px; } +.device-desktop a.phabricator-application-launch-container:hover { + background-color: #1e2225; + text-decoration: none; +} + .phabricator-application-launch-name { display: block; margin-top: 42px; @@ -175,20 +151,3 @@ a.application-tile-full.application-tile-create { top: 16px; right: 8px; } - -.application-tile-full .phabricator-application-launch-create { - display: inline-block; - float: right; - background: rgb(0, 122, 255); - border-radius: 5px; - color: white; - font-weight: bold; - padding: 1px 6px 2px; - border: 2px solid #333; - font-size: 11px; - box-shadow: 0 0px 2px #000; -} - -.phabricator-application-launch-create { - display: none; -} diff --git a/webroot/rsrc/css/application/config/setup-issue.css b/webroot/rsrc/css/application/config/setup-issue.css index 822d28e4d2..23b1248c34 100644 --- a/webroot/rsrc/css/application/config/setup-issue.css +++ b/webroot/rsrc/css/application/config/setup-issue.css @@ -4,24 +4,22 @@ .setup-issue-background { - background-color: #edecef; - padding: 1em 0; + padding: 12px 0; } .setup-issue-shell { max-width: 760px; - margin: 15px auto; + margin: 16px auto; } - .setup-issue { - border: 1px solid #35393d; - background: #ffffff; - box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); + background: #fff; + border: 1px solid #BFCFDA; + border-bottom: 1px solid #8C98B8; } .setup-issue p { - margin: 1em 0; + margin: 12px 0; } .setup-issue table { @@ -54,7 +52,7 @@ } .setup-issue tt { - color: {$greytext}; + color: #74777D; } .setup-issue em { @@ -62,36 +60,45 @@ } .setup-issue-instructions { - font-size: 15px; - padding: 20px; - line-height: 1.4em; - background: #efefef; - border-bottom: 1px solid #bfbfbf; + font-size: 14px; + padding: 12px 0; + line-height: 20px; + background: #fff; + border-bottom: 1px solid #BFCFDA; + margin: 0 12px; } .setup-fatal .setup-issue-instructions { - background: #ffdfdf; + color: #c0392b; + background: #f4dddb; + padding: 12px; + margin: 0 0 12px; + border-bottom: 1px solid #c0392b; } .setup-issue-name { - padding: 15px; - background: #35393d; - color: #ffffff; + color: #6B748C; + text-shadow: 0 1px #fff; + padding: 16px 12px; + border-bottom: 1px solid #BFCFDA; font-size: 15px; font-weight: bold; + background-image: url(/rsrc/image/lightblue-header.png); + background-repeat: repeat-x; } .setup-issue-next { - padding: 15px; - background: #35393d; + padding: 12px; + border-top: 1px solid #BFCFDA; + background: #daeaf3; text-align: center; - font-size: 16px; - color: #ffffff; + font-size: 15px; + color: #2980b9; } .setup-issue-config { - margin: 15px 0; - padding: 0 20px; + padding: 0 12px; + background: #fff; } .setup-issue ul { @@ -104,5 +111,5 @@ margin-top: 8px; padding: 4px; text-align: right; - color: {$greytext}; + color: #74777D; } diff --git a/webroot/rsrc/css/application/conpherence/menu.css b/webroot/rsrc/css/application/conpherence/menu.css index 08a5700332..9a3b858e45 100644 --- a/webroot/rsrc/css/application/conpherence/menu.css +++ b/webroot/rsrc/css/application/conpherence/menu.css @@ -40,13 +40,16 @@ top: 0; bottom: 0; } + .device-desktop .conpherence-layout .conpherence-menu-pane, .device-desktop .conpherence-layout .phabricator-nav-column-background { width: 280px; } + .device .conpherence-menu-pane { top: 41px; } + .device .conpherence-role-list .conpherence-menu-pane { top: 0px; } diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index 2910805fff..6f7d633d3b 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -135,6 +135,9 @@ margin: 5px 15px; min-height: auto; } +.device-phone .conpherence-message-pane .date-marker { + margin: 5px 0; +} .conpherence-message-pane .date-marker .date { position: relative; top: -8px; diff --git a/webroot/rsrc/css/application/conpherence/widget-pane.css b/webroot/rsrc/css/application/conpherence/widget-pane.css index 7a2e2d818d..c39e97a94c 100644 --- a/webroot/rsrc/css/application/conpherence/widget-pane.css +++ b/webroot/rsrc/css/application/conpherence/widget-pane.css @@ -7,10 +7,10 @@ position: fixed; right: 0px; top: 76px; - bottom: 0px; + bottom: 0; width: 240px; border-width: 0 0 0 1px; - border-color: #CCC; + border-color: {$lightblueborder}; border-style: solid; overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -70,9 +70,9 @@ .conpherence-widget-pane .widgets-body { position: fixed; overflow-y: auto; - bottom: 0px; - width: 100%; + bottom: 0; top: 76px; + width: 100%; } #widgets-settings { diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 4cc51529dc..902d3ae16e 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -6,10 +6,11 @@ position: relative; margin: 0; padding-top: 32px; + overflow-x: auto; } .differential-diff { - background: transparent; + background: #fff; width: 100%; border-top: 1px solid {$lightblueborder}; border-bottom: 1px solid {$lightblueborder}; @@ -286,10 +287,9 @@ td.cov-X { font-style: normal; } - -.differential-inline-comment-edit-textarea { +.differential-inline-comment-edit-body .aphront-form-input { + margin: 0; width: 100%; - height: 12em; } .differential-changeset-buttons { diff --git a/webroot/rsrc/css/application/directory/phabricator-jump-nav.css b/webroot/rsrc/css/application/directory/phabricator-jump-nav.css index 0fb9e0d0a0..fe06c4c973 100644 --- a/webroot/rsrc/css/application/directory/phabricator-jump-nav.css +++ b/webroot/rsrc/css/application/directory/phabricator-jump-nav.css @@ -15,12 +15,11 @@ input.phabricator-jump-nav[type='text'] { .phabricator-jump-nav-caption { margin-top: 4px; - padding-bottom: 4px; font-size: 11px; color: {$greytext}; text-align: left; } .phabricator-jump-nav-container form { - padding: 20px; + padding: 12px 16px; } diff --git a/webroot/rsrc/css/application/feed/feed.css b/webroot/rsrc/css/application/feed/feed.css index fbda2c777d..04711eb982 100644 --- a/webroot/rsrc/css/application/feed/feed.css +++ b/webroot/rsrc/css/application/feed/feed.css @@ -19,11 +19,15 @@ .device-desktop .phabricator-feed-frame { max-width: 600px; - margin: 20px; + margin: 16px; +} + +.phabricator-feed-frame .phabricator-action-header { + padding: 0; } .phabricator-feed-story-date-separator { - margin-top: 2em; + margin-top: 16px; } .phabricator-feed-newer-link { diff --git a/webroot/rsrc/css/application/legalpad/legalpad-document.css b/webroot/rsrc/css/application/legalpad/legalpad-document.css new file mode 100644 index 0000000000..3b2f32a32c --- /dev/null +++ b/webroot/rsrc/css/application/legalpad/legalpad-document.css @@ -0,0 +1,15 @@ +/** + * @provides legalpad-document-css + */ + +.legalpad .phabricator-remarkup .remarkup-note { + border-radius: 15px; +} + +.legalpad .phabricator-remarkup .remarkup-warning { + border-radius: 15px; +} + +.legalpad .phabricator-remarkup .remarkup-important { + border-radius: 15px; +} diff --git a/webroot/rsrc/css/application/maniphest/task-summary.css b/webroot/rsrc/css/application/maniphest/task-summary.css index 5f984cae26..8b6b57942b 100644 --- a/webroot/rsrc/css/application/maniphest/task-summary.css +++ b/webroot/rsrc/css/application/maniphest/task-summary.css @@ -3,7 +3,7 @@ */ .maniphest-task-group { - padding-bottom: 30px; + padding-bottom: 24px; } .maniphest-batch-selected td { @@ -75,9 +75,9 @@ } .maniphest-list-container { - padding: 20px; + padding: 16px; } .device-phone .maniphest-list-container { - padding: 20px 5px; + padding: 20px 4px; } diff --git a/webroot/rsrc/css/application/objectselector/object-selector.css b/webroot/rsrc/css/application/objectselector/object-selector.css index 988aa26b63..0abae7eb5b 100644 --- a/webroot/rsrc/css/application/objectselector/object-selector.css +++ b/webroot/rsrc/css/application/objectselector/object-selector.css @@ -3,8 +3,8 @@ * @requires aphront-dialog-view-css */ -.phabricator-object-selector-dialog { - width: 960px; +.device-desktop .phabricator-object-selector-dialog { + width: 860px; } .phabricator-object-selector-dialog .aphront-dialog-body { @@ -13,7 +13,8 @@ .phabricator-object-selector-search { width: 100%; - background: #ededed; + background: {$lightbluebackground}; + border-bottom: 1px solid {$thinblueborder}; } .phabricator-object-selector-search td { @@ -25,27 +26,42 @@ td.phabricator-object-selector-search-text { width: 100%; } +.phabricator-object-selector-row:hover { + background-color: {$greybackground}; +} + +.phabricator-object-selector-row:hover a { + text-decoration: none; +} + .phabricator-object-selector-search-text input { width: 100%; + border-radius: 14px; + padding: 4px 8px; + height: 26px; + margin: 2px 0; +} + +.phabricator-object-selector-popicon { + display: inline-block; + margin-top: 4px; } .phabricator-object-selector-results { position: relative; height: 24em; - border: solid #bbbbbb; - border-width: 1px 0px; overflow-y: scroll; overflow-x: hidden; } .phabricator-object-selector-handle { width: 100%; - background: #e9e9e9; + background: {$lightgreybackground}; margin-bottom: 1px; } .phabricator-object-selector-handle td { - padding: 4px 1em; + padding: 4px 8px; } .phabricator-object-selector-handle th { @@ -57,14 +73,14 @@ td.phabricator-object-selector-search-text { .phabricator-object-selector-handle th a { display: block; - padding: 4px 1em; + padding: 4px 0; } .phabricator-object-selector-header { - padding: 2px; - border-bottom: 1px solid #d0d0d0; - margin-bottom: 16px; - color: {$darkgreytext}; + margin-bottom: 8px; + font-weight: bold; + text-transform: uppercase; + color: {$bluetext}; } .phabricator-object-selector-attach-explicit { @@ -74,17 +90,13 @@ td.phabricator-object-selector-search-text { border-width: 1px 0px; } -.phabricator-object-selector-currently-attached { - background: #fff; - padding: 16px; - border: 1px solid #dddddd; -} - .phabricator-object-selector-current { - background: #ededed; - padding: 8px 8px; + border-top: 1px solid {$thinblueborder}; } +.phabricator-object-selector-currently-attached { + padding: 8px; +} .object-selector-nothing { padding: 1em; diff --git a/webroot/rsrc/css/application/profile/profile-view.css b/webroot/rsrc/css/application/profile/profile-view.css index 9b1665f08a..162aa14ca5 100644 --- a/webroot/rsrc/css/application/profile/profile-view.css +++ b/webroot/rsrc/css/application/profile/profile-view.css @@ -2,127 +2,17 @@ * @provides phabricator-profile-css */ -table.phabricator-profile-master-layout { - width: 100%; -} - -td.phabricator-profile-navigation { - width: 300px; - background: #efefef; - border-right: 1px solid #cccccc; - padding-top: 8px; - padding-bottom: 8em; -} - -td.phabricator-profile-navigation a, -td.phabricator-profile-navigation span { - display: block; - margin: 0 0 2px; - min-width: 150px; - font-weight: bold; - white-space: nowrap; - text-decoration: none; -} - -td.phabricator-profile-navigation a { - padding: 4px 8px 4px 10px; -} - -td.phabricator-profile-navigation a:hover { - text-decoration: none; - background: #cccccc; -} - -td.phabricator-profile-navigation a.phabricator-profile-item-selected, -td.phabricator-profile-navigation a.phabricator-profile-item-selected :hover { - background: #cccccc; -} - -td.phabricator-profile-navigation hr { - border: none; - background: #cccccc; - padding: 0; - margin: 10px 0; - height: 1px; -} - -td.phabricator-profile-navigation h1, -td.phabricator-profile-navigation h2 { - padding: 2px 0px 0px 10px; -} - -td.phabricator-profile-content { - padding: 2em 2%; -} - -.phabricator-profile-info-table th { - font-weight: bold; - text-align: right; - color: {$greytext}; - width: 10%; - padding: 4px; -} - -.phabricator-profile-info-table td { - width: 100%; - padding: 4px; -} - -.phabricator-profile-info-group { - box-shadow: 0 1px 1px rgba(0,0,0,.4); -} - -.phabricator-profile-info-header { - padding: 8px; - background: #e7e7e7; - border-bottom: 1px solid #d9d9d9; -} - -.phabricator-profile-info-pane { - padding: 8px .5em; - background: #fff; -} - -.phabricator-profile-info-table { - width: 100%; -} - -h2.phabricator-profile-realname { - color: {$greytext}; -} - -img.phabricator-profile-image { - width: 280px; - margin: 10px; -} - -.phabricator-profile-info-pane-more-link { - text-align: right; - padding: .25em; - font-weight: bold; - margin: .5em 1em 0; -} - -.profile-wrap-responsive { - margin: 20px; -} - -.device .profile-wrap-responsive { - margin-left: 10px; - margin-right: 10px; -} - -.device-desktop .profile-feed { +.device-desktop .profile-feed, +.device-tablet .profile-feed { max-width: 640px; + padding: 12px 16px; +} + +.device-phone .profile-feed { + padding: 12px 0; } .profile-feed .phabricator-action-header-title { font-size: 16px; margin-bottom: 5px; } - -.device-desktop .phabricator-project-layout - .aphront-multi-column-column-outer:first-child - .phabricator-profile-info-group { - margin-right: 0; -} diff --git a/webroot/rsrc/css/application/settings/settings.css b/webroot/rsrc/css/application/settings/settings.css index 6f71309826..28912b68bf 100644 --- a/webroot/rsrc/css/application/settings/settings.css +++ b/webroot/rsrc/css/application/settings/settings.css @@ -2,19 +2,24 @@ * @provides phabricator-settings-css */ -.phabricator-settings-homepagetable { - width: 60% !important; - margin: auto; -} - -.phabricator-settings-homepagetable td.fixed { - width: 24px; - text-align: center; - -} - -.phabricator-settings-panelview h1 { +.phabricator-settings-homepagetable .fixed { + width: 48px; text-align: center; } +.phabricator-settings-homepagetable td em { + color: {$lightgreytext}; +} +.phabricator-settings-homepagetable-button .aphront-form-input { + margin: 0; + width: auto; +} + +.phabricator-settings-homepagetable-button .aphront-form-control { + padding: 0; +} + +.phabricator-settings-homepagetable-wrap .phui-form-view { + padding: 0; +} diff --git a/webroot/rsrc/css/core/core.css b/webroot/rsrc/css/core/core.css index 08e2c2367a..812577da55 100644 --- a/webroot/rsrc/css/core/core.css +++ b/webroot/rsrc/css/core/core.css @@ -109,7 +109,7 @@ img { height: 44px; border-radius: 5px; opacity: 0.4; - background: #000 url(/res/cb6fc6eb/rsrc/image/loading/compass_24.gif) center no-repeat; + background: #000 url(/rsrc/image/loading/compass_24.gif) center no-repeat; } .grouped:after { diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 8da19295bf..5e4f1add68 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -178,6 +178,20 @@ background: #f3f3ff; } +.phabricator-remarkup .remarkup-warning { + margin: 1em 1.5em; + padding: 0.5em 1em; + border: 1px solid {$yellow}; + background: {$lightyellow}; +} + +.phabricator-remarkup .remarkup-important { + margin: 1em 1.5em; + padding: 0.5em 1em; + border: 1px solid {$red}; + background: {$lightred}; +} + .phabricator-remarkup-toc { float: right; border: 1px solid #d7d7d7; @@ -402,6 +416,8 @@ div.phabricator-remarkup-dark tt { } div.phabricator-remarkup-dark .remarkup-note, +div.phabricator-remarkup-dark .remarkup-warning, +div.phabricator-remarkup-dark .remarkup-important, div.phabricator-remarkup-dark table { color: #000; } diff --git a/webroot/rsrc/css/layout/phabricator-action-header-view.css b/webroot/rsrc/css/layout/phabricator-action-header-view.css index 302843c581..fbc573b919 100644 --- a/webroot/rsrc/css/layout/phabricator-action-header-view.css +++ b/webroot/rsrc/css/layout/phabricator-action-header-view.css @@ -32,7 +32,7 @@ display: inline-block; } -.phabricator-action-header-icon-item .phabricator-tag-view { +.phabricator-action-header-icon-item .phui-tag-view { margin: 4px 2px 0; } @@ -52,7 +52,7 @@ text-shadow: 0 -1px 1px rgba(0,0,0,.7); } -.phabricator-action-header-icon-list .phabricator-tag-view { +.phabricator-action-header-icon-list .phui-tag-view { font-weight: normal; } diff --git a/webroot/rsrc/css/layout/phabricator-source-code-view.css b/webroot/rsrc/css/layout/phabricator-source-code-view.css index 952dfac163..48d7f7d095 100644 --- a/webroot/rsrc/css/layout/phabricator-source-code-view.css +++ b/webroot/rsrc/css/layout/phabricator-source-code-view.css @@ -8,6 +8,14 @@ overflow-y: hidden; } +.phabricator-source-code-view tr:first-child * { + padding-top: 8px; +} + +.phabricator-source-code-view tr:last-child * { + padding-bottom: 8px; +} + .phabricator-source-code { white-space: pre-wrap; padding: 2px 8px 1px; diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css index cc5aba5ed7..5bc2b8e3ab 100644 --- a/webroot/rsrc/css/phui/phui-button.css +++ b/webroot/rsrc/css/phui/phui-button.css @@ -70,6 +70,17 @@ a.grey:visited { text-shadow: none; } +button.simple, +input[type="submit"].simple, +a.simple, +a.simple:visited { + background-color: transparent; + background-image: none; + border: 1px solid transparent; + color: {$bluetext}; + text-shadow: 0 1px #fff; +} + a.disabled, button.disabled, button[disabled] { @@ -102,6 +113,16 @@ button:hover { box-shadow: inset 0 0 5px rgba(0,0,0,.4); } +a.button.simple:hover, +button.simple:hover { + background-color: #fff; + border: 1px solid {$lightgreyborder}; + background-image: none; + border-bottom: 1px solid {$greyborder}; + text-shadow: none; + box-shadow: none; +} + a.button.grey:hover, button.grey:hover { text-decoration: none; diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index 47d560182e..15edafdf35 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -4,7 +4,6 @@ .phui-form-view { padding: 16px; - background: #fff; } .phui-form-view.phui-form-full-width { @@ -411,8 +410,7 @@ table.aphront-form-control-checkbox-layout th { } .login-to-comment { - padding: 12px; - float: right; + margin: 12px; } .phui-form-divider hr { @@ -434,3 +432,21 @@ table.aphront-form-control-checkbox-layout th { padding: 16px 0 4px; margin-bottom: 4px; } + +.device-desktop .text-with-submit-control-outer-bounds { + position: relative; +} + +.device-desktop .text-with-submit-control-text-bounds { + position: absolute; + left: 0; + right: 184px; +} + +.device-desktop .text-with-submit-control-submit-bounds { + text-align: right; +} + +.device-desktop .text-with-submit-control-submit { + width: 180px; +} diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 5c62dfc6a7..e2959375c6 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -41,12 +41,26 @@ body .phui-header-shell.phui-bleed-header padding: 16px; font-size: 15px; color: {$darkbluetext}; + position: relative; } -.phui-header-view a { +.phui-header-view a, +.phui-header-view a.simple { color: {$darkbluetext}; } +.phui-header-view .phui-header-action-links { + position: absolute; + top: 5px; + right: 0; +} + +.device-phone .phui-header-action-link .phui-button-text { + visibility: hidden; + width: 0; + margin-left: 12px; +} + .phui-header-divider { margin: 0 4px; font-weight: normal; @@ -62,7 +76,7 @@ body.device-phone .phui-header-view { font-size: 13px; } -.phui-header-tags .phabricator-tag-view { +.phui-header-tags .phui-tag-view { margin-left: 4px; } diff --git a/webroot/rsrc/css/phui/phui-object-box.css b/webroot/rsrc/css/phui/phui-object-box.css index 5c10d975b7..ffad2db217 100644 --- a/webroot/rsrc/css/phui/phui-object-box.css +++ b/webroot/rsrc/css/phui/phui-object-box.css @@ -22,8 +22,10 @@ padding: 8px 0; } -.phui-object-box .aphront-error-view { - margin-bottom: 0; +.phui-object-box .phui-header-shell + .aphront-error-view { + margin: 0; + border-width: 0; + border-bottom: 1px solid {$lightblueborder}; } .device-phone .phui-object-box { diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css index ee309ab860..16d4b315f1 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -7,7 +7,7 @@ } .device-desktop .phui-object-item-list-view { - padding: 20px; + padding: 16px; } .phui-object-item-list-view + .phui-object-item-list-view { @@ -18,6 +18,16 @@ padding: 0; } +.phui-object-box .phui-object-item-list-view.phui-object-list-flush { + padding: 8px 12px 4px 12px; + background-color: #E5E8EE; +} + +.device-phone .phui-object-box + .phui-object-item-list-view.phui-object-list-flush { + padding: 4px; +} + .phui-object-item-list-view .aphront-error-view { margin: 0; } @@ -370,12 +380,18 @@ border-left-color: #d7d7d7; } +.phui-object-item-disabled .phui-object-item-link, +.phui-object-item-disabled .phui-object-item-link a { + color: {$lightgreytext}; +} + .phui-object-item-disabled .phui-object-item-frame { border-color: #d7d7d7; } .phui-object-item-disabled .phui-object-item-objname { color: {$greytext}; + text-decoration: line-through; } @@ -521,9 +537,10 @@ .drag-ghost { position: relative; - border: 1px dashed #aaaaaa; - background: #f9f9f9; - margin: 4px; + border: 1px dashed #fff; + background: rgba(255,255,255,.5); + margin-bottom: 4px; + border-radius: 3px; } .drag-dragging { @@ -555,4 +572,3 @@ padding-left: 0; padding-top: 0; } - diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css index a6be3d9631..49b531b4ca 100644 --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -137,7 +137,7 @@ /* When tags appear in property lists, give them a little more vertical spacing. */ -.phui-property-list-view .phabricator-tag-view { +.phui-property-list-view .phui-tag-view { margin: 2px 0; } diff --git a/webroot/rsrc/css/layout/phabricator-tag-view.css b/webroot/rsrc/css/phui/phui-tag-view.css similarity index 51% rename from webroot/rsrc/css/layout/phabricator-tag-view.css rename to webroot/rsrc/css/phui/phui-tag-view.css index 923c38419c..db028c8be6 100644 --- a/webroot/rsrc/css/layout/phabricator-tag-view.css +++ b/webroot/rsrc/css/phui/phui-tag-view.css @@ -1,135 +1,137 @@ /** - * @provides phabricator-tag-view-css + * @provides phui-tag-view-css */ -.phabricator-tag-view { +.phui-tag-view { font-weight: bold; text-decoration: none; display: inline-block; + position: relative; } -a.phabricator-tag-view:hover { +a.phui-tag-view:hover { text-decoration: none; } -.phabricator-tag-state-closed .phabricator-tag-core { +.phui-tag-state-closed .phui-tag-core { text-decoration: line-through; } -.phabricator-tag-core { +.phui-tag-core { color: inherit; border: 1px solid transparent; border-radius: 3px; padding: 0 4px; } -.phabricator-tag-view-has-bar .phabricator-tag-core { - border-radius: 0 3px 3px 0; - border-width: 1px 1px 1px 0; +.phui-tag-type-state .phui-tag-core { + padding: 1px 6px; } -.phabricator-tag-dot { +.phui-tag-view .phui-icon-view { + position: absolute; + display: inline-block; + top: 1px; + left: 5px; +} + +.phui-tag-icon-view .phui-tag-core { + padding-left: 22px; +} + +.phui-tag-dot { position: relative; display: inline-block; width: 6px; height: 6px; - margin-right: 3px; + margin-right: 4px; top: -1px; border-radius: 6px; border: 1px solid transparent; - box-shadow: 0 0 2px rgba(255, 255, 255, 0.8); } -.phabricator-tag-type-state { +.phui-tag-type-state { color: #ffffff; text-shadow: rgba(100, 100, 100, 0.40) 0px -1px 1px; } -.phabricator-tag-type-object, -a.phabricator-tag-type-object, -a.phabricator-tag-type-object:link { +.phui-tag-type-object, +a.phui-tag-type-object, +a.phui-tag-type-object:link { color: #000000; } -.phabricator-tag-type-person { +.phui-tag-type-person { white-space: nowrap; color: #19558d; } -.phabricator-tag-color-red { +.phui-tag-color-red { background-color: {$red}; border-color: {$red}; } -.phabricator-tag-color-orange { +.phui-tag-color-orange { background-color: {$orange}; border-color: {$orange}; } -.phabricator-tag-color-yellow { +.phui-tag-color-yellow { background-color: {$yellow}; border-color: {$yellow}; } -.phabricator-tag-color-blue { +.phui-tag-color-blue { background-color: {$blue}; border-color: {$blue}; } -.phabricator-tag-color-indigo { +.phui-tag-color-indigo { background-color: {$indigo}; border-color: {$indigo}; } -.phabricator-tag-color-green { +.phui-tag-color-green { background-color: {$green}; border-color: {$green}; } -.phabricator-tag-color-violet { +.phui-tag-color-violet { background-color: {$violet}; border-color: {$violet}; } -.phabricator-tag-color-black { +.phui-tag-color-black { background-color: #333333; border-color: #333333; } -.phabricator-tag-color-grey { +.phui-tag-color-grey { background-color: {$lightgreytext}; border-color: {$lightgreytext}; } -.phabricator-tag-color-white { +.phui-tag-color-white { background-color: #f7f7f7; border-color: #f7f7f7; } -.phabricator-tag-color-object { +.phui-tag-color-object { background-color: #e7e7e7; border-color: #e7e7e7; } -.phabricator-tag-color-person { +.phui-tag-color-person { background-color: #f1f7ff; border-color: #f1f7ff; } -a.phabricator-tag-view:hover - .phabricator-tag-core.phabricator-tag-color-person { +a.phui-tag-view:hover + .phui-tag-core.phui-tag-color-person { border-color: #d9ebfd; } -a.phabricator-tag-view:hover - .phabricator-tag-core.phabricator-tag-color-object { +a.phui-tag-view:hover + .phui-tag-core.phui-tag-color-object { border-color: #d7d7d7; } - -.phabricator-tag-bar { - padding-left: 4px; - border-width: 1px; - border-style: solid; - border-right-color: rgba(0, 0, 0, 0.33); - border-radius: 2px 0 0 2px; -} diff --git a/webroot/rsrc/css/phui/phui-workpanel-view.css b/webroot/rsrc/css/phui/phui-workpanel-view.css index 89e41ce1f0..160a73213d 100644 --- a/webroot/rsrc/css/phui/phui-workpanel-view.css +++ b/webroot/rsrc/css/phui/phui-workpanel-view.css @@ -56,7 +56,26 @@ width: 300px; } +.phui-workpanel-body .phui-object-item-list-view { + min-height: 54px; +} + .device .aphront-multi-column-outer div.aphront-multi-column-column-outer .phui-workpanel-body { width: auto; } + +.project-column-empty { + background: rgba(255,255,255,.4); + border-radius: 3px; + margin-bottom: 4px; + border: 1px dashed #fff; +} + +.project-column-empty .drag-ghost { + display: none; +} + +.project-column-empty.drag-target-list { + background: rgba(255,255,255,.7); +} diff --git a/webroot/rsrc/css/sprite-apps-large.css b/webroot/rsrc/css/sprite-apps-large.css index f2ccba2433..e65da3853e 100644 --- a/webroot/rsrc/css/sprite-apps-large.css +++ b/webroot/rsrc/css/sprite-apps-large.css @@ -13,7 +13,7 @@ only screen and (min-device-pixel-ratio: 1.5), only screen and (-webkit-min-device-pixel-ratio: 1.5) { .sprite-apps-large { background-image: url(/rsrc/image/sprite-apps-large-X2.png); - background-size: 435px 435px; + background-size: 464px 464px; } } @@ -79,185 +79,201 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { } .apps-authentication-white-large { - background-position: 0px -29px; + background-position: -435px 0px; } .apps-calendar-light-large { - background-position: -29px -29px; + background-position: 0px -29px; } .apps-calendar-dark-large { - background-position: -58px -29px; + background-position: -29px -29px; } .apps-calendar-blue-large, .phabricator-crumb-view:hover .apps-calendar-dark-large { - background-position: -87px -29px; + background-position: -58px -29px; } .apps-calendar-white-large { - background-position: -116px -29px; + background-position: -87px -29px; } .apps-chatlog-light-large { - background-position: -145px -29px; + background-position: -116px -29px; } .apps-chatlog-dark-large { - background-position: -174px -29px; + background-position: -145px -29px; } .apps-chatlog-blue-large, .phabricator-crumb-view:hover .apps-chatlog-dark-large { - background-position: -203px -29px; + background-position: -174px -29px; } .apps-chatlog-white-large { - background-position: -232px -29px; + background-position: -203px -29px; } .apps-conduit-light-large { - background-position: -261px -29px; + background-position: -232px -29px; } .apps-conduit-dark-large { - background-position: -290px -29px; + background-position: -261px -29px; } .apps-conduit-blue-large, .phabricator-crumb-view:hover .apps-conduit-dark-large { - background-position: -319px -29px; + background-position: -290px -29px; } .apps-conduit-white-large { - background-position: -348px -29px; + background-position: -319px -29px; } .apps-conpherence-light-large { - background-position: -377px -29px; + background-position: -348px -29px; } .apps-conpherence-dark-large { - background-position: -406px -29px; + background-position: -377px -29px; } .apps-conpherence-blue-large, .phabricator-crumb-view:hover .apps-conpherence-dark-large { - background-position: 0px -58px; + background-position: -406px -29px; } .apps-conpherence-white-large { - background-position: -29px -58px; + background-position: -435px -29px; } .apps-countdown-light-large { - background-position: -58px -58px; + background-position: 0px -58px; } .apps-countdown-dark-large { - background-position: -87px -58px; + background-position: -29px -58px; } .apps-countdown-blue-large, .phabricator-crumb-view:hover .apps-countdown-dark-large { - background-position: -116px -58px; + background-position: -58px -58px; } .apps-countdown-white-large { - background-position: -145px -58px; + background-position: -87px -58px; } .apps-daemon-light-large { - background-position: -174px -58px; + background-position: -116px -58px; } .apps-daemon-dark-large { - background-position: -203px -58px; + background-position: -145px -58px; } .apps-daemon-blue-large, .phabricator-crumb-view:hover .apps-daemon-dark-large { - background-position: -232px -58px; + background-position: -174px -58px; } .apps-daemon-white-large { - background-position: -261px -58px; + background-position: -203px -58px; } .apps-differential-light-large { - background-position: -290px -58px; + background-position: -232px -58px; } .apps-differential-dark-large { - background-position: -319px -58px; + background-position: -261px -58px; } .apps-differential-blue-large, .phabricator-crumb-view:hover .apps-differential-dark-large { - background-position: -348px -58px; + background-position: -290px -58px; } .apps-differential-white-large { - background-position: -377px -58px; + background-position: -319px -58px; } .apps-diffusion-light-large { - background-position: -406px -58px; + background-position: -348px -58px; } .apps-diffusion-dark-large { - background-position: 0px -87px; + background-position: -377px -58px; } .apps-diffusion-blue-large, .phabricator-crumb-view:hover .apps-diffusion-dark-large { - background-position: -29px -87px; + background-position: -406px -58px; } .apps-diffusion-white-large { - background-position: -58px -87px; + background-position: -435px -58px; } .apps-diviner-light-large { - background-position: -87px -87px; + background-position: 0px -87px; } .apps-diviner-dark-large { - background-position: -116px -87px; + background-position: -29px -87px; } .apps-diviner-blue-large, .phabricator-crumb-view:hover .apps-diviner-dark-large { - background-position: -145px -87px; + background-position: -58px -87px; } .apps-diviner-white-large { - background-position: -174px -87px; + background-position: -87px -87px; } .apps-drydock-light-large { - background-position: -203px -87px; + background-position: -116px -87px; } .apps-drydock-dark-large { - background-position: -232px -87px; + background-position: -145px -87px; } .apps-drydock-blue-large, .phabricator-crumb-view:hover .apps-drydock-dark-large { - background-position: -261px -87px; + background-position: -174px -87px; } .apps-drydock-white-large { - background-position: -290px -87px; + background-position: -203px -87px; } .apps-fact-light-large { - background-position: -319px -87px; + background-position: -232px -87px; } .apps-fact-dark-large { - background-position: -348px -87px; + background-position: -261px -87px; } .apps-fact-blue-large, .phabricator-crumb-view:hover .apps-fact-dark-large { - background-position: -377px -87px; + background-position: -290px -87px; } .apps-fact-white-large { + background-position: -319px -87px; +} + +.apps-fancyhome-light-large { + background-position: -348px -87px; +} + +.apps-fancyhome-dark-large { + background-position: -377px -87px; +} + +.apps-fancyhome-blue-large, .phabricator-crumb-view:hover .apps-fancyhome-dark-large { background-position: -406px -87px; } +.apps-fancyhome-white-large { + background-position: -435px -87px; +} + .apps-feed-light-large { background-position: 0px -116px; } @@ -319,597 +335,693 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { } .apps-harbormaster-white-large { - background-position: 0px -145px; + background-position: -435px -116px; } .apps-help-light-large { - background-position: -29px -145px; + background-position: 0px -145px; } .apps-help-dark-large { - background-position: -58px -145px; + background-position: -29px -145px; } .apps-help-blue-large, .phabricator-crumb-view:hover .apps-help-dark-large { - background-position: -87px -145px; + background-position: -58px -145px; } .apps-help-white-large { - background-position: -116px -145px; + background-position: -87px -145px; } .apps-herald-light-large { - background-position: -145px -145px; + background-position: -116px -145px; } .apps-herald-dark-large { - background-position: -174px -145px; + background-position: -145px -145px; } .apps-herald-blue-large, .phabricator-crumb-view:hover .apps-herald-dark-large { - background-position: -203px -145px; + background-position: -174px -145px; } .apps-herald-white-large { - background-position: -232px -145px; + background-position: -203px -145px; } .apps-home-light-large { - background-position: -261px -145px; + background-position: -232px -145px; } .apps-home-dark-large { - background-position: -290px -145px; + background-position: -261px -145px; } .apps-home-blue-large, .phabricator-crumb-view:hover .apps-home-dark-large { - background-position: -319px -145px; + background-position: -290px -145px; } .apps-home-white-large { + background-position: -319px -145px; +} + +.apps-info-sm-light-large { background-position: -348px -145px; } -.apps-legalpad-light-large { +.apps-info-sm-dark-large { background-position: -377px -145px; } -.apps-legalpad-dark-large { +.apps-info-sm-blue-large, .phabricator-crumb-view:hover .apps-info-sm-dark-large { background-position: -406px -145px; } -.apps-legalpad-blue-large, .phabricator-crumb-view:hover .apps-legalpad-dark-large { +.apps-info-sm-white-large { + background-position: -435px -145px; +} + +.apps-legalpad-light-large { background-position: 0px -174px; } -.apps-legalpad-white-large { +.apps-legalpad-dark-large { background-position: -29px -174px; } -.apps-logo-light-large { +.apps-legalpad-blue-large, .phabricator-crumb-view:hover .apps-legalpad-dark-large { background-position: -58px -174px; } -.apps-logo-dark-large { +.apps-legalpad-white-large { background-position: -87px -174px; } -.apps-logo-blue-large, .phabricator-crumb-view:hover .apps-logo-dark-large { +.apps-logo-light-large { background-position: -116px -174px; } -.apps-logo-white-large { +.apps-logo-dark-large { background-position: -145px -174px; } -.apps-macro-light-large { +.apps-logo-blue-large, .phabricator-crumb-view:hover .apps-logo-dark-large { background-position: -174px -174px; } -.apps-macro-dark-large { +.apps-logo-white-large { background-position: -203px -174px; } -.apps-macro-blue-large, .phabricator-crumb-view:hover .apps-macro-dark-large { +.apps-logout-sm-light-large { background-position: -232px -174px; } -.apps-macro-white-large { +.apps-logout-sm-dark-large { background-position: -261px -174px; } -.apps-mail-light-large { +.apps-logout-sm-blue-large, .phabricator-crumb-view:hover .apps-logout-sm-dark-large { background-position: -290px -174px; } -.apps-mail-dark-large { +.apps-logout-sm-white-large { background-position: -319px -174px; } -.apps-mail-blue-large, .phabricator-crumb-view:hover .apps-mail-dark-large { +.apps-macro-light-large { background-position: -348px -174px; } -.apps-mail-white-large { +.apps-macro-dark-large { background-position: -377px -174px; } -.apps-maniphest-light-large { +.apps-macro-blue-large, .phabricator-crumb-view:hover .apps-macro-dark-large { background-position: -406px -174px; } -.apps-maniphest-dark-large { +.apps-macro-white-large { + background-position: -435px -174px; +} + +.apps-mail-light-large { background-position: 0px -203px; } -.apps-maniphest-blue-large, .phabricator-crumb-view:hover .apps-maniphest-dark-large { +.apps-mail-dark-large { background-position: -29px -203px; } -.apps-maniphest-white-large { +.apps-mail-blue-large, .phabricator-crumb-view:hover .apps-mail-dark-large { background-position: -58px -203px; } -.apps-metamta-light-large { +.apps-mail-white-large { background-position: -87px -203px; } -.apps-metamta-dark-large { +.apps-maniphest-light-large { background-position: -116px -203px; } -.apps-metamta-blue-large, .phabricator-crumb-view:hover .apps-metamta-dark-large { +.apps-maniphest-dark-large { background-position: -145px -203px; } -.apps-metamta-white-large { +.apps-maniphest-blue-large, .phabricator-crumb-view:hover .apps-maniphest-dark-large { background-position: -174px -203px; } -.apps-new-light-large { +.apps-maniphest-white-large { background-position: -203px -203px; } -.apps-new-dark-large { +.apps-metamta-light-large { background-position: -232px -203px; } -.apps-new-blue-large, .phabricator-crumb-view:hover .apps-new-dark-large { +.apps-metamta-dark-large { background-position: -261px -203px; } -.apps-new-white-large { +.apps-metamta-blue-large, .phabricator-crumb-view:hover .apps-metamta-dark-large { background-position: -290px -203px; } -.apps-nuance-light-large { +.apps-metamta-white-large { background-position: -319px -203px; } -.apps-nuance-dark-large { +.apps-new-sm-light-large { background-position: -348px -203px; } -.apps-nuance-blue-large, .phabricator-crumb-view:hover .apps-nuance-dark-large { +.apps-new-sm-dark-large { background-position: -377px -203px; } -.apps-nuance-white-large { +.apps-new-sm-blue-large, .phabricator-crumb-view:hover .apps-new-sm-dark-large { background-position: -406px -203px; } -.apps-owners-light-large { +.apps-new-sm-white-large { + background-position: -435px -203px; +} + +.apps-new-light-large { background-position: 0px -232px; } -.apps-owners-dark-large { +.apps-new-dark-large { background-position: -29px -232px; } -.apps-owners-blue-large, .phabricator-crumb-view:hover .apps-owners-dark-large { +.apps-new-blue-large, .phabricator-crumb-view:hover .apps-new-dark-large { background-position: -58px -232px; } -.apps-owners-white-large { +.apps-new-white-large { background-position: -87px -232px; } -.apps-paste-light-large { +.apps-nuance-light-large { background-position: -116px -232px; } -.apps-paste-dark-large { +.apps-nuance-dark-large { background-position: -145px -232px; } -.apps-paste-blue-large, .phabricator-crumb-view:hover .apps-paste-dark-large { +.apps-nuance-blue-large, .phabricator-crumb-view:hover .apps-nuance-dark-large { background-position: -174px -232px; } -.apps-paste-white-large { +.apps-nuance-white-large { background-position: -203px -232px; } -.apps-people-light-large { +.apps-owners-light-large { background-position: -232px -232px; } -.apps-people-dark-large { +.apps-owners-dark-large { background-position: -261px -232px; } -.apps-people-blue-large, .phabricator-crumb-view:hover .apps-people-dark-large { +.apps-owners-blue-large, .phabricator-crumb-view:hover .apps-owners-dark-large { background-position: -290px -232px; } -.apps-people-white-large { +.apps-owners-white-large { background-position: -319px -232px; } -.apps-phage-light-large { +.apps-passphrase-light-large { background-position: -348px -232px; } -.apps-phage-dark-large { +.apps-passphrase-dark-large { background-position: -377px -232px; } -.apps-phage-blue-large, .phabricator-crumb-view:hover .apps-phage-dark-large { +.apps-passphrase-blue-large, .phabricator-crumb-view:hover .apps-passphrase-dark-large { background-position: -406px -232px; } -.apps-phage-white-large { +.apps-passphrase-white-large { + background-position: -435px -232px; +} + +.apps-paste-light-large { background-position: 0px -261px; } -.apps-phame-light-large { +.apps-paste-dark-large { background-position: -29px -261px; } -.apps-phame-dark-large { +.apps-paste-blue-large, .phabricator-crumb-view:hover .apps-paste-dark-large { background-position: -58px -261px; } -.apps-phame-blue-large, .phabricator-crumb-view:hover .apps-phame-dark-large { +.apps-paste-white-large { background-position: -87px -261px; } -.apps-phame-white-large { +.apps-people-light-large { background-position: -116px -261px; } -.apps-phid-light-large { +.apps-people-dark-large { background-position: -145px -261px; } -.apps-phid-dark-large { +.apps-people-blue-large, .phabricator-crumb-view:hover .apps-people-dark-large { background-position: -174px -261px; } -.apps-phid-blue-large, .phabricator-crumb-view:hover .apps-phid-dark-large { +.apps-people-white-large { background-position: -203px -261px; } -.apps-phid-white-large { +.apps-phage-light-large { background-position: -232px -261px; } -.apps-phlux-light-large { +.apps-phage-dark-large { background-position: -261px -261px; } -.apps-phlux-dark-large { +.apps-phage-blue-large, .phabricator-crumb-view:hover .apps-phage-dark-large { background-position: -290px -261px; } -.apps-phlux-blue-large, .phabricator-crumb-view:hover .apps-phlux-dark-large { +.apps-phage-white-large { background-position: -319px -261px; } -.apps-phlux-white-large { +.apps-phame-light-large { background-position: -348px -261px; } -.apps-pholio-light-large { +.apps-phame-dark-large { background-position: -377px -261px; } -.apps-pholio-dark-large { +.apps-phame-blue-large, .phabricator-crumb-view:hover .apps-phame-dark-large { background-position: -406px -261px; } -.apps-pholio-blue-large, .phabricator-crumb-view:hover .apps-pholio-dark-large { +.apps-phame-white-large { + background-position: -435px -261px; +} + +.apps-phid-light-large { background-position: 0px -290px; } -.apps-pholio-white-large { +.apps-phid-dark-large { background-position: -29px -290px; } -.apps-phortune-light-large { +.apps-phid-blue-large, .phabricator-crumb-view:hover .apps-phid-dark-large { background-position: -58px -290px; } -.apps-phortune-dark-large { +.apps-phid-white-large { background-position: -87px -290px; } -.apps-phortune-blue-large, .phabricator-crumb-view:hover .apps-phortune-dark-large { +.apps-phlux-light-large { background-position: -116px -290px; } -.apps-phortune-white-large { +.apps-phlux-dark-large { background-position: -145px -290px; } -.apps-phpast-light-large { +.apps-phlux-blue-large, .phabricator-crumb-view:hover .apps-phlux-dark-large { background-position: -174px -290px; } -.apps-phpast-dark-large { +.apps-phlux-white-large { background-position: -203px -290px; } -.apps-phpast-blue-large, .phabricator-crumb-view:hover .apps-phpast-dark-large { +.apps-pholio-light-large { background-position: -232px -290px; } -.apps-phpast-white-large { +.apps-pholio-dark-large { background-position: -261px -290px; } -.apps-phrequent-light-large { +.apps-pholio-blue-large, .phabricator-crumb-view:hover .apps-pholio-dark-large { background-position: -290px -290px; } -.apps-phrequent-dark-large { +.apps-pholio-white-large { background-position: -319px -290px; } -.apps-phrequent-blue-large, .phabricator-crumb-view:hover .apps-phrequent-dark-large { +.apps-phortune-light-large { background-position: -348px -290px; } -.apps-phrequent-white-large { +.apps-phortune-dark-large { background-position: -377px -290px; } -.apps-phriction-light-large { +.apps-phortune-blue-large, .phabricator-crumb-view:hover .apps-phortune-dark-large { background-position: -406px -290px; } -.apps-phriction-dark-large { +.apps-phortune-white-large { + background-position: -435px -290px; +} + +.apps-phpast-light-large { background-position: 0px -319px; } -.apps-phriction-blue-large, .phabricator-crumb-view:hover .apps-phriction-dark-large { +.apps-phpast-dark-large { background-position: -29px -319px; } -.apps-phriction-white-large { +.apps-phpast-blue-large, .phabricator-crumb-view:hover .apps-phpast-dark-large { background-position: -58px -319px; } -.apps-policy-light-large { +.apps-phpast-white-large { background-position: -87px -319px; } -.apps-policy-dark-large { +.apps-phragment-light-large { background-position: -116px -319px; } -.apps-policy-blue-large, .phabricator-crumb-view:hover .apps-policy-dark-large { +.apps-phragment-dark-large { background-position: -145px -319px; } -.apps-policy-white-large { +.apps-phragment-blue-large, .phabricator-crumb-view:hover .apps-phragment-dark-large { background-position: -174px -319px; } -.apps-ponder-light-large { +.apps-phragment-white-large { background-position: -203px -319px; } -.apps-ponder-dark-large { +.apps-phrequent-light-large { background-position: -232px -319px; } -.apps-ponder-blue-large, .phabricator-crumb-view:hover .apps-ponder-dark-large { +.apps-phrequent-dark-large { background-position: -261px -319px; } -.apps-ponder-white-large { +.apps-phrequent-blue-large, .phabricator-crumb-view:hover .apps-phrequent-dark-large { background-position: -290px -319px; } -.apps-power-light-large { +.apps-phrequent-white-large { background-position: -319px -319px; } -.apps-power-dark-large { +.apps-phriction-light-large { background-position: -348px -319px; } -.apps-power-blue-large, .phabricator-crumb-view:hover .apps-power-dark-large { +.apps-phriction-dark-large { background-position: -377px -319px; } -.apps-power-white-large { +.apps-phriction-blue-large, .phabricator-crumb-view:hover .apps-phriction-dark-large { background-position: -406px -319px; } -.apps-projects-light-large { +.apps-phriction-white-large { + background-position: -435px -319px; +} + +.apps-policy-light-large { background-position: 0px -348px; } -.apps-projects-dark-large { +.apps-policy-dark-large { background-position: -29px -348px; } -.apps-projects-blue-large, .phabricator-crumb-view:hover .apps-projects-dark-large { +.apps-policy-blue-large, .phabricator-crumb-view:hover .apps-policy-dark-large { background-position: -58px -348px; } -.apps-projects-white-large { +.apps-policy-white-large { background-position: -87px -348px; } -.apps-releeph-light-large { +.apps-ponder-light-large { background-position: -116px -348px; } -.apps-releeph-dark-large { +.apps-ponder-dark-large { background-position: -145px -348px; } -.apps-releeph-blue-large, .phabricator-crumb-view:hover .apps-releeph-dark-large { +.apps-ponder-blue-large, .phabricator-crumb-view:hover .apps-ponder-dark-large { background-position: -174px -348px; } -.apps-releeph-white-large { +.apps-ponder-white-large { background-position: -203px -348px; } -.apps-repositories-light-large { +.apps-power-light-large { background-position: -232px -348px; } -.apps-repositories-dark-large { +.apps-power-dark-large { background-position: -261px -348px; } -.apps-repositories-blue-large, .phabricator-crumb-view:hover .apps-repositories-dark-large { +.apps-power-blue-large, .phabricator-crumb-view:hover .apps-power-dark-large { background-position: -290px -348px; } -.apps-repositories-white-large { +.apps-power-white-large { background-position: -319px -348px; } -.apps-search-light-large { +.apps-projects-light-large { background-position: -348px -348px; } -.apps-search-dark-large { +.apps-projects-dark-large { background-position: -377px -348px; } -.apps-search-blue-large, .phabricator-crumb-view:hover .apps-search-dark-large { +.apps-projects-blue-large, .phabricator-crumb-view:hover .apps-projects-dark-large { background-position: -406px -348px; } -.apps-search-white-large { +.apps-projects-white-large { background-position: 0px -377px; } -.apps-settings-light-large { +.apps-releeph-light-large { background-position: -29px -377px; } -.apps-settings-dark-large { +.apps-releeph-dark-large { background-position: -58px -377px; } -.apps-settings-blue-large, .phabricator-crumb-view:hover .apps-settings-dark-large { +.apps-releeph-blue-large, .phabricator-crumb-view:hover .apps-releeph-dark-large { background-position: -87px -377px; } -.apps-settings-white-large { +.apps-releeph-white-large { background-position: -116px -377px; } -.apps-setup-light-large { +.apps-repositories-light-large { background-position: -145px -377px; } -.apps-setup-dark-large { +.apps-repositories-dark-large { background-position: -174px -377px; } -.apps-setup-blue-large, .phabricator-crumb-view:hover .apps-setup-dark-large { +.apps-repositories-blue-large, .phabricator-crumb-view:hover .apps-repositories-dark-large { background-position: -203px -377px; } -.apps-setup-white-large { +.apps-repositories-white-large { background-position: -232px -377px; } -.apps-slowvote-light-large { +.apps-search-light-large { background-position: -261px -377px; } -.apps-slowvote-dark-large { +.apps-search-dark-large { background-position: -290px -377px; } -.apps-slowvote-blue-large, .phabricator-crumb-view:hover .apps-slowvote-dark-large { +.apps-search-blue-large, .phabricator-crumb-view:hover .apps-search-dark-large { background-position: -319px -377px; } -.apps-slowvote-white-large { +.apps-search-white-large { background-position: -348px -377px; } -.apps-token-light-large { +.apps-settings-sm-light-large { background-position: -377px -377px; } -.apps-token-dark-large { +.apps-settings-sm-dark-large { background-position: -406px -377px; } -.apps-token-blue-large, .phabricator-crumb-view:hover .apps-token-dark-large { +.apps-settings-sm-blue-large, .phabricator-crumb-view:hover .apps-settings-sm-dark-large { background-position: 0px -406px; } -.apps-token-white-large { +.apps-settings-sm-white-large { background-position: -29px -406px; } -.apps-uiexamples-light-large { +.apps-settings-light-large { background-position: -58px -406px; } -.apps-uiexamples-dark-large { +.apps-settings-dark-large { background-position: -87px -406px; } -.apps-uiexamples-blue-large, .phabricator-crumb-view:hover .apps-uiexamples-dark-large { +.apps-settings-blue-large, .phabricator-crumb-view:hover .apps-settings-dark-large { background-position: -116px -406px; } -.apps-uiexamples-white-large { +.apps-settings-white-large { background-position: -145px -406px; } -.apps-workphlow-light-large { +.apps-setup-light-large { background-position: -174px -406px; } -.apps-workphlow-dark-large { +.apps-setup-dark-large { background-position: -203px -406px; } -.apps-workphlow-blue-large, .phabricator-crumb-view:hover .apps-workphlow-dark-large { +.apps-setup-blue-large, .phabricator-crumb-view:hover .apps-setup-dark-large { background-position: -232px -406px; } -.apps-workphlow-white-large { +.apps-setup-white-large { background-position: -261px -406px; } -.apps-xhprof-light-large { +.apps-slowvote-light-large { background-position: -290px -406px; } -.apps-xhprof-dark-large { +.apps-slowvote-dark-large { background-position: -319px -406px; } -.apps-xhprof-blue-large, .phabricator-crumb-view:hover .apps-xhprof-dark-large { +.apps-slowvote-blue-large, .phabricator-crumb-view:hover .apps-slowvote-dark-large { background-position: -348px -406px; } -.apps-xhprof-white-large { +.apps-slowvote-white-large { background-position: -377px -406px; } + +.apps-token-light-large { + background-position: -406px -406px; +} + +.apps-token-dark-large { + background-position: 0px -435px; +} + +.apps-token-blue-large, .phabricator-crumb-view:hover .apps-token-dark-large { + background-position: -29px -435px; +} + +.apps-token-white-large { + background-position: -58px -435px; +} + +.apps-uiexamples-light-large { + background-position: -87px -435px; +} + +.apps-uiexamples-dark-large { + background-position: -116px -435px; +} + +.apps-uiexamples-blue-large, .phabricator-crumb-view:hover .apps-uiexamples-dark-large { + background-position: -145px -435px; +} + +.apps-uiexamples-white-large { + background-position: -174px -435px; +} + +.apps-workphlow-light-large { + background-position: -203px -435px; +} + +.apps-workphlow-dark-large { + background-position: -232px -435px; +} + +.apps-workphlow-blue-large, .phabricator-crumb-view:hover .apps-workphlow-dark-large { + background-position: -261px -435px; +} + +.apps-workphlow-white-large { + background-position: -290px -435px; +} + +.apps-xhprof-light-large { + background-position: -319px -435px; +} + +.apps-xhprof-dark-large { + background-position: -348px -435px; +} + +.apps-xhprof-blue-large, .phabricator-crumb-view:hover .apps-xhprof-dark-large { + background-position: -377px -435px; +} + +.apps-xhprof-white-large { + background-position: -406px -435px; +} diff --git a/webroot/rsrc/css/sprite-apps.css b/webroot/rsrc/css/sprite-apps.css index b6854a2817..981d00f653 100644 --- a/webroot/rsrc/css/sprite-apps.css +++ b/webroot/rsrc/css/sprite-apps.css @@ -13,7 +13,7 @@ only screen and (min-device-pixel-ratio: 1.5), only screen and (-webkit-min-device-pixel-ratio: 1.5) { .sprite-apps { background-image: url(/rsrc/image/sprite-apps-X2.png); - background-size: 165px 165px; + background-size: 165px 180px; } } @@ -22,7 +22,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: 0px 0px; } -.apps-adventure-white { +.apps-adventure-white, .phui-list-item-href:hover .apps-adventure-dark { background-position: -15px 0px; } @@ -30,7 +30,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -30px 0px; } -.apps-application-white { +.apps-application-white, .phui-list-item-href:hover .apps-application-dark { background-position: -45px 0px; } @@ -38,7 +38,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -60px 0px; } -.apps-audit-white { +.apps-audit-white, .phui-list-item-href:hover .apps-audit-dark { background-position: -75px 0px; } @@ -46,7 +46,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -90px 0px; } -.apps-authentication-white { +.apps-authentication-white, .phui-list-item-href:hover .apps-authentication-dark { background-position: -105px 0px; } @@ -54,7 +54,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -120px 0px; } -.apps-calendar-white { +.apps-calendar-white, .phui-list-item-href:hover .apps-calendar-dark { background-position: -135px 0px; } @@ -62,7 +62,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -150px 0px; } -.apps-chatlog-white { +.apps-chatlog-white, .phui-list-item-href:hover .apps-chatlog-dark { background-position: 0px -15px; } @@ -70,7 +70,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -15px -15px; } -.apps-conduit-white { +.apps-conduit-white, .phui-list-item-href:hover .apps-conduit-dark { background-position: -30px -15px; } @@ -78,7 +78,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -45px -15px; } -.apps-conpherence-white { +.apps-conpherence-white, .phui-list-item-href:hover .apps-conpherence-dark { background-position: -60px -15px; } @@ -86,7 +86,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -75px -15px; } -.apps-countdown-white { +.apps-countdown-white, .phui-list-item-href:hover .apps-countdown-dark { background-position: -90px -15px; } @@ -94,7 +94,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -105px -15px; } -.apps-daemon-white { +.apps-daemon-white, .phui-list-item-href:hover .apps-daemon-dark { background-position: -120px -15px; } @@ -102,7 +102,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -135px -15px; } -.apps-differential-white { +.apps-differential-white, .phui-list-item-href:hover .apps-differential-dark { background-position: -150px -15px; } @@ -110,7 +110,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: 0px -30px; } -.apps-diffusion-white { +.apps-diffusion-white, .phui-list-item-href:hover .apps-diffusion-dark { background-position: -15px -30px; } @@ -118,7 +118,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -30px -30px; } -.apps-diviner-white { +.apps-diviner-white, .phui-list-item-href:hover .apps-diviner-dark { background-position: -45px -30px; } @@ -126,7 +126,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -60px -30px; } -.apps-drydock-white { +.apps-drydock-white, .phui-list-item-href:hover .apps-drydock-dark { background-position: -75px -30px; } @@ -134,334 +134,390 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -90px -30px; } -.apps-fact-white { +.apps-fact-white, .phui-list-item-href:hover .apps-fact-dark { background-position: -105px -30px; } -.apps-feed-dark { +.apps-fancyhome-dark { background-position: -120px -30px; } -.apps-feed-white { +.apps-fancyhome-white, .phui-list-item-href:hover .apps-fancyhome-dark { background-position: -135px -30px; } -.apps-files-dark { +.apps-feed-dark { + background-position: -150px -30px; +} + +.apps-feed-white, .phui-list-item-href:hover .apps-feed-dark { background-position: 0px -45px; } -.apps-files-white { +.apps-files-dark { background-position: -15px -45px; } -.apps-flags-dark { +.apps-files-white, .phui-list-item-href:hover .apps-files-dark { background-position: -30px -45px; } -.apps-flags-white { +.apps-flags-dark { background-position: -45px -45px; } -.apps-harbormaster-dark { +.apps-flags-white, .phui-list-item-href:hover .apps-flags-dark { background-position: -60px -45px; } -.apps-harbormaster-white { +.apps-harbormaster-dark { background-position: -75px -45px; } -.apps-help-dark { +.apps-harbormaster-white, .phui-list-item-href:hover .apps-harbormaster-dark { background-position: -90px -45px; } -.apps-help-white { +.apps-help-dark { background-position: -105px -45px; } -.apps-herald-dark { +.apps-help-white, .phui-list-item-href:hover .apps-help-dark { background-position: -120px -45px; } -.apps-herald-white { +.apps-herald-dark { background-position: -135px -45px; } +.apps-herald-white, .phui-list-item-href:hover .apps-herald-dark { + background-position: -150px -45px; +} + .apps-home-dark { background-position: 0px -60px; } -.apps-home-white { +.apps-home-white, .phui-list-item-href:hover .apps-home-dark { background-position: -15px -60px; } -.apps-legalpad-dark { +.apps-info-sm-dark { background-position: -30px -60px; } -.apps-legalpad-white { +.apps-info-sm-white, .phui-list-item-href:hover .apps-info-sm-dark { background-position: -45px -60px; } -.apps-logo-dark { +.apps-legalpad-dark { background-position: -60px -60px; } -.apps-logo-white { +.apps-legalpad-white, .phui-list-item-href:hover .apps-legalpad-dark { background-position: -75px -60px; } -.apps-macro-dark { +.apps-logo-dark { background-position: -90px -60px; } -.apps-macro-white { +.apps-logo-white, .phui-list-item-href:hover .apps-logo-dark { background-position: -105px -60px; } -.apps-mail-dark { +.apps-logout-sm-dark { background-position: -120px -60px; } -.apps-mail-white { +.apps-logout-sm-white, .phui-list-item-href:hover .apps-logout-sm-dark { background-position: -135px -60px; } -.apps-maniphest-dark { +.apps-macro-dark { + background-position: -150px -60px; +} + +.apps-macro-white, .phui-list-item-href:hover .apps-macro-dark { background-position: 0px -75px; } -.apps-maniphest-white { +.apps-mail-dark { background-position: -15px -75px; } -.apps-metamta-dark { +.apps-mail-white, .phui-list-item-href:hover .apps-mail-dark { background-position: -30px -75px; } -.apps-metamta-white { +.apps-maniphest-dark { background-position: -45px -75px; } -.apps-new-dark { +.apps-maniphest-white, .phui-list-item-href:hover .apps-maniphest-dark { background-position: -60px -75px; } -.apps-new-white { +.apps-metamta-dark { background-position: -75px -75px; } -.apps-nuance-dark { +.apps-metamta-white, .phui-list-item-href:hover .apps-metamta-dark { background-position: -90px -75px; } -.apps-nuance-white { +.apps-new-sm-dark { background-position: -105px -75px; } -.apps-owners-dark { +.apps-new-sm-white, .phui-list-item-href:hover .apps-new-sm-dark { background-position: -120px -75px; } -.apps-owners-white { +.apps-new-dark { background-position: -135px -75px; } -.apps-paste-dark { +.apps-new-white, .phui-list-item-href:hover .apps-new-dark { + background-position: -150px -75px; +} + +.apps-nuance-dark { background-position: 0px -90px; } -.apps-paste-white { +.apps-nuance-white, .phui-list-item-href:hover .apps-nuance-dark { background-position: -15px -90px; } -.apps-people-dark { +.apps-owners-dark { background-position: -30px -90px; } -.apps-people-white { +.apps-owners-white, .phui-list-item-href:hover .apps-owners-dark { background-position: -45px -90px; } -.apps-phage-dark { +.apps-passphrase-dark { background-position: -60px -90px; } -.apps-phage-white { +.apps-passphrase-white, .phui-list-item-href:hover .apps-passphrase-dark { background-position: -75px -90px; } -.apps-phame-dark { +.apps-paste-dark { background-position: -90px -90px; } -.apps-phame-white { +.apps-paste-white, .phui-list-item-href:hover .apps-paste-dark { background-position: -105px -90px; } -.apps-phid-dark { +.apps-people-dark { background-position: -120px -90px; } -.apps-phid-white { +.apps-people-white, .phui-list-item-href:hover .apps-people-dark { background-position: -135px -90px; } -.apps-phlux-dark { +.apps-phage-dark { + background-position: -150px -90px; +} + +.apps-phage-white, .phui-list-item-href:hover .apps-phage-dark { background-position: 0px -105px; } -.apps-phlux-white { +.apps-phame-dark { background-position: -15px -105px; } -.apps-pholio-dark { +.apps-phame-white, .phui-list-item-href:hover .apps-phame-dark { background-position: -30px -105px; } -.apps-pholio-white { +.apps-phid-dark { background-position: -45px -105px; } -.apps-phortune-dark { +.apps-phid-white, .phui-list-item-href:hover .apps-phid-dark { background-position: -60px -105px; } -.apps-phortune-white { +.apps-phlux-dark { background-position: -75px -105px; } -.apps-phpast-dark { +.apps-phlux-white, .phui-list-item-href:hover .apps-phlux-dark { background-position: -90px -105px; } -.apps-phpast-white { +.apps-pholio-dark { background-position: -105px -105px; } -.apps-phrequent-dark { +.apps-pholio-white, .phui-list-item-href:hover .apps-pholio-dark { background-position: -120px -105px; } -.apps-phrequent-white { +.apps-phortune-dark { background-position: -135px -105px; } -.apps-phriction-dark { +.apps-phortune-white, .phui-list-item-href:hover .apps-phortune-dark { + background-position: -150px -105px; +} + +.apps-phpast-dark { background-position: 0px -120px; } -.apps-phriction-white { +.apps-phpast-white, .phui-list-item-href:hover .apps-phpast-dark { background-position: -15px -120px; } -.apps-policy-dark { +.apps-phragment-dark { background-position: -30px -120px; } -.apps-policy-white { +.apps-phragment-white, .phui-list-item-href:hover .apps-phragment-dark { background-position: -45px -120px; } -.apps-ponder-dark { +.apps-phrequent-dark { background-position: -60px -120px; } -.apps-ponder-white { +.apps-phrequent-white, .phui-list-item-href:hover .apps-phrequent-dark { background-position: -75px -120px; } -.apps-power-dark { +.apps-phriction-dark { background-position: -90px -120px; } -.apps-power-white { +.apps-phriction-white, .phui-list-item-href:hover .apps-phriction-dark { background-position: -105px -120px; } -.apps-projects-dark { +.apps-policy-dark { background-position: -120px -120px; } -.apps-projects-white { +.apps-policy-white, .phui-list-item-href:hover .apps-policy-dark { background-position: -135px -120px; } -.apps-releeph-dark { +.apps-ponder-dark { + background-position: -150px -120px; +} + +.apps-ponder-white, .phui-list-item-href:hover .apps-ponder-dark { background-position: 0px -135px; } -.apps-releeph-white { +.apps-power-dark { background-position: -15px -135px; } -.apps-repositories-dark { +.apps-power-white, .phui-list-item-href:hover .apps-power-dark { background-position: -30px -135px; } -.apps-repositories-white { +.apps-projects-dark { background-position: -45px -135px; } -.apps-search-dark { +.apps-projects-white, .phui-list-item-href:hover .apps-projects-dark { background-position: -60px -135px; } -.apps-search-white { +.apps-releeph-dark { background-position: -75px -135px; } -.apps-settings-dark { +.apps-releeph-white, .phui-list-item-href:hover .apps-releeph-dark { background-position: -90px -135px; } -.apps-settings-white { +.apps-repositories-dark { background-position: -105px -135px; } -.apps-setup-dark { +.apps-repositories-white, .phui-list-item-href:hover .apps-repositories-dark { background-position: -120px -135px; } -.apps-setup-white { +.apps-search-dark { background-position: -135px -135px; } -.apps-slowvote-dark { +.apps-search-white, .phui-list-item-href:hover .apps-search-dark { + background-position: -150px -135px; +} + +.apps-settings-sm-dark { background-position: 0px -150px; } -.apps-slowvote-white { +.apps-settings-sm-white, .phui-list-item-href:hover .apps-settings-sm-dark { background-position: -15px -150px; } -.apps-token-dark { +.apps-settings-dark { background-position: -30px -150px; } -.apps-token-white { +.apps-settings-white, .phui-list-item-href:hover .apps-settings-dark { background-position: -45px -150px; } -.apps-uiexamples-dark { +.apps-setup-dark { background-position: -60px -150px; } -.apps-uiexamples-white { +.apps-setup-white, .phui-list-item-href:hover .apps-setup-dark { background-position: -75px -150px; } -.apps-workphlow-dark { +.apps-slowvote-dark { background-position: -90px -150px; } -.apps-workphlow-white { +.apps-slowvote-white, .phui-list-item-href:hover .apps-slowvote-dark { background-position: -105px -150px; } -.apps-xhprof-dark { +.apps-token-dark { background-position: -120px -150px; } -.apps-xhprof-white { +.apps-token-white, .phui-list-item-href:hover .apps-token-dark { background-position: -135px -150px; } + +.apps-uiexamples-dark { + background-position: -150px -150px; +} + +.apps-uiexamples-white, .phui-list-item-href:hover .apps-uiexamples-dark { + background-position: 0px -165px; +} + +.apps-workphlow-dark { + background-position: -15px -165px; +} + +.apps-workphlow-white, .phui-list-item-href:hover .apps-workphlow-dark { + background-position: -30px -165px; +} + +.apps-xhprof-dark { + background-position: -45px -165px; +} + +.apps-xhprof-white, .phui-list-item-href:hover .apps-xhprof-dark { + background-position: -60px -165px; +} diff --git a/webroot/rsrc/css/sprite-icons.css b/webroot/rsrc/css/sprite-icons.css index d687c23d00..46aebbf1a9 100644 --- a/webroot/rsrc/css/sprite-icons.css +++ b/webroot/rsrc/css/sprite-icons.css @@ -13,7 +13,7 @@ only screen and (min-device-pixel-ratio: 1.5), only screen and (-webkit-min-device-pixel-ratio: 1.5) { .sprite-icons { background-image: url(/rsrc/image/sprite-icons-X2.png); - background-size: 225px 240px; + background-size: 240px 255px; } } @@ -34,50 +34,54 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -45px 0px; } -.icons-blame { +.icons-backward { background-position: -60px 0px; } -.icons-calendar { +.icons-blame { background-position: -75px 0px; } -.icons-check { +.icons-calendar { background-position: -90px 0px; } -.icons-comment { +.icons-check { background-position: -105px 0px; } -.icons-computer { +.icons-comment { background-position: -120px 0px; } -.icons-create { +.icons-computer { background-position: -135px 0px; } -.icons-data { +.icons-create { background-position: -150px 0px; } -.icons-delete { +.icons-data { background-position: -165px 0px; } -.icons-disable { +.icons-delete { background-position: -180px 0px; } -.icons-dislike { +.icons-disable { background-position: -195px 0px; } -.icons-download-alt { +.icons-dislike { background-position: -210px 0px; } +.icons-download-alt { + background-position: -225px 0px; +} + .icons-download { background-position: 0px -15px; } @@ -139,10 +143,14 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { } .icons-folder-open { - background-position: 0px -30px; + background-position: -225px -15px; } .icons-fork { + background-position: 0px -30px; +} + +.icons-forward { background-position: -15px -30px; } @@ -199,757 +207,845 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { } .icons-merge { - background-position: 0px -45px; + background-position: -225px -30px; } .icons-message { - background-position: -15px -45px; + background-position: 0px -45px; } .icons-meta-mta { - background-position: -30px -45px; + background-position: -15px -45px; } .icons-move { - background-position: -45px -45px; + background-position: -30px -45px; } .icons-music { - background-position: -60px -45px; + background-position: -45px -45px; } .icons-new { - background-position: -75px -45px; + background-position: -60px -45px; } .icons-none { - background-position: -90px -45px; + background-position: -75px -45px; } .icons-normal-priority { + background-position: -90px -45px; +} + +.icons-octocat { background-position: -105px -45px; } -.icons-perflab { +.icons-ok { background-position: -120px -45px; } -.icons-preview { +.icons-pause { background-position: -135px -45px; } -.icons-project { +.icons-perflab { background-position: -150px -45px; } -.icons-raise-priority { +.icons-play { background-position: -165px -45px; } -.icons-refresh { +.icons-popout { background-position: -180px -45px; } -.icons-remove { +.icons-preview { background-position: -195px -45px; } -.icons-search { +.icons-project { background-position: -210px -45px; } -.icons-start-sandcastle { +.icons-raise-priority { + background-position: -225px -45px; +} + +.icons-refresh { background-position: 0px -60px; } -.icons-tag { +.icons-remove { background-position: -15px -60px; } -.icons-transcript { +.icons-search { background-position: -30px -60px; } -.icons-undo { +.icons-start-sandcastle { background-position: -45px -60px; } -.icons-unlock { +.icons-stop { background-position: -60px -60px; } -.icons-unmerge { +.icons-tag { background-position: -75px -60px; } -.icons-unpublish { +.icons-transcript { background-position: -90px -60px; } -.icons-upload { +.icons-undo { background-position: -105px -60px; } -.icons-user { +.icons-unlock { background-position: -120px -60px; } -.icons-warning { +.icons-unmerge { background-position: -135px -60px; } -.icons-world { +.icons-unpublish { background-position: -150px -60px; } -.icons-wrench { +.icons-upload { background-position: -165px -60px; } -.icons-zip { +.icons-user { background-position: -180px -60px; } -.icons-action-menu-grey { +.icons-warning { background-position: -195px -60px; } -.icons-arrow_left-grey { +.icons-world { background-position: -210px -60px; } -.icons-arrow_right-grey { +.icons-wrench { + background-position: -225px -60px; +} + +.icons-zip { background-position: 0px -75px; } -.icons-attach-grey { +.icons-action-menu-grey { background-position: -15px -75px; } -.icons-blame-grey { +.icons-arrow_left-grey { background-position: -30px -75px; } -.icons-calendar-grey { +.icons-arrow_right-grey { background-position: -45px -75px; } -.icons-check-grey { +.icons-attach-grey { background-position: -60px -75px; } -.icons-comment-grey { +.icons-backward-grey { background-position: -75px -75px; } -.icons-computer-grey { +.icons-blame-grey { background-position: -90px -75px; } -.icons-create-grey { +.icons-calendar-grey { background-position: -105px -75px; } -.icons-data-grey { +.icons-check-grey { background-position: -120px -75px; } -.icons-delete-grey { +.icons-comment-grey { background-position: -135px -75px; } -.icons-disable-grey { +.icons-computer-grey { background-position: -150px -75px; } -.icons-dislike-grey { +.icons-create-grey { background-position: -165px -75px; } -.icons-download-alt-grey { +.icons-data-grey { background-position: -180px -75px; } -.icons-download-grey { +.icons-delete-grey { background-position: -195px -75px; } -.icons-edit-grey { +.icons-disable-grey { background-position: -210px -75px; } -.icons-enable-grey { +.icons-dislike-grey { + background-position: -225px -75px; +} + +.icons-download-alt-grey { background-position: 0px -90px; } -.icons-file-grey { +.icons-download-grey { background-position: -15px -90px; } -.icons-film-grey { +.icons-edit-grey { background-position: -30px -90px; } -.icons-flag-0-grey { +.icons-enable-grey { background-position: -45px -90px; } -.icons-flag-1-grey { +.icons-file-grey { background-position: -60px -90px; } -.icons-flag-2-grey { +.icons-film-grey { background-position: -75px -90px; } -.icons-flag-3-grey { +.icons-flag-0-grey { background-position: -90px -90px; } -.icons-flag-4-grey { +.icons-flag-1-grey { background-position: -105px -90px; } -.icons-flag-5-grey { +.icons-flag-2-grey { background-position: -120px -90px; } -.icons-flag-6-grey { +.icons-flag-3-grey { background-position: -135px -90px; } -.icons-flag-7-grey { +.icons-flag-4-grey { background-position: -150px -90px; } -.icons-flag-ghost-grey { +.icons-flag-5-grey { background-position: -165px -90px; } -.icons-flag-grey { +.icons-flag-6-grey { background-position: -180px -90px; } -.icons-folder-open-grey { +.icons-flag-7-grey { background-position: -195px -90px; } -.icons-fork-grey { +.icons-flag-ghost-grey { background-position: -210px -90px; } -.icons-herald-grey { +.icons-flag-grey { + background-position: -225px -90px; +} + +.icons-folder-open-grey { background-position: 0px -105px; } -.icons-highlight-grey { +.icons-fork-grey { background-position: -15px -105px; } -.icons-history-grey { +.icons-forward-grey { background-position: -30px -105px; } -.icons-home-grey { +.icons-herald-grey { background-position: -45px -105px; } -.icons-image-grey { +.icons-highlight-grey { background-position: -60px -105px; } -.icons-like-grey { +.icons-history-grey { background-position: -75px -105px; } -.icons-link-grey { +.icons-home-grey { background-position: -90px -105px; } -.icons-lint-info-grey { +.icons-image-grey { background-position: -105px -105px; } -.icons-lint-ok-grey { +.icons-like-grey { background-position: -120px -105px; } -.icons-lint-warning-grey { +.icons-link-grey { background-position: -135px -105px; } -.icons-lock-grey { +.icons-lint-info-grey { background-position: -150px -105px; } -.icons-love-grey { +.icons-lint-ok-grey { background-position: -165px -105px; } -.icons-lower-priority-grey { +.icons-lint-warning-grey { background-position: -180px -105px; } -.icons-merge-grey { +.icons-lock-grey { background-position: -195px -105px; } -.icons-message-grey { +.icons-love-grey { background-position: -210px -105px; } -.icons-meta-mta-grey { +.icons-lower-priority-grey { + background-position: -225px -105px; +} + +.icons-merge-grey { background-position: 0px -120px; } -.icons-move-grey { +.icons-message-grey { background-position: -15px -120px; } -.icons-music-grey { +.icons-meta-mta-grey { background-position: -30px -120px; } -.icons-new-grey { +.icons-move-grey { background-position: -45px -120px; } -.icons-none-grey { +.icons-music-grey { background-position: -60px -120px; } -.icons-normal-priority-grey { +.icons-new-grey { background-position: -75px -120px; } -.icons-perflab-grey { +.icons-none-grey { background-position: -90px -120px; } -.icons-preview-grey { +.icons-normal-priority-grey { background-position: -105px -120px; } -.icons-project-grey { +.icons-octocat-grey { background-position: -120px -120px; } -.icons-raise-priority-grey { +.icons-ok-grey { background-position: -135px -120px; } -.icons-refresh-grey { +.icons-pause-grey { background-position: -150px -120px; } -.icons-remove-grey { +.icons-perflab-grey { background-position: -165px -120px; } -.icons-search-grey { +.icons-play-grey { background-position: -180px -120px; } -.icons-start-sandcastle-grey { +.icons-popout-grey { background-position: -195px -120px; } -.icons-tag-grey { +.icons-preview-grey { background-position: -210px -120px; } -.icons-transcript-grey { +.icons-project-grey { + background-position: -225px -120px; +} + +.icons-raise-priority-grey { background-position: 0px -135px; } -.icons-undo-grey { +.icons-refresh-grey { background-position: -15px -135px; } -.icons-unlock-grey { +.icons-remove-grey { background-position: -30px -135px; } -.icons-unmerge-grey { +.icons-search-grey { background-position: -45px -135px; } -.icons-unpublish-grey { +.icons-start-sandcastle-grey { background-position: -60px -135px; } -.icons-upload-grey { +.icons-stop-grey { background-position: -75px -135px; } -.icons-user-grey { +.icons-tag-grey { background-position: -90px -135px; } -.icons-warning-grey { +.icons-transcript-grey { background-position: -105px -135px; } -.icons-world-grey { +.icons-undo-grey { background-position: -120px -135px; } -.icons-wrench-grey { +.icons-unlock-grey { background-position: -135px -135px; } -.icons-zip-grey { +.icons-unmerge-grey { background-position: -150px -135px; } -.icons-action-menu-white, .device-desktop .phabricator-action-view:hover .icons-action-menu, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-action-menu { +.icons-unpublish-grey { background-position: -165px -135px; } -.icons-arrow_left-white, .device-desktop .phabricator-action-view:hover .icons-arrow_left, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-arrow_left { +.icons-upload-grey { background-position: -180px -135px; } -.icons-arrow_right-white, .device-desktop .phabricator-action-view:hover .icons-arrow_right, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-arrow_right { +.icons-user-grey { background-position: -195px -135px; } -.icons-attach-white, .device-desktop .phabricator-action-view:hover .icons-attach, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-attach { +.icons-warning-grey { background-position: -210px -135px; } -.icons-blame-white, .device-desktop .phabricator-action-view:hover .icons-blame, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-blame { +.icons-world-grey { + background-position: -225px -135px; +} + +.icons-wrench-grey { background-position: 0px -150px; } -.icons-calendar-white, .device-desktop .phabricator-action-view:hover .icons-calendar, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-calendar { +.icons-zip-grey { background-position: -15px -150px; } -.icons-check-white, .device-desktop .phabricator-action-view:hover .icons-check, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-check { +.icons-action-menu-white, .device-desktop .phabricator-action-view:hover .icons-action-menu, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-action-menu { background-position: -30px -150px; } -.icons-comment-white, .device-desktop .phabricator-action-view:hover .icons-comment, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-comment { +.icons-arrow_left-white, .device-desktop .phabricator-action-view:hover .icons-arrow_left, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-arrow_left { background-position: -45px -150px; } -.icons-computer-white, .device-desktop .phabricator-action-view:hover .icons-computer, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-computer { +.icons-arrow_right-white, .device-desktop .phabricator-action-view:hover .icons-arrow_right, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-arrow_right { background-position: -60px -150px; } -.icons-create-white, .device-desktop .phabricator-action-view:hover .icons-create, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-create { +.icons-attach-white, .device-desktop .phabricator-action-view:hover .icons-attach, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-attach { background-position: -75px -150px; } -.icons-data-white, .device-desktop .phabricator-action-view:hover .icons-data, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-data { +.icons-backward-white, .device-desktop .phabricator-action-view:hover .icons-backward, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-backward { background-position: -90px -150px; } -.icons-delete-white, .device-desktop .phabricator-action-view:hover .icons-delete, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-delete { +.icons-blame-white, .device-desktop .phabricator-action-view:hover .icons-blame, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-blame { background-position: -105px -150px; } -.icons-disable-white, .device-desktop .phabricator-action-view:hover .icons-disable, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-disable { +.icons-calendar-white, .device-desktop .phabricator-action-view:hover .icons-calendar, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-calendar { background-position: -120px -150px; } -.icons-dislike-white, .device-desktop .phabricator-action-view:hover .icons-dislike, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-dislike { +.icons-check-white, .device-desktop .phabricator-action-view:hover .icons-check, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-check { background-position: -135px -150px; } -.icons-download-alt-white, .device-desktop .phabricator-action-view:hover .icons-download-alt, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-download-alt { +.icons-comment-white, .device-desktop .phabricator-action-view:hover .icons-comment, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-comment { background-position: -150px -150px; } -.icons-download-white, .device-desktop .phabricator-action-view:hover .icons-download, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-download { +.icons-computer-white, .device-desktop .phabricator-action-view:hover .icons-computer, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-computer { background-position: -165px -150px; } -.icons-edit-white, .device-desktop .phabricator-action-view:hover .icons-edit, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-edit { +.icons-create-white, .device-desktop .phabricator-action-view:hover .icons-create, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-create { background-position: -180px -150px; } -.icons-enable-white, .device-desktop .phabricator-action-view:hover .icons-enable, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-enable { +.icons-data-white, .device-desktop .phabricator-action-view:hover .icons-data, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-data { background-position: -195px -150px; } -.icons-file-white, .device-desktop .phabricator-action-view:hover .icons-file, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-file { +.icons-delete-white, .device-desktop .phabricator-action-view:hover .icons-delete, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-delete { background-position: -210px -150px; } -.icons-film-white, .device-desktop .phabricator-action-view:hover .icons-film, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-film { +.icons-disable-white, .device-desktop .phabricator-action-view:hover .icons-disable, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-disable { + background-position: -225px -150px; +} + +.icons-dislike-white, .device-desktop .phabricator-action-view:hover .icons-dislike, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-dislike { background-position: 0px -165px; } -.icons-flag-0-white, .device-desktop .phabricator-action-view:hover .icons-flag-0, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-0 { +.icons-download-alt-white, .device-desktop .phabricator-action-view:hover .icons-download-alt, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-download-alt { background-position: -15px -165px; } -.icons-flag-1-white, .device-desktop .phabricator-action-view:hover .icons-flag-1, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-1 { +.icons-download-white, .device-desktop .phabricator-action-view:hover .icons-download, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-download { background-position: -30px -165px; } -.icons-flag-2-white, .device-desktop .phabricator-action-view:hover .icons-flag-2, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-2 { +.icons-edit-white, .device-desktop .phabricator-action-view:hover .icons-edit, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-edit { background-position: -45px -165px; } -.icons-flag-3-white, .device-desktop .phabricator-action-view:hover .icons-flag-3, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-3 { +.icons-enable-white, .device-desktop .phabricator-action-view:hover .icons-enable, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-enable { background-position: -60px -165px; } -.icons-flag-4-white, .device-desktop .phabricator-action-view:hover .icons-flag-4, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-4 { +.icons-file-white, .device-desktop .phabricator-action-view:hover .icons-file, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-file { background-position: -75px -165px; } -.icons-flag-5-white, .device-desktop .phabricator-action-view:hover .icons-flag-5, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-5 { +.icons-film-white, .device-desktop .phabricator-action-view:hover .icons-film, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-film { background-position: -90px -165px; } -.icons-flag-6-white, .device-desktop .phabricator-action-view:hover .icons-flag-6, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-6 { +.icons-flag-0-white, .device-desktop .phabricator-action-view:hover .icons-flag-0, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-0 { background-position: -105px -165px; } -.icons-flag-7-white, .device-desktop .phabricator-action-view:hover .icons-flag-7, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-7 { +.icons-flag-1-white, .device-desktop .phabricator-action-view:hover .icons-flag-1, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-1 { background-position: -120px -165px; } -.icons-flag-ghost-white, .device-desktop .phabricator-action-view:hover .icons-flag-ghost, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-ghost { +.icons-flag-2-white, .device-desktop .phabricator-action-view:hover .icons-flag-2, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-2 { background-position: -135px -165px; } -.icons-flag-white, .device-desktop .phabricator-action-view:hover .icons-flag, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag { +.icons-flag-3-white, .device-desktop .phabricator-action-view:hover .icons-flag-3, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-3 { background-position: -150px -165px; } -.icons-folder-open-white, .device-desktop .phabricator-action-view:hover .icons-folder-open, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-folder-open { +.icons-flag-4-white, .device-desktop .phabricator-action-view:hover .icons-flag-4, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-4 { background-position: -165px -165px; } -.icons-fork-white, .device-desktop .phabricator-action-view:hover .icons-fork, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-fork { +.icons-flag-5-white, .device-desktop .phabricator-action-view:hover .icons-flag-5, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-5 { background-position: -180px -165px; } -.icons-herald-white, .device-desktop .phabricator-action-view:hover .icons-herald, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-herald { +.icons-flag-6-white, .device-desktop .phabricator-action-view:hover .icons-flag-6, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-6 { background-position: -195px -165px; } -.icons-highlight-white, .device-desktop .phabricator-action-view:hover .icons-highlight, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-highlight { +.icons-flag-7-white, .device-desktop .phabricator-action-view:hover .icons-flag-7, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-7 { background-position: -210px -165px; } -.icons-history-white, .device-desktop .phabricator-action-view:hover .icons-history, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-history { +.icons-flag-ghost-white, .device-desktop .phabricator-action-view:hover .icons-flag-ghost, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag-ghost { + background-position: -225px -165px; +} + +.icons-flag-white, .device-desktop .phabricator-action-view:hover .icons-flag, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-flag { background-position: 0px -180px; } -.icons-home-white, .device-desktop .phabricator-action-view:hover .icons-home, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-home { +.icons-folder-open-white, .device-desktop .phabricator-action-view:hover .icons-folder-open, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-folder-open { background-position: -15px -180px; } -.icons-image-white, .device-desktop .phabricator-action-view:hover .icons-image, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-image { +.icons-fork-white, .device-desktop .phabricator-action-view:hover .icons-fork, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-fork { background-position: -30px -180px; } -.icons-like-white, .device-desktop .phabricator-action-view:hover .icons-like, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-like { +.icons-forward-white, .device-desktop .phabricator-action-view:hover .icons-forward, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-forward { background-position: -45px -180px; } -.icons-link-white, .device-desktop .phabricator-action-view:hover .icons-link, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-link { +.icons-herald-white, .device-desktop .phabricator-action-view:hover .icons-herald, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-herald { background-position: -60px -180px; } -.icons-lint-info-white, .device-desktop .phabricator-action-view:hover .icons-lint-info, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lint-info { +.icons-highlight-white, .device-desktop .phabricator-action-view:hover .icons-highlight, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-highlight { background-position: -75px -180px; } -.icons-lint-ok-white, .device-desktop .phabricator-action-view:hover .icons-lint-ok, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lint-ok { +.icons-history-white, .device-desktop .phabricator-action-view:hover .icons-history, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-history { background-position: -90px -180px; } -.icons-lint-warning-white, .device-desktop .phabricator-action-view:hover .icons-lint-warning, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lint-warning { +.icons-home-white, .device-desktop .phabricator-action-view:hover .icons-home, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-home { background-position: -105px -180px; } -.icons-lock-white, .device-desktop .phabricator-action-view:hover .icons-lock, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lock { +.icons-image-white, .device-desktop .phabricator-action-view:hover .icons-image, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-image { background-position: -120px -180px; } -.icons-love-white, .device-desktop .phabricator-action-view:hover .icons-love, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-love { +.icons-like-white, .device-desktop .phabricator-action-view:hover .icons-like, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-like { background-position: -135px -180px; } -.icons-lower-priority-white, .device-desktop .phabricator-action-view:hover .icons-lower-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lower-priority { +.icons-link-white, .device-desktop .phabricator-action-view:hover .icons-link, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-link { background-position: -150px -180px; } -.icons-merge-white, .device-desktop .phabricator-action-view:hover .icons-merge, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-merge { +.icons-lint-info-white, .device-desktop .phabricator-action-view:hover .icons-lint-info, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lint-info { background-position: -165px -180px; } -.icons-message-white, .device-desktop .phabricator-action-view:hover .icons-message, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-message { +.icons-lint-ok-white, .device-desktop .phabricator-action-view:hover .icons-lint-ok, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lint-ok { background-position: -180px -180px; } -.icons-meta-mta-white, .device-desktop .phabricator-action-view:hover .icons-meta-mta, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-meta-mta { +.icons-lint-warning-white, .device-desktop .phabricator-action-view:hover .icons-lint-warning, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lint-warning { background-position: -195px -180px; } -.icons-move-white, .device-desktop .phabricator-action-view:hover .icons-move, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-move { +.icons-lock-white, .device-desktop .phabricator-action-view:hover .icons-lock, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lock { background-position: -210px -180px; } -.icons-music-white, .device-desktop .phabricator-action-view:hover .icons-music, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-music { +.icons-love-white, .device-desktop .phabricator-action-view:hover .icons-love, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-love { + background-position: -225px -180px; +} + +.icons-lower-priority-white, .device-desktop .phabricator-action-view:hover .icons-lower-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-lower-priority { background-position: 0px -195px; } -.icons-new-white, .device-desktop .phabricator-action-view:hover .icons-new, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-new { +.icons-merge-white, .device-desktop .phabricator-action-view:hover .icons-merge, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-merge { background-position: -15px -195px; } -.icons-none-white, .device-desktop .phabricator-action-view:hover .icons-none, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-none { +.icons-message-white, .device-desktop .phabricator-action-view:hover .icons-message, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-message { background-position: -30px -195px; } -.icons-normal-priority-white, .device-desktop .phabricator-action-view:hover .icons-normal-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-normal-priority { +.icons-meta-mta-white, .device-desktop .phabricator-action-view:hover .icons-meta-mta, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-meta-mta { background-position: -45px -195px; } -.icons-perflab-white, .device-desktop .phabricator-action-view:hover .icons-perflab, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-perflab { +.icons-move-white, .device-desktop .phabricator-action-view:hover .icons-move, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-move { background-position: -60px -195px; } -.icons-preview-white, .device-desktop .phabricator-action-view:hover .icons-preview, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-preview { +.icons-music-white, .device-desktop .phabricator-action-view:hover .icons-music, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-music { background-position: -75px -195px; } -.icons-project-white, .device-desktop .phabricator-action-view:hover .icons-project, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-project { +.icons-new-white, .device-desktop .phabricator-action-view:hover .icons-new, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-new { background-position: -90px -195px; } -.icons-raise-priority-white, .device-desktop .phabricator-action-view:hover .icons-raise-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-raise-priority { +.icons-none-white, .device-desktop .phabricator-action-view:hover .icons-none, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-none { background-position: -105px -195px; } -.icons-refresh-white, .device-desktop .phabricator-action-view:hover .icons-refresh, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-refresh { +.icons-normal-priority-white, .device-desktop .phabricator-action-view:hover .icons-normal-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-normal-priority { background-position: -120px -195px; } -.icons-remove-white, .device-desktop .phabricator-action-view:hover .icons-remove, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-remove { +.icons-octocat-white, .device-desktop .phabricator-action-view:hover .icons-octocat, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-octocat { background-position: -135px -195px; } -.icons-search-white, .device-desktop .phabricator-action-view:hover .icons-search, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-search { +.icons-ok-white, .device-desktop .phabricator-action-view:hover .icons-ok, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-ok { background-position: -150px -195px; } -.icons-start-sandcastle-white, .device-desktop .phabricator-action-view:hover .icons-start-sandcastle, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-start-sandcastle { +.icons-pause-white, .device-desktop .phabricator-action-view:hover .icons-pause, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-pause { background-position: -165px -195px; } -.icons-tag-white, .device-desktop .phabricator-action-view:hover .icons-tag, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-tag { +.icons-perflab-white, .device-desktop .phabricator-action-view:hover .icons-perflab, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-perflab { background-position: -180px -195px; } -.icons-transcript-white, .device-desktop .phabricator-action-view:hover .icons-transcript, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-transcript { +.icons-play-white, .device-desktop .phabricator-action-view:hover .icons-play, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-play { background-position: -195px -195px; } -.icons-undo-white, .device-desktop .phabricator-action-view:hover .icons-undo, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-undo { +.icons-popout-white, .device-desktop .phabricator-action-view:hover .icons-popout, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-popout { background-position: -210px -195px; } -.icons-unlock-white, .device-desktop .phabricator-action-view:hover .icons-unlock, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-unlock { +.icons-preview-white, .device-desktop .phabricator-action-view:hover .icons-preview, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-preview { + background-position: -225px -195px; +} + +.icons-project-white, .device-desktop .phabricator-action-view:hover .icons-project, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-project { background-position: 0px -210px; } -.icons-unmerge-white, .device-desktop .phabricator-action-view:hover .icons-unmerge, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-unmerge { +.icons-raise-priority-white, .device-desktop .phabricator-action-view:hover .icons-raise-priority, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-raise-priority { background-position: -15px -210px; } -.icons-unpublish-white, .device-desktop .phabricator-action-view:hover .icons-unpublish, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-unpublish { +.icons-refresh-white, .device-desktop .phabricator-action-view:hover .icons-refresh, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-refresh { background-position: -30px -210px; } -.icons-upload-white, .device-desktop .phabricator-action-view:hover .icons-upload, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-upload { +.icons-remove-white, .device-desktop .phabricator-action-view:hover .icons-remove, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-remove { background-position: -45px -210px; } -.icons-user-white, .device-desktop .phabricator-action-view:hover .icons-user, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-user { +.icons-search-white, .device-desktop .phabricator-action-view:hover .icons-search, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-search { background-position: -60px -210px; } -.icons-warning-white, .device-desktop .phabricator-action-view:hover .icons-warning, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-warning { +.icons-start-sandcastle-white, .device-desktop .phabricator-action-view:hover .icons-start-sandcastle, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-start-sandcastle { background-position: -75px -210px; } -.icons-world-white, .device-desktop .phabricator-action-view:hover .icons-world, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-world { +.icons-stop-white, .device-desktop .phabricator-action-view:hover .icons-stop, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-stop { background-position: -90px -210px; } -.icons-wrench-white, .device-desktop .phabricator-action-view:hover .icons-wrench, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-wrench { +.icons-tag-white, .device-desktop .phabricator-action-view:hover .icons-tag, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-tag { background-position: -105px -210px; } -.icons-zip-white, .device-desktop .phabricator-action-view:hover .icons-zip, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-zip { +.icons-transcript-white, .device-desktop .phabricator-action-view:hover .icons-transcript, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-transcript { background-position: -120px -210px; } -.remarkup-assist-b { +.icons-undo-white, .device-desktop .phabricator-action-view:hover .icons-undo, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-undo { background-position: -135px -210px; } -.remarkup-assist-code { +.icons-unlock-white, .device-desktop .phabricator-action-view:hover .icons-unlock, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-unlock { background-position: -150px -210px; } -.remarkup-assist-fullscreen { +.icons-unmerge-white, .device-desktop .phabricator-action-view:hover .icons-unmerge, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-unmerge { background-position: -165px -210px; } -.remarkup-control-fullscreen-mode .remarkup-assist-fullscreen { +.icons-unpublish-white, .device-desktop .phabricator-action-view:hover .icons-unpublish, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-unpublish { background-position: -180px -210px; } -.remarkup-assist-help { +.icons-upload-white, .device-desktop .phabricator-action-view:hover .icons-upload, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-upload { background-position: -195px -210px; } -.remarkup-assist-i { +.icons-user-white, .device-desktop .phabricator-action-view:hover .icons-user, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-user { background-position: -210px -210px; } -.remarkup-assist-image { +.icons-warning-white, .device-desktop .phabricator-action-view:hover .icons-warning, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-warning { + background-position: -225px -210px; +} + +.icons-world-white, .device-desktop .phabricator-action-view:hover .icons-world, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-world { background-position: 0px -225px; } -.remarkup-assist-larger { +.icons-wrench-white, .device-desktop .phabricator-action-view:hover .icons-wrench, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-wrench { background-position: -15px -225px; } -.remarkup-assist-link { +.icons-zip-white, .device-desktop .phabricator-action-view:hover .icons-zip, .device-desktop .phui-list-sidenav .phui-list-item-href:hover .icons-zip { background-position: -30px -225px; } -.remarkup-assist-meme { +.remarkup-assist-b { background-position: -45px -225px; } -.remarkup-assist-ol { +.remarkup-assist-code { background-position: -60px -225px; } -.remarkup-assist-table { +.remarkup-assist-fullscreen { background-position: -75px -225px; } -.remarkup-assist-tag { +.remarkup-control-fullscreen-mode .remarkup-assist-fullscreen { background-position: -90px -225px; } -.remarkup-assist-tt { +.remarkup-assist-help { background-position: -105px -225px; } -.remarkup-assist-ul { +.remarkup-assist-i { background-position: -120px -225px; } + +.remarkup-assist-image { + background-position: -135px -225px; +} + +.remarkup-assist-larger { + background-position: -150px -225px; +} + +.remarkup-assist-link { + background-position: -165px -225px; +} + +.remarkup-assist-meme { + background-position: -180px -225px; +} + +.remarkup-assist-ol { + background-position: -195px -225px; +} + +.remarkup-assist-table { + background-position: -210px -225px; +} + +.remarkup-assist-tag { + background-position: -225px -225px; +} + +.remarkup-assist-tt { + background-position: 0px -240px; +} + +.remarkup-assist-ul { + background-position: -15px -240px; +} diff --git a/webroot/rsrc/css/sprite-main-header.css b/webroot/rsrc/css/sprite-main-header.css new file mode 100644 index 0000000000..20af55aa9b --- /dev/null +++ b/webroot/rsrc/css/sprite-main-header.css @@ -0,0 +1,48 @@ +/** + * @provides sprite-main-header-css + * @generated + */ + +.sprite-main-header { + background-image: url(/rsrc/image/sprite-main-header.png); + background-repeat: repeat-x; +} + + + + +.main-header-applebloom { + background-position: 0px 0px; +} + +.main-header-blue { + background-position: 0px -45px; +} + +.main-header-dark { + background-position: 0px -90px; +} + +.main-header-fluttershy { + background-position: 0px -135px; +} + +.main-header-green { + background-position: 0px -180px; +} + +.main-header-nightmaremoon { + background-position: 0px -225px; +} + +.main-header-red { + background-position: 0px -270px; +} + +.main-header-scootaloo { + background-position: 0px -315px; +} + +.main-header-yellow { + background-position: 0px -360px; +} diff --git a/webroot/rsrc/css/sprite-status.css b/webroot/rsrc/css/sprite-status.css index dd69f257ee..79d2fb6036 100644 --- a/webroot/rsrc/css/sprite-status.css +++ b/webroot/rsrc/css/sprite-status.css @@ -13,7 +13,7 @@ only screen and (min-device-pixel-ratio: 1.5), only screen and (-webkit-min-device-pixel-ratio: 1.5) { .sprite-status { background-image: url(/rsrc/image/sprite-status-X2.png); - background-size: 150px 150px; + background-size: 150px 165px; } } @@ -278,138 +278,146 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5) { background-position: -60px -90px; } -.status-question-blue { +.status-policy-user-white { background-position: -75px -90px; } -.status-question-dark { +.status-policy-user { background-position: -90px -90px; } -.status-question-green { +.status-question-blue { background-position: -105px -90px; } -.status-question-red { +.status-question-dark { background-position: -120px -90px; } -.status-question-white { +.status-question-green { background-position: -135px -90px; } -.status-question { +.status-question-red { background-position: 0px -105px; } -.status-reject-blue { +.status-question-white { background-position: -15px -105px; } -.status-reject-dark { +.status-question { background-position: -30px -105px; } -.status-reject-green { +.status-reject-blue { background-position: -45px -105px; } -.status-reject-red { +.status-reject-dark { background-position: -60px -105px; } -.status-reject-white { +.status-reject-green { background-position: -75px -105px; } -.status-reject { +.status-reject-red { background-position: -90px -105px; } -.status-right-blue { +.status-reject-white { background-position: -105px -105px; } -.status-right-dark { +.status-reject { background-position: -120px -105px; } -.status-right-green { +.status-right-blue { background-position: -135px -105px; } -.status-right-red { +.status-right-dark { background-position: 0px -120px; } -.status-right-white { +.status-right-green { background-position: -15px -120px; } -.status-right { +.status-right-red { background-position: -30px -120px; } -.status-time-green { +.status-right-white { background-position: -45px -120px; } -.status-time-orange { +.status-right { background-position: -60px -120px; } -.status-time-red { +.status-time-green { background-position: -75px -120px; } -.status-time-yellow { +.status-time-orange { background-position: -90px -120px; } -.status-up-blue { +.status-time-red { background-position: -105px -120px; } -.status-up-dark { +.status-time-yellow { background-position: -120px -120px; } -.status-up-green { +.status-up-blue { background-position: -135px -120px; } -.status-up-red { +.status-up-dark { background-position: 0px -135px; } -.status-up-white { +.status-up-green { background-position: -15px -135px; } -.status-up { +.status-up-red { background-position: -30px -135px; } -.status-warning-blue { +.status-up-white { background-position: -45px -135px; } -.status-warning-dark { +.status-up { background-position: -60px -135px; } -.status-warning-green { +.status-warning-blue { background-position: -75px -135px; } -.status-warning-red { +.status-warning-dark { background-position: -90px -135px; } -.status-warning-white { +.status-warning-green { background-position: -105px -135px; } -.status-warning { +.status-warning-red { background-position: -120px -135px; } + +.status-warning-white { + background-position: -135px -135px; +} + +.status-warning { + background-position: 0px -150px; +} diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js index c982f5c3b0..6a3ff7a6e5 100644 --- a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js +++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js @@ -185,10 +185,9 @@ JX.install('TypeaheadSource', { this._raw[obj.id] = obj; var t = this.tokenize(obj.tokenizable || obj.name); for (var jj = 0; jj < t.length; ++jj) { - if (typeof this._lookup[t[jj]] == "function") + if (!this._lookup.hasOwnProperty(t[jj])) { this._lookup[t[jj]] = []; - else - this._lookup[t[jj]] = this._lookup[t[jj]] || []; + } this._lookup[t[jj]].push(obj.id); } }, diff --git a/webroot/rsrc/image/lightblue-header.png b/webroot/rsrc/image/lightblue-header.png new file mode 100644 index 0000000000..b2177d7d7e Binary files /dev/null and b/webroot/rsrc/image/lightblue-header.png differ diff --git a/webroot/rsrc/image/search-white.png b/webroot/rsrc/image/search-white.png new file mode 100644 index 0000000000..98b022ff0f Binary files /dev/null and b/webroot/rsrc/image/search-white.png differ diff --git a/webroot/rsrc/image/sprite-apps-X2.png b/webroot/rsrc/image/sprite-apps-X2.png index 7e5e55f5d3..7c1713f155 100644 Binary files a/webroot/rsrc/image/sprite-apps-X2.png and b/webroot/rsrc/image/sprite-apps-X2.png differ diff --git a/webroot/rsrc/image/sprite-apps-large-X2.png b/webroot/rsrc/image/sprite-apps-large-X2.png index d68e356824..20e15bd66d 100644 Binary files a/webroot/rsrc/image/sprite-apps-large-X2.png and b/webroot/rsrc/image/sprite-apps-large-X2.png differ diff --git a/webroot/rsrc/image/sprite-apps-large.png b/webroot/rsrc/image/sprite-apps-large.png index 4def217a8c..dc81d384fa 100644 Binary files a/webroot/rsrc/image/sprite-apps-large.png and b/webroot/rsrc/image/sprite-apps-large.png differ diff --git a/webroot/rsrc/image/sprite-apps.png b/webroot/rsrc/image/sprite-apps.png index 52165fb46c..a4fdcf5b67 100644 Binary files a/webroot/rsrc/image/sprite-apps.png and b/webroot/rsrc/image/sprite-apps.png differ diff --git a/webroot/rsrc/image/sprite-icons-X2.png b/webroot/rsrc/image/sprite-icons-X2.png index 9ed031b1c5..91be5be190 100644 Binary files a/webroot/rsrc/image/sprite-icons-X2.png and b/webroot/rsrc/image/sprite-icons-X2.png differ diff --git a/webroot/rsrc/image/sprite-icons.png b/webroot/rsrc/image/sprite-icons.png index 3bbbdcfb12..fde10a28b2 100644 Binary files a/webroot/rsrc/image/sprite-icons.png and b/webroot/rsrc/image/sprite-icons.png differ diff --git a/webroot/rsrc/image/sprite-main-header.png b/webroot/rsrc/image/sprite-main-header.png new file mode 100644 index 0000000000..dc5ebe7319 Binary files /dev/null and b/webroot/rsrc/image/sprite-main-header.png differ diff --git a/webroot/rsrc/image/sprite-status-X2.png b/webroot/rsrc/image/sprite-status-X2.png index 82ada07a06..9c6366f070 100644 Binary files a/webroot/rsrc/image/sprite-status-X2.png and b/webroot/rsrc/image/sprite-status-X2.png differ diff --git a/webroot/rsrc/image/sprite-status.png b/webroot/rsrc/image/sprite-status.png index cda769abcf..ba8335c4b5 100644 Binary files a/webroot/rsrc/image/sprite-status.png and b/webroot/rsrc/image/sprite-status.png differ diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js index 5dce1a9a4f..77317a54df 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-dropdown.js @@ -6,6 +6,7 @@ * javelin-vector * javelin-dom * javelin-uri + * javelin-behavior-device */ JX.behavior('aphlict-dropdown', function(config, statics) { @@ -13,10 +14,15 @@ JX.behavior('aphlict-dropdown', function(config, statics) { statics.visible = statics.visible || null; var dropdown = JX.$(config.dropdownID); - var count = JX.$(config.countID); var bubble = JX.$(config.bubbleID); + + var count; + if (config.countID) { + count = JX.$(config.countID); + } + var request = null; - var dirty = true; + var dirty = config.local ? false : true; function refresh() { if (dirty) { @@ -86,6 +92,10 @@ JX.behavior('aphlict-dropdown', function(config, statics) { return; } + if (config.desktop && JX.Device.getDevice() != 'desktop') { + return; + } + e.kill(); // If a menu is currently open, close it. @@ -108,16 +118,24 @@ JX.behavior('aphlict-dropdown', function(config, statics) { } var p = JX.$V(bubble); + JX.DOM.show(dropdown); + p.y = null; - p.x -= 6; + if (config.right) { + p.x -= (JX.Vector.getDim(dropdown).x - JX.Vector.getDim(bubble).x); + } else { + p.x -= 6; + } p.setPos(dropdown); - JX.DOM.show(dropdown); statics.visible = dropdown; } ); JX.Stratcom.listen('notification-panel-update', null, function() { + if (config.local) { + return; + } dirty = true; refresh(); }); diff --git a/webroot/rsrc/js/application/harbormaster/behavior-reorder-steps.js b/webroot/rsrc/js/application/harbormaster/behavior-reorder-steps.js new file mode 100644 index 0000000000..f47bea8ecd --- /dev/null +++ b/webroot/rsrc/js/application/harbormaster/behavior-reorder-steps.js @@ -0,0 +1,42 @@ +/** + * @provides javelin-behavior-harbormaster-reorder-steps + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + * phabricator-draggable-list + */ + +JX.behavior('harbormaster-reorder-steps', function(config) { + + var root = JX.$(config.listID); + + var list = new JX.DraggableList('build-step', root) + .setFindItemsHandler(function() { + return JX.DOM.scry(root, 'li', 'build-step'); + }); + + list.listen('didDrop', function(node, after) { + var nodes = list.findItems(); + var order = []; + var key; + for (var ii = 0; ii < nodes.length; ii++) { + key = JX.Stratcom.getData(nodes[ii]).stepID; + if (key) { + order.push(key); + } + } + + list.lock(); + JX.DOM.alterClass(node, 'drag-sending', true); + + new JX.Workflow(config.orderURI, {order: order.join()}) + .setHandler(function(e) { + JX.DOM.alterClass(node, 'drag-sending', false); + list.unlock(); + }) + .start(); + }); + +}); + diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js index 35b7a01e90..ee3a597fb3 100644 --- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -221,6 +221,7 @@ JX.install('HeraldRuleEditor', { case 'package': case 'project': case 'userorproject': + case 'buildplan': var tokenizer = this._newTokenizer(type); input = tokenizer[0]; get_fn = tokenizer[1]; @@ -232,16 +233,13 @@ JX.install('HeraldRuleEditor', { set_fn = JX.bag; break; case 'contentsource': - input = this._renderSelect(this._config.template.contentSources); - get_fn = function() { return input.value; }; - set_fn = function(v) { input.value = v; }; - set_fn(this._config.template.defaultSource); - break; case 'flagcolor': - input = this._renderSelect(this._config.template.colors); + case 'value-ref-type': + case 'value-ref-change': + input = this._renderSelect(this._config.select[type].options); get_fn = function() { return input.value; }; set_fn = function(v) { input.value = v; }; - set_fn(this._config.template.defaultColor); + set_fn(this._config.select[type]['default']); break; default: input = JX.$N('input', {type: 'text'}); diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js index 6fb3d90aee..31d8da7ba6 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js +++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js @@ -41,6 +41,18 @@ JX.behavior('maniphest-batch-selector', function(config) { update(); }; + var redraw = function (task) { + var selected = is_selected(task); + change(task, selected); + }; + JX.Stratcom.listen( + 'subpriority-changed', + null, + function (e) { + e.kill(); + var data = e.getData(); + redraw(data.task); + }); // Change all tasks to some state (used by "select all" / "clear selection" // buttons). diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js index 9199828397..cfa32e719e 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js +++ b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js @@ -64,7 +64,7 @@ JX.behavior('line-chart', function(config) { if (config.x[0][ii] > this.axis) { break; } - yvalue = format(config.y[yy][ii], (config.yformat || [])[yy]); + yvalue = format(config.y[yy][ii], config.yformat); } var xvalue = format(this.axis, config.xformat); diff --git a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js b/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js index 65052e37b4..047811e91f 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js +++ b/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js @@ -57,8 +57,11 @@ JX.behavior('maniphest-subpriority-editor', function(config) { var nodes = JX.$H(r.tasks).getFragment().firstChild; var task = JX.DOM.find(nodes, 'li', 'maniphest-task'); JX.DOM.replace(node, task); - draggable.unlock(); + JX.Stratcom.invoke( + 'subpriority-changed', + null, + { 'task' : task }); }; new JX.Workflow(config.uri, data) diff --git a/webroot/rsrc/js/application/passphrase/phame-credential-control.js b/webroot/rsrc/js/application/passphrase/phame-credential-control.js new file mode 100644 index 0000000000..6dd20fbd2c --- /dev/null +++ b/webroot/rsrc/js/application/passphrase/phame-credential-control.js @@ -0,0 +1,63 @@ +/** + * @provides javelin-behavior-passphrase-credential-control + * @requires javelin-behavior + * javelin-dom + * javelin-stratcom + * javelin-workflow + * javelin-util + * javelin-uri + * @javelin + */ + +JX.behavior('passphrase-credential-control', function(config) { + + JX.Stratcom.listen( + 'click', + 'passphrase-credential-add', + function(e) { + var control = e.getNode('passphrase-credential-control'); + var data = e.getNodeData('passphrase-credential-control'); + + var uri = JX.$U('/passphrase/edit/'); + uri.setQueryParam('type', data.type); + uri.setQueryParam('username', data.username); + + new JX.Workflow(uri) + .setHandler(JX.bind(null, onadd, control)) + .start(); + + e.kill(); + }); + + function onadd(control, response) { + var select = JX.DOM.find(control, 'select', 'passphrase-credential-select'); + + var data = JX.Stratcom.getData(control); + + // If this allows the user to select "No Credential" (`allowNull`), + // put the new credential in the menu below the "No Credential" option. + + // Otherwise, remove the "(No Existing Credentials)" if it exists and + // put the new credential at the top. + + var target = 0; + for (var ii = 0; ii < select.options.length; ii++) { + if (!select.options[ii].value) { + if (!data.allowNull) { + select.remove(ii); + } else { + target = ii + 1; + } + break; + } + } + + select.add( + JX.$N('option', {value: response.phid}, response.name), + select.options[target] || null); + + select.value = response.phid; + select.disabled = null; + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js new file mode 100644 index 0000000000..478f950d14 --- /dev/null +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -0,0 +1,66 @@ +/** + * @provides javelin-behavior-project-boards + * @requires javelin-behavior + * javelin-dom + * javelin-util + * javelin-stratcom + * javelin-workflow + * phabricator-draggable-list + */ + +JX.behavior('project-boards', function(config) { + + function finditems(col) { + return JX.DOM.scry(col, 'li', 'project-card'); + } + + function onupdate(node) { + JX.DOM.alterClass(node, 'project-column-empty', !this.findItems().length); + } + + function onresponse(response) { + + } + + function ondrop(list, item, after, from) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var data = { + objectPHID: JX.Stratcom.getData(item).objectPHID, + columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID, + afterPHID: after && JX.Stratcom.getData(after).objectPHID + }; + + var workflow = new JX.Workflow(config.moveURI, data) + .setHandler(function(response) { + onresponse(response); + list.unlock(); + + JX.DOM.alterClass(item, 'drag-sending', false); + }); + + workflow.start(); + } + + var lists = []; + var ii; + var cols = JX.DOM.scry(JX.$(config.boardID), 'ul', 'project-column'); + + for (ii = 0; ii < cols.length; ii++) { + var list = new JX.DraggableList('project-card', cols[ii]) + .setFindItemsHandler(JX.bind(null, finditems, cols[ii])); + + list.listen('didSend', JX.bind(list, onupdate, cols[ii])); + list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); + + list.listen('didDrop', JX.bind(null, ondrop, list)); + + lists.push(list); + } + + for (ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + +}); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 87037f1df7..651b4285c2 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -14,6 +14,7 @@ JX.install('DraggableList', { construct : function(sigil, root) { this._sigil = sigil; this._root = root || document.body; + this._group = [this]; // NOTE: Javelin does not dispatch mousemove by default. JX.enableDispatch(document.body, 'mousemove'); @@ -30,7 +31,9 @@ JX.install('DraggableList', { 'didBeginDrag', 'didCancelDrag', 'didEndDrag', - 'didDrop'], + 'didDrop', + 'didSend', + 'didReceive'], properties : { findItemsHandler : null @@ -46,6 +49,11 @@ JX.install('DraggableList', { _dimensions : null, _ghostHandler : null, _ghostNode : null, + _group : null, + + getRootNode : function() { + return this._root; + }, setGhostHandler : function(handler) { this._ghostHandler = handler; @@ -68,8 +76,41 @@ JX.install('DraggableList', { return this; }, + setGroup : function(lists) { + var result = []; + var need_self = true; + for (var ii = 0; ii < lists.length; ii++) { + if (lists[ii] == this) { + need_self = false; + } + result.push(lists[ii]); + } + + if (need_self) { + result.push(this); + } + + this._group = result; + return this; + }, + + _canDragX : function() { + return this._hasGroup(); + }, + + _hasGroup : function() { + return (this._group.length > 1); + }, + _defaultGhostHandler : function(ghost, target) { - var parent = this._dragging.parentNode; + var parent; + + if (!this._hasGroup()) { + parent = this._dragging.parentNode; + } else { + parent = this.getRootNode(); + } + if (target && target.nextSibling) { parent.insertBefore(ghost, target.nextSibling); } else if (!target && parent.firstChild) { @@ -116,6 +157,24 @@ JX.install('DraggableList', { this._origin = JX.$V(e); this._dimensions = JX.$V(this._dragging); + for (var ii = 0; ii < this._group.length; ii++) { + this._group[ii]._clearTarget(); + this._group[ii]._generateTargets(); + } + + if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { + // Set the height of all the ghosts in the group. In the normal case, + // this just sets this list's ghost height. + for (var jj = 0; jj < this._group.length; jj++) { + var ghost = this._group[jj].getGhostNode(); + ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; + } + + JX.DOM.alterClass(this._dragging, 'drag-dragging', true); + } + }, + + _generateTargets : function() { var targets = []; var items = this.findItems(); for (var ii = 0; ii < items.length; ii++) { @@ -126,30 +185,73 @@ JX.install('DraggableList', { } targets.sort(function(u, v) { return v.y - u.y; }); this._targets = targets; - this._target = false; - if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { - var ghost = this.getGhostNode(); - ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; - JX.DOM.alterClass(this._dragging, 'drag-dragging', true); - } + return this; }, - _onmove : function(e) { - if (!this._dragging) { - return; + _getTargetList : function(p) { + var target_list; + if (this._hasGroup()) { + var group = this._group; + for (var ii = 0; ii < group.length; ii++) { + var root = group[ii].getRootNode(); + var rp = JX.$V(root); + var rd = JX.Vector.getDim(root); + + var is_target = false; + if (p.x >= rp.x && p.y >= rp.y) { + if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) { + is_target = true; + target_list = group[ii]; + } + } + + JX.DOM.alterClass(root, 'drag-target-list', is_target); + } + } else { + target_list = this; } + return target_list; + }, + + _setTarget : function(cur_target) { + var ghost = this.getGhostNode(); + var target = this._target; + + if (cur_target !== target) { + this._clearTarget(); + if (cur_target !== false) { + var ok = this.getGhostHandler()(ghost, cur_target); + // If the handler returns explicit `false`, prevent the drag. + if (ok === false) { + cur_target = false; + } + } + + this._target = cur_target; + } + + return this; + }, + + _clearTarget : function() { + var target = this._target; + var ghost = this.getGhostNode(); + + if (target !== false) { + JX.DOM.remove(ghost); + } + + this._target = false; + return this; + }, + + _getCurrentTarget : function(p) { var ghost = this.getGhostNode(); var target = this._target; var targets = this._targets; var dragging = this._dragging; - var origin = this._origin; - - var p = JX.$V(e); - - // Compute the size and position of the drop target indicator, because we - // need to update our static position computations to account for it. var adjust_h = JX.Vector.getDim(ghost).y; var adjust_y = JX.$V(ghost).y; @@ -187,44 +289,59 @@ JX.install('DraggableList', { // Don't choose the dragged row or its predecessor as targets. cur_target = targets[ii].item; - if (cur_target == dragging) { - cur_target = false; - } - if (targets[ii - 1] && targets[ii - 1].item == dragging) { - cur_target = false; + if (!dragging) { + // If the item on the cursor isn't from this list, it can't be + // dropped onto itself or its predecessor in this list. + } else { + if (cur_target == dragging) { + cur_target = false; + } + if (targets[ii - 1] && targets[ii - 1].item == dragging) { + cur_target = false; + } } break; } + // If the dragged row is the first row, don't allow it to be dragged + // into the first position, since this operation doesn't make sense. + if (dragging && cur_target === null) { + var first_item = targets[targets.length - 1].item; + if (dragging === first_item) { + cur_target = false; + } + } + + return cur_target; + }, + + _onmove : function(e) { + if (!this._dragging) { + return; + } + + var p = JX.$V(e); + + var group = this._group; + var target_list = this._getTargetList(p); + + // Compute the size and position of the drop target indicator, because we + // need to update our static position computations to account for it. + + var cur_target = false; + if (target_list) { + cur_target = target_list._getCurrentTarget(p); + } + // If we've selected a new target, update the UI to show where we're // going to drop the row. - if (cur_target != target) { - - if (target) { - JX.DOM.remove(ghost); - } - - if (cur_target !== false) { - var ok = this.getGhostHandler()(ghost, cur_target); - // If the handler returns explicit `false`, prevent the drag. - if (ok === false) { - cur_target = false; - } - } - - target = cur_target; - - if (target !== false) { - - // If we've changed where the ghost node is, update the adjustments - // so we accurately reflect document state when we tweak things below. - // This avoids a flash of bad state as the mouse is dragged upward - // across the document. - - adjust_h = JX.Vector.getDim(ghost).y; - adjust_y = JX.$V(ghost).y; + for (var ii = 0; ii < group.length; ii++) { + if (group[ii] == target_list) { + group[ii]._setTarget(cur_target); + } else { + group[ii]._clearTarget(); } } @@ -232,16 +349,28 @@ JX.install('DraggableList', { // adjust the cursor position for the change in node document position. // Do this before choosing a new target to avoid a flash of nonsense. - if (target !== false) { + var origin = this._origin; + + var adjust_h = 0; + var adjust_y = 0; + if (this._target !== false) { + var ghost = this.getGhostNode(); + adjust_h = JX.Vector.getDim(ghost).y; + adjust_y = JX.$V(ghost).y; + if (adjust_y <= origin.y) { p.y -= adjust_h; } } - p.x = 0; + if (this._canDragX()) { + p.x -= origin.x; + } else { + p.x = 0; + } + p.y -= origin.y; - p.setPos(dragging); - this._target = target; + p.setPos(this._dragging); e.kill(); }, @@ -251,22 +380,38 @@ JX.install('DraggableList', { return; } - var target = this._target; - var dragging = this._dragging; - var ghost = this.getGhostNode(); + var p = JX.$V(e); + var dragging = this._dragging; this._dragging = null; + var target = false; + var ghost = false; + + var target_list = this._getTargetList(p); + if (target_list) { + target = target_list._target; + ghost = target_list.getGhostNode(); + } + JX.$V(0, 0).setPos(dragging); if (target !== false) { JX.DOM.remove(dragging); JX.DOM.replace(ghost, dragging); - this.invoke('didDrop', dragging, target); + this.invoke('didSend', dragging, target_list); + target_list.invoke('didReceive', dragging, this); + target_list.invoke('didDrop', dragging, target, this); } else { this.invoke('didCancelDrag', dragging); } + var group = this._group; + for (var ii = 0; ii < group.length; ii++) { + JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); + group[ii]._clearTarget(); + } + if (!this.invoke('didEndDrag', dragging).getPrevented()) { JX.DOM.alterClass(dragging, 'drag-dragging', false); } @@ -275,6 +420,13 @@ JX.install('DraggableList', { }, lock : function() { + for (var ii = 0; ii < this._group.length; ii++) { + this._group[ii]._lock(); + } + return this; + }, + + _lock : function() { this._locked++; if (this._locked === 1) { this.invoke('didLock'); @@ -282,7 +434,14 @@ JX.install('DraggableList', { return this; }, - unlock : function() { + unlock: function() { + for (var ii = 0; ii < this._group.length; ii++) { + this._group[ii]._unlock(); + } + return this; + }, + + _unlock : function() { if (__DEV__) { if (!this._locked) { JX.$E("JX.Draggable.unlock(): Draggable is not locked!"); diff --git a/webroot/rsrc/js/core/DropdownMenu.js b/webroot/rsrc/js/core/DropdownMenu.js index f298acd8c8..490e92e223 100644 --- a/webroot/rsrc/js/core/DropdownMenu.js +++ b/webroot/rsrc/js/core/DropdownMenu.js @@ -77,6 +77,8 @@ JX.install('PhabricatorDropdownMenu', { this._open = true; this._show(); + + return this; }, close : function() { @@ -85,6 +87,8 @@ JX.install('PhabricatorDropdownMenu', { } this._open = false; this._hide(); + + return this; }, clear : function() { diff --git a/webroot/rsrc/js/core/behavior-object-selector.js b/webroot/rsrc/js/core/behavior-object-selector.js index 8affe37c43..23cdeafe54 100644 --- a/webroot/rsrc/js/core/behavior-object-selector.js +++ b/webroot/rsrc/js/core/behavior-object-selector.js @@ -61,10 +61,16 @@ JX.behavior('phabricator-object-selector', function(config) { function renderHandle(h, attach) { + var some_icon = JX.$N( + 'span', + {className: 'phui-icon-view sprite-icons ' + + 'icons-popout phabricator-object-selector-popicon'}, + ""); + var view_object_link = JX.$N( 'a', {href: h.uri, target: '_blank'}, - "\u2197"); + some_icon); var select_object_link = JX.$N( 'a', @@ -89,7 +95,9 @@ JX.behavior('phabricator-object-selector', function(config) { table.appendChild( JX.$N( 'tr', - {sigil: 'object-attach-row', meta: {handle: h, table:table}}, + {sigil: 'object-attach-row', + className: 'phabricator-object-selector-row', + meta: {handle: h, table:table}}, cells)); if (attach) {