diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index f27b4d86a..d966240f1 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -1,200 +1,199 @@ #!/usr/bin/env php loadOneWhere( 'username = %s', $username); if (!$user) { $original = new PhabricatorUser(); echo "There is no existing user account '{$username}'.\n"; $ok = phutil_console_confirm( "Do you want to create a new '{$username}' account?", $default_no = false); if (!$ok) { echo "Cancelled.\n"; exit(1); } $user = new PhabricatorUser(); $user->setUsername($username); $is_new = true; } else { $original = clone $user; echo "There is an existing user account '{$username}'.\n"; $ok = phutil_console_confirm( "Do you want to edit the existing '{$username}' account?", $default_no = false); if (!$ok) { echo "Cancelled.\n"; exit(1); } $is_new = false; } $user_realname = $user->getRealName(); if (strlen($user_realname)) { $realname_prompt = ' ['.$user_realname.']'; } else { $realname_prompt = ''; } $realname = nonempty( phutil_console_prompt("Enter user real name{$realname_prompt}:"), $user_realname); $user->setRealName($realname); // When creating a new user we prompt for an email address; when editing an // existing user we just skip this because it would be quite involved to provide // a reasonable CLI interface for editing multiple addresses and managing email // verification and primary addresses. $create_email = null; if ($is_new) { do { $email = phutil_console_prompt("Enter user email address:"); $duplicate = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $email); if ($duplicate) { echo "ERROR: There is already a user with that email address. ". "Each user must have a unique email address.\n"; } else { break; } } while (true); $create_email = $email; } $changed_pass = false; // This disables local echo, so the user's password is not shown as they type // it. phutil_passthru('stty -echo'); $password = phutil_console_prompt( "Enter a password for this user [blank to leave unchanged]:"); phutil_passthru('stty echo'); if (strlen($password)) { $changed_pass = $password; } $is_system_agent = $user->getIsSystemAgent(); $set_system_agent = phutil_console_confirm( 'Should this user be a system agent?', $default_no = !$is_system_agent); $verify_email = null; $set_verified = false; // Allow administrators to verify primary email addresses at this time in edit // scenarios. (Create will work just fine from here as we auto-verify email // on create.) if (!$is_new) { $verify_email = $user->loadPrimaryEmail(); if (!$verify_email->getIsVerified()) { $set_verified = phutil_console_confirm( 'Should the primary email address be verified?', - $default_no = true - ); + $default_no = true); } else { // already verified so let's not make a fuss $verify_email = null; } } $is_admin = $user->getIsAdmin(); $set_admin = phutil_console_confirm( 'Should this user be an administrator?', $default_no = !$is_admin); echo "\n\nACCOUNT SUMMARY\n\n"; $tpl = "%12s %-30s %-30s\n"; printf($tpl, null, 'OLD VALUE', 'NEW VALUE'); printf($tpl, 'Username', $original->getUsername(), $user->getUsername()); printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName()); if ($is_new) { printf($tpl, 'Email', '', $create_email); } printf($tpl, 'Password', null, ($changed_pass !== false) ? 'Updated' : 'Unchanged'); printf( $tpl, 'System Agent', $original->getIsSystemAgent() ? 'Y' : 'N', $set_system_agent ? 'Y' : 'N'); if ($verify_email) { printf( $tpl, 'Verify Email', $verify_email->getIsVerified() ? 'Y' : 'N', $set_verified ? 'Y' : 'N'); } printf( $tpl, 'Admin', $original->getIsAdmin() ? 'Y' : 'N', $set_admin ? 'Y' : 'N'); echo "\n"; if (!phutil_console_confirm("Save these changes?", $default_no = false)) { echo "Cancelled.\n"; exit(1); } $user->openTransaction(); $editor = new PhabricatorUserEditor(); // TODO: This is wrong, but we have a chicken-and-egg problem when you use // this script to create the first user. $editor->setActor($user); if ($is_new) { $email = id(new PhabricatorUserEmail()) ->setAddress($create_email) ->setIsVerified(1); $editor->createNewUser($user, $email); } else { if ($verify_email) { $verify_email->setIsVerified($set_verified ? 1 : 0); } $editor->updateUser($user, $verify_email); } $editor->makeAdminUser($user, $set_admin); $editor->makeSystemAgentUser($user, $set_system_agent); if ($changed_pass !== false) { $envelope = new PhutilOpaqueEnvelope($changed_pass); $editor->changePassword($user, $envelope); } $user->saveTransaction(); echo "Saved changes.\n"; diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php index 5d2f9a622..d6a325513 100644 --- a/src/applications/differential/parser/DifferentialHunkParser.php +++ b/src/applications/differential/parser/DifferentialHunkParser.php @@ -1,652 +1,652 @@ Map of lines where hunks start, other than line 1. */ public function getHunkStartLines(array $hunks) { assert_instances_of($hunks, 'DifferentialHunk'); $map = array(); foreach ($hunks as $hunk) { $line = $hunk->getOldOffset(); if ($line > 1) { $map[$line] = true; } } return $map; } private function setVisibleLinesMask($mask) { $this->visibleLinesMask = $mask; return $this; } public function getVisibleLinesMask() { if ($this->visibleLinesMask === null) { throw new Exception( 'You must generateVisibileLinesMask before accessing this data.' ); } return $this->visibleLinesMask; } private function setIntraLineDiffs($intra_line_diffs) { $this->intraLineDiffs = $intra_line_diffs; return $this; } public function getIntraLineDiffs() { if ($this->intraLineDiffs === null) { throw new Exception( 'You must generateIntraLineDiffs before accessing this data.' ); } return $this->intraLineDiffs; } private function setNewLines($new_lines) { $this->newLines = $new_lines; return $this; } public function getNewLines() { if ($this->newLines === null) { throw new Exception( 'You must parseHunksForLineData before accessing this data.' ); } return $this->newLines; } private function setOldLines($old_lines) { $this->oldLines = $old_lines; return $this; } public function getOldLines() { if ($this->oldLines === null) { throw new Exception( 'You must parseHunksForLineData before accessing this data.' ); } return $this->oldLines; } public function getOldLineTypeMap() { $map = array(); $old = $this->getOldLines(); foreach ($old as $o) { if (!$o) { continue; } $map[$o['line']] = $o['type']; } return $map; } public function setOldLineTypeMap(array $map) { $lines = $this->getOldLines(); foreach ($lines as $key => $data) { $lines[$key]['type'] = $map[$data['line']]; } $this->oldLines = $lines; return $this; } public function getNewLineTypeMap() { $map = array(); $new = $this->getNewLines(); foreach ($new as $n) { if (!$n) { continue; } $map[$n['line']] = $n['type']; } return $map; } public function setNewLineTypeMap(array $map) { $lines = $this->getNewLines(); foreach ($lines as $key => $data) { $lines[$key]['type'] = $map[$data['line']]; } $this->newLines = $lines; return $this; } public function setWhitespaceMode($white_space_mode) { $this->whitespaceMode = $white_space_mode; return $this; } private function getWhitespaceMode() { if ($this->whitespaceMode === null) { throw new Exception( 'You must setWhitespaceMode before accessing this data.' ); } return $this->whitespaceMode; } public function getIsDeleted() { foreach ($this->getNewLines() as $line) { if ($line) { // At least one new line, so the entire file wasn't deleted. return false; } } foreach ($this->getOldLines() as $line) { if ($line) { // No new lines, at least one old line; the entire file was deleted. return true; } } // This is an empty file. return false; } /** * Returns true if the hunks change any text, not just whitespace. */ public function getHasTextChanges() { return $this->getHasChanges('text'); } /** * Returns true if the hunks change anything, including whitespace. */ public function getHasAnyChanges() { return $this->getHasChanges('any'); } private function getHasChanges($filter) { if ($filter !== 'any' && $filter !== 'text') { throw new Exception("Unknown change filter '{$filter}'."); } $old = $this->getOldLines(); $new = $this->getNewLines(); $is_any = ($filter === 'any'); foreach ($old as $key => $o) { $n = $new[$key]; if ($o === null || $n === null) { // One side is missing, and it's impossible for both sides to be null, // so the other side must have something, and thus the two sides are // different and the file has been changed under any type of filter. return true; } if ($o['type'] !== $n['type']) { // The types are different, so either the underlying text is actually // different or whatever whitespace rules we're using consider them // different. return true; } if ($o['text'] !== $n['text']) { if ($is_any) { // The text is different, so there's a change. return true; } else if (trim($o['text']) !== trim($n['text'])) { return true; } } } // No changes anywhere in the file. return false; } /** * This function takes advantage of the parsing work done in * @{method:parseHunksForLineData} and continues the struggle to hammer this * data into something we can display to a user. * * In particular, this function re-parses the hunks to make them equivalent * in length for easy rendering, adding `null` as necessary to pad the * length. * * Anyhoo, this function is not particularly well-named but I try. * * NOTE: this function must be called after * @{method:parseHunksForLineData}. */ public function reparseHunksForSpecialAttributes() { $rebuild_old = array(); $rebuild_new = array(); $old_lines = array_reverse($this->getOldLines()); $new_lines = array_reverse($this->getNewLines()); while (count($old_lines) || count($new_lines)) { $old_line_data = array_pop($old_lines); $new_line_data = array_pop($new_lines); if ($old_line_data) { $o_type = $old_line_data['type']; } else { $o_type = null; } if ($new_line_data) { $n_type = $new_line_data['type']; } else { $n_type = null; } // This line does not exist in the new file. if (($o_type != null) && ($n_type == null)) { $rebuild_old[] = $old_line_data; $rebuild_new[] = null; if ($new_line_data) { array_push($new_lines, $new_line_data); } continue; } // This line does not exist in the old file. if (($n_type != null) && ($o_type == null)) { $rebuild_old[] = null; $rebuild_new[] = $new_line_data; if ($old_line_data) { array_push($old_lines, $old_line_data); } continue; } $rebuild_old[] = $old_line_data; $rebuild_new[] = $new_line_data; } $this->setOldLines($rebuild_old); $this->setNewLines($rebuild_new); $this->updateChangeTypesForWhitespaceMode(); return $this; } private function updateChangeTypesForWhitespaceMode() { $mode = $this->getWhitespaceMode(); $mode_show_all = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; if ($mode === $mode_show_all) { // If we're showing all whitespace, we don't need to perform any updates. return; } $mode_trailing = DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING; $is_trailing = ($mode === $mode_trailing); $new = $this->getNewLines(); $old = $this->getOldLines(); foreach ($old as $key => $o) { $n = $new[$key]; if (!$o || !$n) { continue; } if ($is_trailing) { // In "trailing" mode, we need to identify lines which are marked // changed but differ only by trailing whitespace. We mark these lines // unchanged. if ($o['type'] != $n['type']) { if (rtrim($o['text']) === rtrim($n['text'])) { $old[$key]['type'] = null; $new[$key]['type'] = null; } } } else { // In "ignore most" and "ignore all" modes, we need to identify lines // which are marked unchanged but have internal whitespace changes. // We want to ignore leading and trailing whitespace changes only, not // internal whitespace changes (`diff` doesn't have a mode for this, so // we have to fix it here). If the text is marked unchanged but the // old and new text differs by internal space, mark the lines changed. if ($o['type'] === null && $n['type'] === null) { if ($o['text'] !== $n['text']) { if (trim($o['text']) !== trim($n['text'])) { $old[$key]['type'] = '-'; $new[$key]['type'] = '+'; } } } } } $this->setOldLines($old); $this->setNewLines($new); return $this; } public function generateIntraLineDiffs() { $old = $this->getOldLines(); $new = $this->getNewLines(); $diffs = array(); foreach ($old as $key => $o) { $n = $new[$key]; if (!$o || !$n) { continue; } if ($o['type'] != $n['type']) { $diffs[$key] = ArcanistDiffUtils::generateIntralineDiff( $o['text'], $n['text']); } } $this->setIntraLineDiffs($diffs); return $this; } public function generateVisibileLinesMask() { $lines_context = DifferentialChangesetParser::LINES_CONTEXT; $old = $this->getOldLines(); $new = $this->getNewLines(); $max_length = max(count($old), count($new)); $visible = false; $last = 0; $mask = array(); for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) { $offset = $cursor + $lines_context; if ((isset($old[$offset]) && $old[$offset]['type']) || (isset($new[$offset]) && $new[$offset]['type'])) { $visible = true; $last = $offset; } else if ($cursor > $last + $lines_context) { $visible = false; } if ($visible && $cursor > 0) { $mask[$cursor] = 1; } } $this->setVisibleLinesMask($mask); return $this; } public function getOldCorpus() { return $this->getCorpus($this->getOldLines()); } public function getNewCorpus() { return $this->getCorpus($this->getNewLines()); } private function getCorpus(array $lines) { $corpus = array(); foreach ($lines as $l) { if ($l['type'] != '\\') { if ($l['text'] === null) { // There's no text on this side of the diff, but insert a placeholder // newline so the highlighted line numbers match up. $corpus[] = "\n"; } else { $corpus[] = $l['text']; } } } return $corpus; } public function parseHunksForLineData(array $hunks) { assert_instances_of($hunks, 'DifferentialHunk'); $old_lines = array(); $new_lines = array(); foreach ($hunks as $hunk) { $lines = $hunk->getChanges(); $lines = phutil_split_lines($lines); $line_type_map = array(); foreach ($lines as $line_index => $line) { if (isset($line[0])) { $char = $line[0]; if ($char == ' ') { $line_type_map[$line_index] = null; } else { $line_type_map[$line_index] = $char; } } else { $line_type_map[$line_index] = null; } } $old_line = $hunk->getOldOffset(); $new_line = $hunk->getNewOffset(); $num_lines = count($lines); for ($cursor = 0; $cursor < $num_lines; $cursor++) { $type = $line_type_map[$cursor]; $data = array( 'type' => $type, 'text' => (string)substr($lines[$cursor], 1), 'line' => $new_line, ); if ($type == '\\') { $type = $line_type_map[$cursor - 1]; $data['text'] = ltrim($data['text']); } switch ($type) { case '+': $new_lines[] = $data; ++$new_line; break; case '-': $data['line'] = $old_line; $old_lines[] = $data; ++$old_line; break; default: $new_lines[] = $data; $data['line'] = $old_line; $old_lines[] = $data; ++$new_line; ++$old_line; break; } } } $this->setOldLines($old_lines); $this->setNewLines($new_lines); return $this; } public function parseHunksForHighlightMasks( array $changeset_hunks, array $old_hunks, array $new_hunks) { assert_instances_of($changeset_hunks, 'DifferentialHunk'); assert_instances_of($old_hunks, 'DifferentialHunk'); assert_instances_of($new_hunks, 'DifferentialHunk'); // Put changes side by side. $olds = array(); $news = array(); foreach ($changeset_hunks as $hunk) { $n_old = $hunk->getOldOffset(); $n_new = $hunk->getNewOffset(); $changes = phutil_split_lines($hunk->getChanges()); foreach ($changes as $line) { $diff_type = $line[0]; // Change type in diff of diffs. $orig_type = $line[1]; // Change type in the original diff. if ($diff_type == ' ') { // Use the same key for lines that are next to each other. $key = max(last_key($olds), last_key($news)) + 1; $olds[$key] = null; $news[$key] = null; } else if ($diff_type == '-') { $olds[] = array($n_old, $orig_type); } else if ($diff_type == '+') { $news[] = array($n_new, $orig_type); } if (($diff_type == '-' || $diff_type == ' ') && $orig_type != '-') { $n_old++; } if (($diff_type == '+' || $diff_type == ' ') && $orig_type != '-') { $n_new++; } } } $offsets_old = $this->computeOffsets($old_hunks); $offsets_new = $this->computeOffsets($new_hunks); // Highlight lines that were added on each side or removed on the other // side. $highlight_old = array(); $highlight_new = array(); $last = max(last_key($olds), last_key($news)); for ($i = 0; $i <= $last; $i++) { if (isset($olds[$i])) { list($n, $type) = $olds[$i]; if ($type == '+' || ($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) { $highlight_old[] = $offsets_old[$n]; } } if (isset($news[$i])) { list($n, $type) = $news[$i]; if ($type == '+' || ($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) { $highlight_new[] = $offsets_new[$n]; } } } return array($highlight_old, $highlight_new); } public function makeContextDiff( array $hunks, PhabricatorInlineCommentInterface $inline, $add_context) { assert_instances_of($hunks, 'DifferentialHunk'); $context = array(); $debug = false; if ($debug) { $context[] = 'Inline: '.$inline->getIsNewFile().' '. $inline->getLineNumber().' '.$inline->getLineLength(); foreach ($hunks as $hunk) { $context[] = 'hunk: '.$hunk->getOldOffset().'-'. $hunk->getOldLen().'; '.$hunk->getNewOffset().'-'.$hunk->getNewLen(); $context[] = $hunk->getChanges(); } } if ($inline->getIsNewFile()) { $prefix = '+'; } else { $prefix = '-'; } foreach ($hunks as $hunk) { if ($inline->getIsNewFile()) { $offset = $hunk->getNewOffset(); $length = $hunk->getNewLen(); } else { $offset = $hunk->getOldOffset(); $length = $hunk->getOldLen(); } $start = $inline->getLineNumber() - $offset; $end = $start + $inline->getLineLength(); // We need to go in if $start == $length, because the last line // might be a "\No newline at end of file" marker, which we want // to show if the additional context is > 0. if ($start <= $length && $end >= 0) { $start = $start - $add_context; $end = $end + $add_context; $hunk_content = array(); $hunk_pos = array( "-" => 0, "+" => 0 ); - $hunk_offset = array( "-" => NULL, "+" => NULL ); - $hunk_last = array( "-" => NULL, "+" => NULL ); + $hunk_offset = array( "-" => null, "+" => null ); + $hunk_last = array( "-" => null, "+" => null ); foreach (explode("\n", $hunk->getChanges()) as $line) { $in_common = strncmp($line, " ", 1) === 0; $in_old = strncmp($line, "-", 1) === 0 || $in_common; $in_new = strncmp($line, "+", 1) === 0 || $in_common; $in_selected = strncmp($line, $prefix, 1) === 0; $skip = !$in_selected && !$in_common; if ($hunk_pos[$prefix] <= $end) { if ($start <= $hunk_pos[$prefix]) { if (!$skip || ($hunk_pos[$prefix] != $start && $hunk_pos[$prefix] != $end)) { if ($in_old) { - if ($hunk_offset["-"] === NULL) { + if ($hunk_offset["-"] === null) { $hunk_offset["-"] = $hunk_pos["-"]; } $hunk_last["-"] = $hunk_pos["-"]; } if ($in_new) { - if ($hunk_offset["+"] === NULL) { + if ($hunk_offset["+"] === null) { $hunk_offset["+"] = $hunk_pos["+"]; } $hunk_last["+"] = $hunk_pos["+"]; } $hunk_content[] = $line; } } if ($in_old) { ++$hunk_pos["-"]; } if ($in_new) { ++$hunk_pos["+"]; } } } - if ($hunk_offset["-"] !== NULL || $hunk_offset["+"] !== NULL) { + if ($hunk_offset["-"] !== null || $hunk_offset["+"] !== null) { $header = "@@"; - if ($hunk_offset["-"] !== NULL) { + if ($hunk_offset["-"] !== null) { $header .= " -" . ($hunk->getOldOffset() + $hunk_offset["-"]) . "," . ($hunk_last["-"] - $hunk_offset["-"] + 1); } - if ($hunk_offset["+"] !== NULL) { + if ($hunk_offset["+"] !== null) { $header .= " +" . ($hunk->getNewOffset() + $hunk_offset["+"]) . "," . ($hunk_last["+"] - $hunk_offset["+"] + 1); } $header .= " @@"; $context[] = $header; $context[] = implode("\n", $hunk_content); } } } return implode("\n", $context); } private function computeOffsets(array $hunks) { assert_instances_of($hunks, 'DifferentialHunk'); $offsets = array(); $n = 1; foreach ($hunks as $hunk) { for ($i = 0; $i < $hunk->getNewLen(); $i++) { $offsets[$n] = $hunk->getNewOffset() + $i; $n++; } } return $offsets; } } diff --git a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php index cfec9d9c5..4f5aa2b17 100644 --- a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php +++ b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php @@ -1,274 +1,274 @@ createComment(); - $comment->setIsNewFile(True); + $comment->setIsNewFile(true); $comment->setLineNumber($line); $comment->setLineLength($length); return $comment; } // $line: 1 based // $length: 0 based (0 meaning 1 line) private function createOldComment($line, $length) { $comment = $this->createComment(); - $comment->setIsNewFile(False); + $comment->setIsNewFile(false); $comment->setLineNumber($line); $comment->setLineLength($length); return $comment; } private function createHunk($oldOffset, $oldLen, $newOffset, $newLen, $changes) { $hunk = new DifferentialHunk(); $hunk->setOldOffset($oldOffset); $hunk->setOldLen($oldLen); $hunk->setNewOffset($newOffset); $hunk->setNewLen($newLen); $hunk->setChanges($changes); return $hunk; } // Returns a change that consists of a single hunk, starting at line 1. private function createSingleChange($old_lines, $new_lines, $changes) { return array( 0 => $this->createHunk(1, $old_lines, 1, $new_lines, $changes), ); } private function createHunksFromFile($name) { $data = Filesystem::readFile(dirname(__FILE__).'/data/'.$name); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($data); if (count($changes) !== 1) { throw new Exception("Expected 1 changeset for '{$name}'!"); } $diff = DifferentialDiff::newFromRawChanges($changes); return head($diff->getChangesets())->getHunks(); } public function testOneLineOldComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(1, 0, "-a"); $context = $parser->makeContextDiff( $hunks, $this->createOldComment(1, 0), 0); $this->assertEqual("@@ -1,1 @@\n-a", $context); } public function testOneLineNewComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a"); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(1, 0), 0); $this->assertEqual("@@ +1,1 @@\n+a", $context); } public function testCannotFindContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a"); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(2, 0), 0); $this->assertEqual("", $context); } public function testOverlapFromStartOfHunk() { $parser = new DifferentialHunkParser(); $hunks = array( 0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"), ); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(41, 1), 0); $this->assertEqual("@@ -23,1 +42,1 @@\n 1", $context); } public function testOverlapAfterEndOfHunk() { $parser = new DifferentialHunkParser(); $hunks = array( 0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"), ); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(43, 1), 0); $this->assertEqual("@@ -24,1 +43,1 @@\n 2", $context); } public function testInclusionOfNewFileInOldCommentFromStart() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 3, "+n1\n". " e1/2\n". "-o2\n". "+n3\n"); $context = $parser->makeContextDiff( $hunks, $this->createOldComment(1, 1), 0); $this->assertEqual( "@@ -1,2 +2,1 @@\n". " e1/2\n". "-o2", $context); } public function testInclusionOfOldFileInNewCommentFromStart() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 2, "-o1\n". " e2/1\n". "-o3\n". "+n2\n"); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(1, 1), 0); $this->assertEqual( "@@ -2,1 +1,2 @@\n". " e2/1\n". "+n2", $context); } public function testNoNewlineAtEndOfFile() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a\n". "\\No newline at end of file"); // Note that this only works with additional context. $context = $parser->makeContextDiff( $hunks, $this->createNewComment(2, 0), 1); $this->assertEqual( "@@ +1,1 @@\n". "+a\n". "\\No newline at end of file", $context); } public function testMultiLineNewComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(7, 7, " e1\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6\n". " e7\n"); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(2, 4), 0); $this->assertEqual( "@@ -2,5 +2,5 @@\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6", $context); } public function testMultiLineOldComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(7, 7, " e1\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6\n". " e7\n"); $context = $parser->makeContextDiff( $hunks, $this->createOldComment(2, 4), 0); $this->assertEqual( "@@ -2,5 +2,4 @@\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5", $context); } public function testInclusionOfNewFileInOldCommentFromStartWithContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 3, "+n1\n". " e1/2\n". "-o2\n". "+n3\n"); $context = $parser->makeContextDiff( $hunks, $this->createOldComment(1, 1), 1); $this->assertEqual( "@@ -1,2 +1,2 @@\n". "+n1\n". " e1/2\n". "-o2", $context); } public function testInclusionOfOldFileInNewCommentFromStartWithContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 2, "-o1\n". " e2/1\n". "-o3\n". "+n2\n"); $context = $parser->makeContextDiff( $hunks, $this->createNewComment(1, 1), 1); $this->assertEqual( "@@ -1,3 +1,2 @@\n". "-o1\n". " e2/1\n". "-o3\n". "+n2", $context); } public function testMissingContext() { $tests = array( 'missing_context.diff' => array( 4 => true, ), 'missing_context_2.diff' => array( 5 => true, ), 'missing_context_3.diff' => array( 4 => true, 13 => true, ), ); foreach ($tests as $name => $expect) { $hunks = $this->createHunksFromFile($name); $parser = new DifferentialHunkParser(); $actual = $parser->getHunkStartLines($hunks); $this->assertEqual($expect, $actual, $name); } } } diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php index a7114c856..cb8513206 100644 --- a/src/applications/phid/PhabricatorObjectHandle.php +++ b/src/applications/phid/PhabricatorObjectHandle.php @@ -1,219 +1,219 @@ uri = $uri; return $this; } public function getURI() { return $this->uri; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function getPHID() { return $this->phid; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setStatus($status) { $this->status = $status; return $this; } public function getStatus() { return $this->status; } public function setFullName($full_name) { $this->fullName = $full_name; return $this; } public function getFullName() { if ($this->fullName !== null) { return $this->fullName; } return $this->getName(); } - + public function setTitle($title) { $this->title = $title; return $this; } - + public function getTitle() { return $this->title; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setImageURI($uri) { $this->imageURI = $uri; return $this; } public function getImageURI() { return $this->imageURI; } public function setTimestamp($timestamp) { $this->timestamp = $timestamp; return $this; } public function getTimestamp() { return $this->timestamp; } public function getTypeName() { static $map = array( PhabricatorPHIDConstants::PHID_TYPE_USER => 'User', PhabricatorPHIDConstants::PHID_TYPE_TASK => 'Task', PhabricatorPHIDConstants::PHID_TYPE_DREV => 'Revision', PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'Commit', PhabricatorPHIDConstants::PHID_TYPE_WIKI => 'Phriction Document', PhabricatorPHIDConstants::PHID_TYPE_MCRO => 'Image Macro', PhabricatorPHIDConstants::PHID_TYPE_MOCK => 'Pholio Mock', PhabricatorPHIDConstants::PHID_TYPE_FILE => 'File', PhabricatorPHIDConstants::PHID_TYPE_BLOG => 'Blog', PhabricatorPHIDConstants::PHID_TYPE_POST => 'Post', PhabricatorPHIDConstants::PHID_TYPE_QUES => 'Question', PhabricatorPHIDConstants::PHID_TYPE_PVAR => 'Variable', PhabricatorPHIDConstants::PHID_TYPE_PSTE => 'Paste', PhabricatorPHIDConstants::PHID_TYPE_PROJ => 'Project', ); return idx($map, $this->getType(), $this->getType()); } /** * Set whether or not the underlying object is complete. See * @{method:isComplete} for an explanation of what it means to be complete. * * @param bool True if the handle represents a complete object. * @return this */ public function setComplete($complete) { $this->complete = $complete; return $this; } /** * Determine if the handle represents an object which was completely loaded * (i.e., the underlying object exists) vs an object which could not be * completely loaded (e.g., the type or data for the PHID could not be * identified or located). * * Basically, @{class:PhabricatorObjectHandleData} gives you back a handle for * any PHID you give it, but it gives you a complete handle only for valid * PHIDs. * * @return bool True if the handle represents a complete object. */ public function isComplete() { return $this->complete; } /** * Set whether or not the underlying object is disabled. See * @{method:isDisabled} for an explanation of what it means to be disabled. * * @param bool True if the handle represents a disabled object. * @return this */ public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } /** * Determine if the handle represents an object which has been disabled -- * for example, disabled users, archived projects, etc. These objects are * complete and exist, but should be excluded from some system interactions * (for instance, they usually should not appear in typeaheads, and should * not have mail/notifications delivered to or about them). * * @return bool True if the handle represents a disabled object. */ public function isDisabled() { return $this->disabled; } public function renderLink($name = null) { if ($name === null) { $name = $this->getLinkName(); } $class = null; $title = $this->title; if ($this->status != PhabricatorObjectHandleStatus::STATUS_OPEN) { $class .= ' handle-status-'.$this->status; $title = $title ? $title : $this->status; } if ($this->disabled) { $class .= ' handle-disabled'; $title = 'disabled'; // Overwrite status. } return phutil_tag( 'a', array( 'href' => $this->getURI(), 'class' => $class, 'title' => $title, ), $name); } public function getLinkName() { switch ($this->getType()) { case PhabricatorPHIDConstants::PHID_TYPE_USER: $name = $this->getName(); break; default: $name = $this->getFullName(); break; } return $name; } } diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php index eadfda86b..d00d852a3 100644 --- a/src/applications/pholio/editor/PholioMockEditor.php +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -1,173 +1,173 @@ getTransactionType()) { case PholioTransactionType::TYPE_NAME: return $object->getName(); case PholioTransactionType::TYPE_DESCRIPTION: return $object->getDescription(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PholioTransactionType::TYPE_NAME: case PholioTransactionType::TYPE_DESCRIPTION: return $xaction->getNewValue(); } } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PholioTransactionType::TYPE_INLINE: return true; } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PholioTransactionType::TYPE_NAME: $object->setName($xaction->getNewValue()); if ($object->getOriginalName() === null) { $object->setOriginalName($xaction->getNewValue()); } break; case PholioTransactionType::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PholioTransactionType::TYPE_NAME: case PholioTransactionType::TYPE_DESCRIPTION: return $v; } return parent::mergeTransactions($u, $v); } protected function supportsMail() { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PholioReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); $original_name = $object->getOriginalName(); return id(new PhabricatorMetaMTAMail()) ->setSubject("M{$id}: {$name}") ->addHeader('Thread-Topic', "M{$id}: {$original_name}"); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getAuthorPHID(), $this->requireActor()->getPHID(), ); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $body->addTextSection( pht('MOCK DETAIL'), PhabricatorEnv::getProductionURI('/M'.$object->getID())); return $body; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix'); } protected function supportsFeed() { return true; } protected function supportsSearch() { return true; } protected function sortTransactions(array $xactions) { $head = array(); $tail = array(); - // Move inline comments to the end, so the comments preceed them. + // Move inline comments to the end, so the comments precede them. foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PholioTransactionType::TYPE_INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PholioTransactionType::TYPE_INLINE: return true; } return parent::shouldImplyCC($object, $xaction); } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index ae39382c7..f427b62a0 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1,1247 +1,1247 @@ continueOnNoEffect = $continue; return $this; } public function getContinueOnNoEffect() { return $this->continueOnNoEffect; } /** * Not strictly necessary, but reply handlers ideally set this value to * make email threading work better. */ public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function getParentMessageID() { return $this->parentMessageID; } protected function getIsNewObject() { return $this->isNewObject; } protected function getMentionedPHIDs() { return $this->mentionedPHIDs; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function getTransactionTypes() { $types = array(); if ($this->object instanceof PhabricatorSubscribableInterface) { $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS; } return $types; } private function adjustTransactionValues( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $this->getTransactionOldValue($object, $xaction); $xaction->setOldValue($old); $new = $this->getTransactionNewValue($object, $xaction); $xaction->setNewValue($new); } private function getTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: return array_values($this->subscribers); case PhabricatorTransactions::TYPE_VIEW_POLICY: return $object->getViewPolicy(); case PhabricatorTransactions::TYPE_EDIT_POLICY: return $object->getEditPolicy(); case PhabricatorTransactions::TYPE_EDGE: $edge_type = $xaction->getMetadataValue('edge:type'); if (!$edge_type) { throw new Exception("Edge transaction has no 'edge:type'!"); } $old_edges = array(); if ($object->getPHID()) { $edge_src = $object->getPHID(); $old_edges = id(new PhabricatorEdgeQuery()) ->setViewer($this->getActor()) ->withSourcePHIDs(array($edge_src)) ->withEdgeTypes(array($edge_type)) ->needEdgeData(true) ->execute(); $old_edges = $old_edges[$edge_src][$edge_type]; } return $old_edges; default: return $this->getCustomTransactionOldValue($object, $xaction); } } private function getTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: return $this->getPHIDTransactionNewValue($xaction); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: return $xaction->getNewValue(); case PhabricatorTransactions::TYPE_EDGE: return $this->getEdgeTransactionNewValue($xaction); default: return $this->getCustomTransactionNewValue($object, $xaction); } } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception("Capability not supported!"); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception("Capability not supported!"); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return $xaction->hasComment(); } return ($xaction->getOldValue() !== $xaction->getNewValue()); } private function applyInternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: $object->setEditPolicy($xaction->getNewValue()); break; } return $this->applyCustomInternalTransaction($object, $xaction); } private function applyExternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($object) ->setActor($this->requireActor()); $old_map = array_fuse($xaction->getOldValue()); $new_map = array_fuse($xaction->getNewValue()); $subeditor->unsubscribe( array_keys( array_diff_key($old_map, $new_map))); $subeditor->subscribeExplicit( array_keys( array_diff_key($new_map, $old_map))); $subeditor->save(); break; case PhabricatorTransactions::TYPE_EDGE: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $src = $object->getPHID(); $type = $xaction->getMetadataValue('edge:type'); foreach ($new as $dst_phid => $edge) { $new[$dst_phid]['src'] = $src; } $editor = id(new PhabricatorEdgeEditor()) ->setActor($this->getActor()); foreach ($old as $dst_phid => $edge) { if (!empty($new[$dst_phid])) { if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { continue; } } $editor->removeEdge($src, $type, $dst_phid); } foreach ($new as $dst_phid => $edge) { if (!empty($old[$dst_phid])) { if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { continue; } } $data = array( 'data' => $edge['data'], ); $editor->addEdge($src, $type, $dst_phid, $data); } $editor->save(); break; } return $this->applyCustomExternalTransaction($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception("Capability not supported!"); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { throw new Exception("Capability not supported!"); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function setContentSourceFromRequest(AphrontRequest $request) { return $this->setContentSource( PhabricatorContentSource::newFromRequest($request)); } public function getContentSource() { return $this->contentSource; } final public function applyTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->object = $object; $this->xactions = $xactions; $this->isNewObject = ($object->getPHID() === null); $this->validateEditParameters($object, $xactions); $actor = $this->requireActor(); if ($object->getPHID() && ($object instanceof PhabricatorSubscribableInterface)) { $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); $this->subscribers = array_fuse($subs); } else { $this->subscribers = array(); } $xactions = $this->applyImplicitCC($object, $xactions); $mention_xaction = $this->buildMentionTransaction($object, $xactions); if ($mention_xaction) { $xactions[] = $mention_xaction; } $xactions = $this->combineTransactions($xactions); foreach ($xactions as $xaction) { // TODO: This needs to be more sophisticated once we have meta-policies. $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC); $xaction->setEditPolicy($actor->getPHID()); $xaction->setAuthorPHID($actor->getPHID()); $xaction->setContentSource($this->getContentSource()); } $is_preview = $this->getIsPreview(); $read_locking = false; if (!$is_preview && $object->getID()) { foreach ($xactions as $xaction) { // If any of the transactions require a read lock, hold one and reload // the object. We need to do this fairly early so that the call to // `adjustTransactionValues()` (which populates old values) is based // on the synchronized state of the object, which may differ from the // state when it was originally loaded. if ($this->shouldReadLock($object, $xaction)) { $object->openTransaction(); $object->beginReadLocking(); $read_locking = true; $object->reload(); break; } } } foreach ($xactions as $xaction) { $this->adjustTransactionValues($object, $xaction); } $xactions = $this->filterTransactions($object, $xactions); if (!$xactions) { if ($read_locking) { $object->endReadLocking(); $read_locking = false; $object->killTransaction(); } return array(); } $xactions = $this->sortTransactions($xactions); if ($is_preview) { $this->loadHandles($xactions); return $xactions; } $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($actor) ->setContentSource($this->getContentSource()); if (!$read_locking) { $object->openTransaction(); } foreach ($xactions as $xaction) { $this->applyInternalEffects($object, $xaction); } $object->save(); foreach ($xactions as $xaction) { $xaction->setObjectPHID($object->getPHID()); if ($xaction->getComment()) { $xaction->setPHID($xaction->generatePHID()); $comment_editor->applyEdit($xaction, $xaction->getComment()); } else { $xaction->save(); } } foreach ($xactions as $xaction) { $this->applyExternalEffects($object, $xaction); } $this->applyFinalEffects($object, $xactions); if ($read_locking) { $object->endReadLocking(); $read_locking = false; } $object->saveTransaction(); $this->loadHandles($xactions); $mail = null; if ($this->supportsMail()) { $mail = $this->sendMail($object, $xactions); } if ($this->supportsSearch()) { id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($object->getPHID()); } if ($this->supportsFeed()) { $mailed = array(); if ($mail) { $mailed = $mail->buildRecipientList(); } $this->publishFeedStory( $object, $xactions, $mailed); } $this->didApplyTransactions($xactions); return $xactions; } protected function didApplyTransactions(array $xactions) { // Hook for subclasses. return; } /** * Determine if the editor should hold a read lock on the object while * applying a transaction. * * If the editor does not hold a lock, two editors may read an object at the * same time, then apply their changes without any synchronization. For most * transactions, this does not matter much. However, it is important for some * transactions. For example, if an object has a transaction count on it, both * editors may read the object with `count = 23`, then independently update it * and save the object with `count = 24` twice. This will produce the wrong * state: the object really has 25 transactions, but the count is only 24. * * Generally, transactions fall into one of four buckets: * * - Append operations: Actions like adding a comment to an object purely * add information to its state, and do not depend on the current object * state in any way. These transactions never need to hold locks. * - Overwrite operations: Actions like changing the title or description * of an object replace the current value with a new value, so the end * state is consistent without a lock. We currently do not lock these * transactions, although we may in the future. * - Edge operations: Edge and subscription operations have internal * synchronization which limits the damage race conditions can cause. * We do not currently lock these transactions, although we may in the * future. * - Update operations: Actions like incrementing a count on an object. * These operations generally should use locks, unless it is not * important that the state remain consistent in the presence of races. * * @param PhabricatorLiskDAO Object being updated. * @param PhabricatorApplicationTransaction Transaction being applied. * @return bool True to synchronize the edit with a lock. */ protected function shouldReadLock( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return false; } private function loadHandles(array $xactions) { $phids = array(); foreach ($xactions as $key => $xaction) { $phids[$key] = $xaction->getRequiredHandlePHIDs(); } $handles = array(); $merged = array_mergev($phids); if ($merged) { $handles = id(new PhabricatorObjectHandleData($merged)) ->setViewer($this->requireActor()) ->loadHandles(); } foreach ($xactions as $key => $xaction) { $xaction->setHandles(array_select_keys($handles, $phids[$key])); } } private function validateEditParameters( PhabricatorLiskDAO $object, array $xactions) { if (!$this->getContentSource()) { throw new Exception( "Call setContentSource() before applyTransactions()!"); } // Do a bunch of sanity checks that the incoming transactions are fresh. // They should be unsaved and have only "transactionType" and "newValue" // set. $types = array_fill_keys($this->getTransactionTypes(), true); assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); foreach ($xactions as $xaction) { if ($xaction->getPHID() || $xaction->getID()) { throw new Exception( "You can not apply transactions which already have IDs/PHIDs!"); } if ($xaction->getObjectPHID()) { throw new Exception( "You can not apply transactions which already have objectPHIDs!"); } if ($xaction->getAuthorPHID()) { throw new Exception( "You can not apply transactions which already have authorPHIDs!"); } if ($xaction->getCommentPHID()) { throw new Exception( "You can not apply transactions which already have commentPHIDs!"); } if ($xaction->getCommentVersion() !== 0) { throw new Exception( "You can not apply transactions which already have commentVersions!"); } if ($xaction->getOldValue() !== null) { throw new Exception( "You can not apply transactions which already have oldValue!"); } $type = $xaction->getTransactionType(); if (empty($types[$type])) { throw new Exception("Transaction has unknown type '{$type}'."); } } // The actor must have permission to view and edit the object. $actor = $this->requireActor(); PhabricatorPolicyFilter::requireCapability( $actor, $xaction, PhabricatorPolicyCapability::CAN_VIEW); PhabricatorPolicyFilter::requireCapability( $actor, $xaction, PhabricatorPolicyCapability::CAN_EDIT); } private function buildMentionTransaction( PhabricatorLiskDAO $object, array $xactions) { if (!($object instanceof PhabricatorSubscribableInterface)) { return null; } $texts = array(); foreach ($xactions as $xaction) { $texts[] = $this->getMentionableTextsFromTransaction($xaction); } $texts = array_mergev($texts); $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions($texts); $this->mentionedPHIDs = $phids; if ($object->getPHID()) { // Don't try to subscribe already-subscribed mentions: we want to generate // a dialog about an action having no effect if the user explicitly adds // existing CCs, but not if they merely mention existing subscribers. $phids = array_diff($phids, $this->subscribers); } foreach ($phids as $key => $phid) { if ($object->isAutomaticallySubscribed($phid)) { unset($phids[$key]); } } $phids = array_values($phids); if (!$phids) { return null; } $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => $phids)); return $xaction; } protected function getMentionableTextsFromTransaction( PhabricatorApplicationTransaction $transaction) { $texts = array(); if ($transaction->getComment()) { $texts[] = $transaction->getComment()->getContent(); } return $texts; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: return $this->mergePHIDOrEdgeTransactions($u, $v); case PhabricatorTransactions::TYPE_EDGE: $u_type = $u->getMetadataValue('edge:type'); $v_type = $v->getMetadataValue('edge:type'); if ($u_type == $v_type) { return $this->mergePHIDOrEdgeTransactions($u, $v); } return null; } // By default, do not merge the transactions. return null; } /** * Attempt to combine similar transactions into a smaller number of total * transactions. For example, two transactions which edit the title of an * object can be merged into a single edit. */ private function combineTransactions(array $xactions) { $stray_comments = array(); $result = array(); $types = array(); foreach ($xactions as $key => $xaction) { $type = $xaction->getTransactionType(); if (isset($types[$type])) { foreach ($types[$type] as $other_key) { $merged = $this->mergeTransactions($result[$other_key], $xaction); if ($merged) { $result[$other_key] = $merged; if ($xaction->getComment() && ($xaction->getComment() !== $merged->getComment())) { $stray_comments[] = $xaction->getComment(); } if ($result[$other_key]->getComment() && ($result[$other_key]->getComment() !== $merged->getComment())) { $stray_comments[] = $result[$other_key]->getComment(); } // Move on to the next transaction. continue 2; } } } $result[$key] = $xaction; $types[$type][] = $key; } // If we merged any comments away, restore them. foreach ($stray_comments as $comment) { $xaction = newv(get_class(head($result)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT); $xaction->setComment($comment); $result[] = $xaction; } return array_values($result); } protected function mergePHIDOrEdgeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $result = $u->getNewValue(); foreach ($v->getNewValue() as $key => $value) { $result[$key] = array_merge($value, idx($result, $key, array())); } $u->setNewValue($result); return $u; } protected function getPHIDTransactionNewValue( PhabricatorApplicationTransaction $xaction) { $old = array_fuse($xaction->getOldValue()); $new = $xaction->getNewValue(); $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); unset($new['-']); $new_set = idx($new, '=', null); if ($new_set !== null) { $new_set = array_fuse($new_set); } unset($new['=']); if ($new) { throw new Exception( "Invalid 'new' value for PHID transaction. Value should contain only ". "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS)."); } $result = array(); foreach ($old as $phid) { if ($new_set !== null && empty($new_set[$phid])) { continue; } $result[$phid] = $phid; } if ($new_set !== null) { foreach ($new_set as $phid) { $result[$phid] = $phid; } } foreach ($new_add as $phid) { $result[$phid] = $phid; } foreach ($new_rem as $phid) { unset($result[$phid]); } return array_values($result); } protected function getEdgeTransactionNewValue( PhabricatorApplicationTransaction $xaction) { $new = $xaction->getNewValue(); $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); unset($new['-']); $new_set = idx($new, '=', null); unset($new['=']); if ($new) { throw new Exception( "Invalid 'new' value for Edge transaction. Value should contain only ". "keys '+' (add edges), '-' (remove edges) and '=' (set edges)."); } $old = $xaction->getOldValue(); $lists = array($new_set, $new_add, $new_rem); foreach ($lists as $list) { $this->checkEdgeList($list); } $result = array(); foreach ($old as $dst_phid => $edge) { if ($new_set !== null && empty($new_set[$dst_phid])) { continue; } $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge); } if ($new_set !== null) { foreach ($new_set as $dst_phid => $edge) { $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge); } } foreach ($new_add as $dst_phid => $edge) { $result[$dst_phid] = $this->normalizeEdgeTransactionValue( $xaction, $edge); } foreach ($new_rem as $dst_phid => $edge) { unset($result[$dst_phid]); } return $result; } private function checkEdgeList($list) { if (!$list) { return; } foreach ($list as $key => $item) { if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { throw new Exception( "Edge transactions must have destination PHIDs as in edge ". "lists (found key '{$key}')."); } if (!is_array($item) && $item !== $key) { throw new Exception( "Edge transactions must have PHIDs or edge specs as values ". "(found value '{$item}')."); } } } protected function normalizeEdgeTransactionValue( PhabricatorApplicationTransaction $xaction, $edge) { if (!is_array($edge)) { $edge = array( 'dst' => $edge, ); } $edge_type = $xaction->getMetadataValue('edge:type'); if (empty($edge['type'])) { $edge['type'] = $edge_type; } else { if ($edge['type'] != $edge_type) { $this_type = $edge['type']; throw new Exception( "Edge transaction includes edge of type '{$this_type}', but ". "transaction is of type '{$edge_type}'. Each edge transaction must ". "alter edges of only one type."); } } if (!isset($edge['data'])) { $edge['data'] = null; } return $edge; } protected function sortTransactions(array $xactions) { $head = array(); $tail = array(); - // Move bare comments to the end, so the actions preceed them. + // Move bare comments to the end, so the actions precede them. foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorTransactions::TYPE_COMMENT) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function filterTransactions( PhabricatorLiskDAO $object, array $xactions) { $type_comment = PhabricatorTransactions::TYPE_COMMENT; $no_effect = array(); $has_comment = false; $any_effect = false; foreach ($xactions as $key => $xaction) { if ($this->transactionHasEffect($object, $xaction)) { if ($xaction->getTransactionType() != $type_comment) { $any_effect = true; } } else { $no_effect[$key] = $xaction; } if ($xaction->hasComment()) { $has_comment = true; } } if (!$no_effect) { return $xactions; } if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) { throw new PhabricatorApplicationTransactionNoEffectException( $no_effect, $any_effect, $has_comment); } if (!$any_effect && !$has_comment) { // If we only have empty comment transactions, just drop them all. return array(); } foreach ($no_effect as $key => $xaction) { if ($xaction->getComment()) { $xaction->setTransactionType($type_comment); $xaction->setOldValue(null); $xaction->setNewValue(null); } else { unset($xactions[$key]); } } return $xactions; } /* -( Implicit CCs )------------------------------------------------------- */ /** * When a user interacts with an object, we might want to add them to CC. */ final public function applyImplicitCC( PhabricatorLiskDAO $object, array $xactions) { if (!($object instanceof PhabricatorSubscribableInterface)) { // If the object isn't subscribable, we can't CC them. return $xactions; } $actor_phid = $this->requireActor()->getPHID(); if ($object->isAutomaticallySubscribed($actor_phid)) { // If they're auto-subscribed, don't CC them. return $xactions; } $should_cc = false; foreach ($xactions as $xaction) { if ($this->shouldImplyCC($object, $xaction)) { $should_cc = true; break; } } if (!$should_cc) { // Only some types of actions imply a CC (like adding a comment). return $xactions; } if ($object->getPHID()) { if (isset($this->subscribers[$actor_phid])) { // If the user is already subscribed, don't implicitly CC them. return $xactions; } $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); $unsub = array_fuse($unsub); if (isset($unsub[$actor_phid])) { // If the user has previously unsubscribed from this object explicitly, // don't implicitly CC them. return $xactions; } } $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => array($actor_phid))); array_unshift($xactions, $xaction); return $xactions; } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return true; default: return false; } } /* -( Sending Mail )------------------------------------------------------- */ /** * @task mail */ protected function supportsMail() { return false; } /** * @task mail */ protected function sendMail( PhabricatorLiskDAO $object, array $xactions) { $email_to = $this->getMailTo($object); $email_cc = $this->getMailCC($object); $phids = array_merge($email_to, $email_cc); $handles = id(new PhabricatorObjectHandleData($phids)) ->setViewer($this->requireActor()) ->loadHandles(); $template = $this->buildMailTemplate($object); $body = $this->buildMailBody($object, $xactions); $mail_tags = $this->getMailTags($object, $xactions); $action = $this->getStrongestAction($object, $xactions)->getActionName(); $template ->setFrom($this->requireActor()->getPHID()) ->setSubjectPrefix($this->getMailSubjectPrefix()) ->setVarySubjectPrefix('['.$action.']') ->setThreadID($object->getPHID(), $this->getIsNewObject()) ->setRelatedPHID($object->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setMailTags($mail_tags) ->setIsBulk(true) ->setBody($body->render()); if ($this->getParentMessageID()) { $template->setParentMessageID($this->getParentMessageID()); } $mails = $this ->buildReplyHandler($object) ->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } $template->addTos($email_to); $template->addCCs($email_cc); return $template; } /** * @task mail */ protected function getStrongestAction( PhabricatorLiskDAO $object, array $xactions) { return last(msort($xactions, 'getActionStrength')); } /** * @task mail */ protected function buildReplyHandler(PhabricatorLiskDAO $object) { throw new Exception("Capability not supported."); } /** * @task mail */ protected function getMailSubjectPrefix() { throw new Exception("Capability not supported."); } /** * @task mail */ protected function getMailTags( PhabricatorLiskDAO $object, array $xactions) { $tags = array(); foreach ($xactions as $xaction) { $tags[] = $xaction->getMailTags(); } return array_mergev($tags); } /** * @task mail */ protected function buildMailTemplate(PhabricatorLiskDAO $object) { throw new Exception("Capability not supported."); } /** * @task mail */ protected function getMailTo(PhabricatorLiskDAO $object) { throw new Exception("Capability not supported."); } /** * @task mail */ protected function getMailCC(PhabricatorLiskDAO $object) { if ($object instanceof PhabricatorSubscribableInterface) { return $this->subscribers; } throw new Exception("Capability not supported."); } /** * @task mail */ protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $headers = array(); $comments = array(); foreach ($xactions as $xaction) { $headers[] = id(clone $xaction)->setRenderingTarget('text')->getTitle(); $comment = $xaction->getComment(); if ($comment && strlen($comment->getContent())) { $comments[] = $comment->getContent(); } } $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection(implode("\n", $headers)); foreach ($comments as $comment) { $body->addRawSection($comment); } return $body; } /* -( Publishing Feed Stories )-------------------------------------------- */ /** * @task feed */ protected function supportsFeed() { return false; } /** * @task feed */ protected function getFeedStoryType() { return 'PhabricatorApplicationTransactionFeedStory'; } /** * @task feed */ protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { return array( $object->getPHID(), $this->requireActor()->getPHID(), ); } /** * @task feed */ protected function getFeedNotifyPHIDs( PhabricatorLiskDAO $object, array $xactions) { return array_merge( $this->getMailTo($object), $this->getMailCC($object)); } /** * @task feed */ protected function getFeedStoryData( PhabricatorLiskDAO $object, array $xactions) { $xactions = msort($xactions, 'getActionStrength'); $xactions = array_reverse($xactions); return array( 'objectPHID' => $object->getPHID(), 'transactionPHIDs' => mpull($xactions, 'getPHID'), ); } /** * @task feed */ protected function publishFeedStory( PhabricatorLiskDAO $object, array $xactions, array $mailed_phids) { $related_phids = $this->getFeedRelatedPHIDs($object, $xactions); $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions); $story_type = $this->getFeedStoryType(); $story_data = $this->getFeedStoryData($object, $xactions); id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) ->setStoryTime(time()) ->setStoryAuthorPHID($this->requireActor()->getPHID()) ->setRelatedPHIDs($related_phids) ->setPrimaryObjectPHID($object->getPHID()) ->setSubscribedPHIDs($subscribed_phids) ->setMailRecipientPHIDs($mailed_phids) ->publish(); } /* -( Search Index )------------------------------------------------------- */ /** * @task search */ protected function supportsSearch() { return false; } } diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php index 6c4d7f162..2e81dee3e 100644 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php +++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php @@ -1,163 +1,163 @@ server); return $uri->getDomain(); } public function connect() { $this->server = $this->getConfig('server'); $this->authtoken = $this->getConfig('authtoken'); $rooms = $this->getConfig('join'); // First, join the room if (!$rooms) { throw new Exception("Not configured to join any rooms!"); } $this->readBuffers = array(); // Set up our long poll in a curl multi request so we can // continue running while it executes in the background $this->multiHandle = curl_multi_init(); $this->readHandles = array(); foreach ($rooms as $room_id) { $this->joinRoom($room_id); // Set up the curl stream for reading $url = $this->buildStreamingUrl($room_id); $this->readHandle[$url] = curl_init(); curl_setopt($this->readHandle[$url], CURLOPT_URL, $url); curl_setopt($this->readHandle[$url], CURLOPT_RETURNTRANSFER, true); curl_setopt($this->readHandle[$url], CURLOPT_FOLLOWLOCATION, 1); curl_setopt( $this->readHandle[$url], CURLOPT_USERPWD, $this->authtoken.':x'); curl_setopt( $this->readHandle[$url], CURLOPT_HTTPHEADER, array("Content-type: application/json")); curl_setopt( $this->readHandle[$url], CURLOPT_WRITEFUNCTION, array($this, 'read')); curl_setopt($this->readHandle[$url], CURLOPT_BUFFERSIZE, 128); curl_setopt($this->readHandle[$url], CURLOPT_TIMEOUT, 0); curl_multi_add_handle($this->multiHandle, $this->readHandle[$url]); // Initialize read buffer $this->readBuffers[$url] = ''; } $this->active = null; $this->blockingMultiExec(); } protected function joinRoom($room_id) { // Optional hook, by default, do nothing } // This is our callback for the background curl multi-request. // Puts the data read in on the readBuffer for processing. private function read($ch, $data) { $info = curl_getinfo($ch); $length = strlen($data); $this->readBuffers[$info['url']] .= $data; return $length; } private function blockingMultiExec() { do { $status = curl_multi_exec($this->multiHandle, $this->active); } while ($status == CURLM_CALL_MULTI_PERFORM); // Check for errors if ($status != CURLM_OK) { throw new Exception( "Phabricator Bot had a problem reading from stream."); } } public function getNextMessages($poll_frequency) { $messages = array(); if (!$this->active) { throw new Exception("Phabricator Bot stopped reading from stream."); } // Prod our http request curl_multi_select($this->multiHandle, $poll_frequency); $this->blockingMultiExec(); // Process anything waiting on the read buffer while ($m = $this->processReadBuffer()) { $messages[] = $m; } return $messages; } private function processReadBuffer() { foreach ($this->readBuffers as $url => &$buffer) { $until = strpos($buffer, "}\r"); if ($until == false) { continue; } $message = substr($buffer, 0, $until + 1); $buffer = substr($buffer, $until + 2); $m_obj = json_decode($message, true); if ($message = $this->processMessage($m_obj)) { return $message; } } // If we're here, there's nothing to process return false; } - protected function performPost($endpoint, $data = Null) { + protected function performPost($endpoint, $data = null) { $uri = new PhutilURI($this->server); $uri->setPath($endpoint); $payload = json_encode($data); list($output) = id(new HTTPSFuture($uri)) ->setMethod('POST') ->addHeader('Content-Type', 'application/json') ->addHeader('Authorization', $this->getAuthorizationHeader()) ->setData($payload) ->resolvex(); $output = trim($output); if (strlen($output)) { return json_decode($output, true); } return true; } protected function getAuthorizationHeader() { return 'Basic '.base64_encode($this->authtoken.':x'); } abstract protected function buildStreamingUrl($channel); abstract protected function processMessage($raw_object); } diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php index 4ee2b68b8..7de1b43f3 100644 --- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php +++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php @@ -1,439 +1,439 @@ addEdge($src, $type, $dst) * ->setActor($user) * ->save(); * * @task edit Editing Edges * @task cycles Cycle Prevention * @task internal Internals */ final class PhabricatorEdgeEditor extends PhabricatorEditor { private $addEdges = array(); private $remEdges = array(); private $openTransactions = array(); private $suppressEvents; /* -( Editing Edges )------------------------------------------------------ */ /** * Add a new edge (possibly also adding its inverse). Changes take effect when * you call @{method:save}. If the edge already exists, it will not be * overwritten, but if data is attached to the edge it will be updated. * Removals queued with @{method:removeEdge} are executed before * adds, so the effect of removing and adding the same edge is to overwrite * any existing edge. * * The `$options` parameter accepts these values: * * - `data` Optional, data to write onto the edge. * - `inverse_data` Optional, data to write on the inverse edge. If not * provided, `data` will be written. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @param map Options map (see documentation). * @return this * * @task edit */ public function addEdge($src, $type, $dst, array $options = array()) { foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) { $this->addEdges[] = $spec; } return $this; } /** * Remove an edge (possibly also removing its inverse). Changes take effect * when you call @{method:save}. If an edge does not exist, the removal * will be ignored. Edges are added after edges are removed, so the effect of * a remove plus an add is to overwrite. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @return this * * @task edit */ public function removeEdge($src, $type, $dst) { foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) { $this->remEdges[] = $spec; } return $this; } /** * Apply edge additions and removals queued by @{method:addEdge} and * @{method:removeEdge}. Note that transactions are opened, all additions and * removals are executed, and then transactions are saved. Thus, in some cases * it may be slightly more efficient to perform multiple edit operations * (e.g., adds followed by removals) if their outcomes are not dependent, * since transactions will not be held open as long. * * @return this * @task edit */ public function save() { $cycle_types = $this->getPreventCyclesEdgeTypes(); $locks = array(); $caught = null; try { // NOTE: We write edge data first, before doing any transactions, since // it's OK if we just leave it hanging out in space unattached to // anything. $this->writeEdgeData(); // If we're going to perform cycle detection, lock the edge type before // doing edits. if ($cycle_types) { $src_phids = ipull($this->addEdges, 'src'); foreach ($cycle_types as $cycle_type) { $key = 'edge.cycle:'.$cycle_type; $locks[] = PhabricatorGlobalLock::newLock($key)->lock(15); } } static $id = 0; $id++; $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES); // NOTE: Removes first, then adds, so that "remove + add" is a useful // operation meaning "overwrite". $this->executeRemoves(); $this->executeAdds(); foreach ($cycle_types as $cycle_type) { $this->detectCycles($src_phids, $cycle_type); } $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES); $this->saveTransactions(); } catch (Exception $ex) { $caught = $ex; } if ($caught) { $this->killTransactions(); } foreach ($locks as $lock) { $lock->unlock(); } if ($caught) { throw $caught; } } /* -( Internals )---------------------------------------------------------- */ /** * Build the specification for an edge operation, and possibly build its * inverse as well. * * @task internal */ private function buildEdgeSpecs($src, $type, $dst, array $options = array()) { $data = array(); if (!empty($options['data'])) { $data['data'] = $options['data']; } $src_type = phid_get_type($src); $dst_type = phid_get_type($dst); $specs = array(); $specs[] = array( 'src' => $src, 'src_type' => $src_type, 'dst' => $dst, 'dst_type' => $dst_type, 'type' => $type, 'data' => $data, ); $inverse = PhabricatorEdgeConfig::getInverse($type); if ($inverse) { // If `inverse_data` is set, overwrite the edge data. Normally, just // write the same data to the inverse edge. if (array_key_exists('inverse_data', $options)) { $data['data'] = $options['inverse_data']; } $specs[] = array( 'src' => $dst, 'src_type' => $dst_type, 'dst' => $src, 'dst_type' => $src_type, 'type' => $inverse, 'data' => $data, ); } return $specs; } /** * Write edge data. * * @task internal */ private function writeEdgeData() { $adds = $this->addEdges; $writes = array(); foreach ($adds as $key => $edge) { if ($edge['data']) { $writes[] = array($key, $edge['src_type'], json_encode($edge['data'])); } } foreach ($writes as $write) { list($key, $src_type, $data) = $write; $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); queryfx( $conn_w, 'INSERT INTO %T (data) VALUES (%s)', PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, $data); $this->addEdges[$key]['data_id'] = $conn_w->getInsertID(); } } /** * Add queued edges. * * @task internal */ private function executeAdds() { $adds = $this->addEdges; $adds = igroup($adds, 'src_type'); // Assign stable sequence numbers to each edge, so we have a consistent // ordering across edges by source and type. foreach ($adds as $src_type => $edges) { $edges_by_src = igroup($edges, 'src'); foreach ($edges_by_src as $src => $src_edges) { $seq = 0; foreach ($src_edges as $key => $edge) { $src_edges[$key]['seq'] = $seq++; $src_edges[$key]['dateCreated'] = time(); } $edges_by_src[$src] = $src_edges; } $adds[$src_type] = array_mergev($edges_by_src); } $inserts = array(); foreach ($adds as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(%s, %d, %s, %d, %d, %nd)', $edge['src'], $edge['type'], $edge['dst'], $edge['dateCreated'], $edge['seq'], idx($edge, 'data_id')); } $inserts[] = array($conn_w, $sql); } foreach ($inserts as $insert) { list($conn_w, $sql) = $insert; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID) VALUES %Q ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, implode(', ', $chunk)); } } } /** * Remove queued edges. * * @task internal */ private function executeRemoves() { $rems = $this->remEdges; $rems = igroup($rems, 'src_type'); $deletes = array(); foreach ($rems as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(src = %s AND type = %d AND dst = %s)', $edge['src'], $edge['type'], $edge['dst']); } $deletes[] = array($conn_w, $sql); } foreach ($deletes as $delete) { list($conn_w, $sql) = $delete; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'DELETE FROM %T WHERE (%Q)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, implode(' OR ', $chunk)); } } } /** * Save open transactions. * * @task internal */ private function saveTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->saveTransaction(); unset($this->openTransactions[$key]); } } private function killTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->killTransaction(); unset($this->openTransactions[$key]); } } /** * Suppress edge edit events. This prevents listeners from making updates in * response to edits, and is primarily useful when performing migrations. You * should not normally need to use it. * - * @param bool True to supress events related to edits. + * @param bool True to suppress events related to edits. * @return this * @task internal */ public function setSuppressEvents($suppress) { $this->suppressEvents = $suppress; return $this; } private function sendEvent($edit_id, $event_type) { if ($this->suppressEvents) { return; } $event = new PhabricatorEvent( $event_type, array( 'id' => $edit_id, 'add' => $this->addEdges, 'rem' => $this->remEdges, )); $event->setUser($this->getActor()); PhutilEventEngine::dispatchEvent($event); } /* -( Cycle Prevention )--------------------------------------------------- */ /** * Get a list of all edge types which are being added, and which we should * prevent cycles on. * * @return list List of edge types which should have cycles prevented. * @task cycle */ private function getPreventCyclesEdgeTypes() { $edge_types = array(); foreach ($this->addEdges as $edge) { $edge_types[$edge['type']] = true; } foreach ($edge_types as $type => $ignored) { if (!PhabricatorEdgeConfig::shouldPreventCycles($type)) { unset($edge_types[$type]); } } return array_keys($edge_types); } /** * Detect graph cycles of a given edge type. If the edit introduces a cycle, * a @{class:PhabricatorEdgeCycleException} is thrown with details. * * @return void * @task cycle */ private function detectCycles(array $phids, $edge_type) { // For simplicity, we just seed the graph with the affected nodes rather // than seeding it with their edges. To do this, we just add synthetic // edges from an imaginary '' node to the known edges. $graph = id(new PhabricatorEdgeGraph()) ->setEdgeType($edge_type) ->addNodes( array( '' => $phids, )) ->loadGraph(); foreach ($phids as $phid) { $cycle = $graph->detectCycles($phid); if ($cycle) { throw new PhabricatorEdgeCycleException($edge_type, $cycle); } } } } diff --git a/src/view/layout/PhabricatorCrumbsView.php b/src/view/layout/PhabricatorCrumbsView.php index 9fc18f98c..c742db4b8 100644 --- a/src/view/layout/PhabricatorCrumbsView.php +++ b/src/view/layout/PhabricatorCrumbsView.php @@ -1,133 +1,132 @@ crumbs[] = $crumb; return $this; } public function addAction(PhabricatorMenuItemView $action) { $this->actions[] = $action; return $this; } public function setActionList(PhabricatorActionListView $list) { $this->actionListID = celerity_generate_unique_node_id(); $list->setId($this->actionListID); return $this; } public function render() { require_celerity_resource('phabricator-crumbs-view-css'); $action_view = null; if (($this->actions) || ($this->actionListID)) { $actions = array(); foreach ($this->actions as $action) { $icon = null; if ($action->getIcon()) { $icon = phutil_tag( 'span', array( 'class' => 'sprite-icons icons-'.$action->getIcon(), ), ''); } $name = phutil_tag( 'span', array( 'class' => 'phabricator-crumbs-action-name' ), - $action->getName() - ); + $action->getName()); $action_sigils = $action->getSigils(); if ($action->getWorkflow()) { $action_sigils[] = 'workflow'; } $action_classes = $action->getClasses(); $action_classes[] = 'phabricator-crumbs-action'; $actions[] = javelin_tag( 'a', array( 'href' => $action->getHref(), 'class' => implode(' ', $action_classes), 'sigil' => implode(' ', $action_sigils), 'style' => $action->getStyle() ), array( $icon, $name, )); } if ($this->actionListID) { $icon_id = celerity_generate_unique_node_id(); $icon = phutil_tag( 'span', array( 'class' => 'sprite-icons action-action-menu' ), ''); $name = phutil_tag( 'span', array( 'class' => 'phabricator-crumbs-action-name' ), pht('Actions')); $actions[] = javelin_tag( 'a', array( 'href' => '#', 'class' => 'phabricator-crumbs-action phabricator-crumbs-action-menu', 'sigil' => 'jx-toggle-class', 'id' => $icon_id, 'meta' => array( 'map' => array( $this->actionListID => 'phabricator-action-list-toggle', $icon_id => 'phabricator-crumbs-action-menu-open' ), ), ), array( $icon, $name, )); } $action_view = phutil_tag( 'div', array( 'class' => 'phabricator-crumbs-actions', ), $actions); } if ($this->crumbs) { last($this->crumbs)->setIsLastCrumb(true); } return phutil_tag( 'div', array( 'class' => 'phabricator-crumbs-view '. 'sprite-gradient gradient-breadcrumbs', ), array( $action_view, $this->crumbs, )); } } diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index d8f0eb9d4..59f9e3d6f 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -1,220 +1,220 @@ title = $title; return $this; } public function setEpoch($epoch) { $this->epoch = $epoch; return $this; } public function setImage($image) { $this->image = $image; return $this; } public function setImageHref($image_href) { $this->imageHref = $image_href; return $this; } public function setAppIcon($icon) { $this->appIcon = $icon; return $this; } public function setViewed($viewed) { $this->viewed = $viewed; return $this; } public function getViewed() { return $this->viewed; } public function setHref($href) { $this->href = $href; return $this; } public function setTokenBar(array $tokens) { $this->tokenBar = $tokens; return $this; } public function addProject($project) { $this->projects[] = $project; return $this; } public function addAction(PHUIIconView $action) { $this->actions[] = $action; return $this; } public function setPontification($text, $title = null) { if ($title) { $title = phutil_tag('h3', array(), $title); } $copy = phutil_tag( 'div', array( 'class' => 'phui-feed-story-bigtext-post', ), array( $title, $text)); $this->appendChild($copy); return $this; } public function getHref() { return $this->href; } public function renderNotification() { $classes = array( 'phabricator-notification', ); if (!$this->viewed) { $classes[] = 'phabricator-notification-unread'; } return javelin_tag( 'div', array( 'class' => implode(' ', $classes), 'sigil' => 'notification', 'meta' => array( 'href' => $this->getHref(), ), ), $this->title); } public function render() { require_celerity_resource('phui-feed-story-css'); $actor = ''; if ($this->image) { $actor = new PHUIIconView(); $actor->setImage($this->image); $actor->addClass('phui-feed-story-actor-image'); if ($this->imageHref) { $actor->setHref($this->imageHref); } } $action_list = array(); $icons = null; foreach ($this->actions as $action) { $action_list[] = phutil_tag( 'li', array( 'class' => 'phui-feed-story-action-item' ), $action); } if (!empty($action_list)) { $icons = phutil_tag( 'ul', array( 'class' => 'phui-feed-story-action-list' ), $action_list); } $head = phutil_tag( 'div', array( 'class' => 'phui-feed-story-head', ), array( $actor, nonempty($this->title, pht('Untitled Story')), $icons )); $body = null; $foot = null; $image_style = null; if (!empty($this->tokenBar)) { $tokenview = phutil_tag( 'div', array( 'class' => 'phui-feed-token-bar' ), $this->tokenBar); $this->appendChild($tokenview); } $body_content = $this->renderChildren(); if ($body_content) { $body = phutil_tag( 'div', array( 'class' => 'phui-feed-story-body', ), $body_content); } if ($this->epoch) { $foot = phabricator_datetime($this->epoch, $this->user); } else { $foot = pht('No time specified.'); } $icon = null; if ($this->appIcon) { $icon = new PHUIIconView(); $icon->setSpriteIcon($this->appIcon); $icon->setSpriteSheet(PHUIIconView::SPRITE_APPS); } $foot = phutil_tag( 'div', array( 'class' => 'phui-feed-story-foot', ), array( $icon, $foot)); return id(new PHUIBoxView()) ->addClass('phui-feed-story') ->setShadow(true) ->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM) ->appendChild(array($head, $body, $foot)); } public function setAppIconFromPHID($phid) { - switch(phid_get_type($phid)) { + switch (phid_get_type($phid)) { case PhabricatorPHIDConstants::PHID_TYPE_MOCK: $this->setAppIcon("pholio-dark"); break; case PhabricatorPHIDConstants::PHID_TYPE_MCRO: $this->setAppIcon("macro-dark"); break; } } }