Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15437574
D14104.id34088.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D14104.id34088.diff
View Options
diff --git a/differential_extension/DifferentialGetInlineCommentsConduitAPIMethod.php b/differential_extension/DifferentialGetInlineCommentsConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/differential_extension/DifferentialGetInlineCommentsConduitAPIMethod.php
@@ -0,0 +1,107 @@
+<?php
+
+final class DifferentialGetInlineCommentsConduitAPIMethod
+ extends DifferentialConduitAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'differential.getinlinecomments';
+ }
+
+ public function getMethodStatus() {
+ return self::METHOD_STATUS_UNSTABLE;
+ }
+
+ public function getMethodStatusDescription() {
+ return pht('In extensions/. Used for arc revise');
+ }
+
+ public function getMethodDescription() {
+ return pht('Get all inline comments on a revision, sorted by diff.');
+ }
+
+ protected function defineParamTypes() {
+ return array(
+ 'id' => 'required int',
+ );
+ }
+
+ protected function defineReturnType() {
+ return 'wild'; // nonempty dict<int, list<wild>>';
+ }
+
+ protected function execute(ConduitAPIRequest $request) {
+ $viewer = $request->getUser();
+ $revision_id = $request->getValue('id');
+
+ if (!$revision_id) {
+ return array();
+ }
+
+ $revisions = id(new DifferentialRevisionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($revision_id))
+ ->execute();
+
+ $xactions = id(new DifferentialTransactionQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(mpull($revisions, 'getPHID'))
+ ->execute();
+
+ $comments = array();
+ foreach ($xactions as $xact) {
+ if ($xact->isInlineCommentTransaction()) {
+ $comments[] = $xact->getComment();
+ }
+ }
+
+ $changeset_ids = array_unique(mpull($comments, 'getChangesetID'));
+
+ $changesets = id(new DifferentialChangesetQuery())
+ ->setViewer($viewer)
+ ->withIDs($changeset_ids)
+ ->needHunks(true)
+ ->execute();
+
+ $ret = array();
+ foreach ($comments as $comment) {
+ if ($comment->getIsDeleted()) {
+ continue;
+ }
+
+ $changeset = idx($changesets, $comment->getChangesetID());
+ if (!$changeset) {
+ continue;
+ }
+
+ $file = $changeset->makeNewFile();
+ $commented_text = null;
+ if ($file) {
+ $commented_lines = array_slice(
+ explode("\n", $file),
+ $comment->getLineNumber() - 1,
+ $comment->getLineLength() + 1);
+
+ $commented_text = implode("\n", $commented_lines)."\n";
+ }
+
+ $diff_id = $changeset->getDiffID();
+ $ret[$diff_id][] = array(
+ 'phid' => $comment->getTransactionPHID(),
+ 'authorPHID' => $comment->getAuthorPHID(),
+ 'content' => $comment->getContent(),
+ 'contentSource' => $comment->getContentSource() ?
+ $comment->getContentSource()->getSource() : null,
+ 'filename' => $changeset->getFilename(),
+ 'changeType' => DifferentialChangeType::getFullNameForChangeType(
+ $changeset->getChangeType()),
+ 'lineNumber' => $comment->getLineNumber(),
+ 'lineLength' => $comment->getLineLength(),
+ 'commentedLines' => $commented_text,
+ 'repliedID' => $comment->getHasReplies() ?
+ $comment->getReplyToCommentPHID() : null,
+ );
+ }
+
+ return $ret;
+ }
+}
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -215,6 +215,7 @@
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php',
'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php',
'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.php',
+ 'ArcanistReviseWorkflow' => 'workflow/ArcanistReviseWorkflow.php',
'ArcanistRuboCopLinter' => 'lint/linter/ArcanistRuboCopLinter.php',
'ArcanistRuboCopLinterTestCase' => 'lint/linter/__tests__/ArcanistRuboCopLinterTestCase.php',
'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php',
@@ -501,6 +502,7 @@
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistRevertWorkflow' => 'ArcanistWorkflow',
+ 'ArcanistReviseWorkflow' => 'ArcanistWorkflow',
'ArcanistRuboCopLinter' => 'ArcanistExternalLinter',
'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRubyLinter' => 'ArcanistExternalLinter',
diff --git a/src/workflow/ArcanistReviseWorkflow.php b/src/workflow/ArcanistReviseWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/workflow/ArcanistReviseWorkflow.php
@@ -0,0 +1,249 @@
+<?php
+
+/**
+ * Patches in inline comments from differential
+ */
+final class ArcanistReviseWorkflow extends ArcanistWorkflow {
+
+ public function getWorkflowName() {
+ return 'revise`';
+ }
+
+ public function getCommandSynopses() {
+ return phutil_console_format(<<<EOTEXT
+ **revise**
+EOTEXT
+ );
+ }
+
+ public function getCommandHelp() {
+ return phutil_console_format(<<<EOTEXT
+ Supports: git, hg
+ Runs amend, then patches in any inline comments in Differential that
+ start with a code block. Allows a more collaborative editing process.
+
+ Supported in Mercurial 2.2 and newer (same restrictions as amend)
+EOTEXT
+ );
+ }
+
+ public function requiresWorkingCopy() {
+ return true;
+ }
+
+ public function requiresConduit() {
+ return true;
+ }
+
+ public function requiresAuthentication() {
+ return true;
+ }
+
+ public function requiresRepositoryAPI() {
+ return true;
+ }
+
+ public function getArguments() {
+ return array(
+ );
+ }
+
+ public function run() {
+ $console = PhutilConsole::getConsole();
+
+ $amend_workflow = $this->buildChildWorkflow(
+ 'amend',
+ array());
+ $amend_workflow->run();
+
+ $repository_api = $this->getRepositoryAPI();
+ $repository_api->setBaseCommitArgumentRules('arc:this');
+ $in_working_copy = $repository_api->loadWorkingCopyDifferentialRevisions(
+ $this->getConduit(),
+ array(
+ 'status' => 'status-any',
+ ));
+ $in_working_copy = ipull($in_working_copy, null, 'id');
+
+ if (count($in_working_copy) == 0) {
+ throw new ArcanistUsageException(
+ pht(
+ 'No revisions found '.
+ 'in the working copy. We may soon add --revision so you can specify which revision to use')
+ );
+ } else if (count($in_working_copy) > 1) {
+ $message = pht(
+ "More than one revision was found in the working copy. ".
+ "I don't know how to deal with this yet");
+ throw new ArcanistUsageException($message);
+ }
+ $revision_id = key($in_working_copy);
+ $revision = $in_working_copy[$revision_id];
+
+ $conduit = $this->getConduit();
+ $comments = $conduit->callMethodSynchronous(
+ 'differential.getinlinecomments',
+ array(
+ 'id' => $revision_id,
+ ));
+
+ $results = array();
+ foreach ($comments as $diff_id => $diff_comments) {
+ foreach ($diff_comments as $comment) {
+ $raw_replacement_text = $this->getCodeBlockAtBeginning($comment['content']);
+ if (!$raw_replacement_text) {
+ continue;
+ }
+
+ if (!idx($results, $comment['filename'])) {
+ $file_path_on_disk = Filesystem::resolvePath(
+ $comment['filename'],
+ $this->getWorkingCopy()->getProjectRoot());
+
+ $file_data = Filesystem::readFile($file_path_on_disk);
+
+ $new_result = id(new ArcanistLintResult())
+ ->setPath($comment['filename'])
+ ->setFilePathOnDisk($file_path_on_disk)
+ ->setData($file_data);
+
+ $results[$comment['filename']] = $new_result;
+ }
+ $result = $results[$comment['filename']];
+
+ $file_lines = explode("\n", $result->getData());
+
+ $original_lines = array_slice($file_lines,
+ $comment['lineNumber'] - 1,
+ $comment['lineLength'] + 1);
+
+ $original_text = implode("\n", $original_lines);
+
+ if (strpos($original_text, trim($comment['commentedLines'])) === false) {
+ $console->writeOut("Skipping patch written for outdated code\n");
+ continue;
+ }
+
+ // TODO: verify the original text that the comment author saw is the
+ // same as the current original text
+
+ $replacement_text = $this->matchIndentation(
+ $original_text, $raw_replacement_text);
+
+ $message = id(new ArcanistLintMessage())
+ ->setPath($comment['filename'])
+ ->setLine($comment['lineNumber'])
+ ->setChar(1)
+ ->setCode('PATCH')
+ ->setName('Inline Patch')
+ ->setSeverity(ArcanistLintSeverity::SEVERITY_AUTOFIX)
+ ->setDescription('Comment on diff '.$diff_id)
+ ->setOriginalText($original_text)
+ ->setReplacementText($replacement_text);
+
+ $results[$comment['filename']]->addMessage($message);
+ }
+ }
+
+ // we might have created files that have no patchable comments
+ foreach ($results as $filename => $result) {
+ if (!$result->getMessages()) {
+ unset($results[$filename]);
+ }
+ }
+
+ $patchers = array();
+ $diffs = array();
+ foreach ($results as $filename => $result) {
+ $patchers[$filename] = ArcanistLintPatcher::newFromArcanistLintResult(
+ $result);
+
+ // run diff on the applied patches
+ $temp_patched_file = new TempFile();
+ Filesystem::writeFile(
+ $temp_patched_file,
+ $patchers[$filename]->getModifiedFileContent());
+
+ list(, $stdout, $stderr) = exec_manual(
+ 'diff -u %s %s --label %s --label %s',
+ $result->getFilePathOnDisk(),
+ $temp_patched_file,
+ $result->getPath(),
+ 'Patched');
+
+ $diffs[] = $stdout;
+ }
+
+ $console->writeOut('%s', implode("\n\n", $diffs));
+
+ $prompt = pht('Apply patches?');
+
+ if (!$console->confirm($prompt, $default = true)) {
+ return;
+ }
+
+ foreach ($patchers as $patcher) {
+ $patcher->writePatchToDisk();
+ }
+
+ $console->writeOut("Done.\n");
+
+ return 0;
+ }
+
+ public function getCodeBlockAtBeginning($content) {
+ $lines = explode("\n", $content);
+
+ $cursor = 0;
+ $code_block_rule = new PhutilRemarkupCodeBlockRule();
+ $line_count = $code_block_rule->getMatchingLineCount($lines, $cursor);
+
+ if ($line_count === 0) {
+ return null;
+ }
+
+ $replacement_lines = array();
+ for ($i = 0; $i < $line_count; $i++) {
+ // if a line only consists of a ```, we want to skip it.
+ // even if it doesn't, we still want to delete all ```,
+ // since a single line could have two of these.
+ if (trim($lines[$i]) !== '```') {
+ $replacement_lines[] = str_replace('```', '', $lines[$i]);
+ }
+ }
+
+ return implode("\n", $replacement_lines);
+ }
+
+ public function matchIndentation($original, $replacement) {
+ // TODO: assumes no line is less indented than the first (bad things happen
+ // right now if that's not true). Probably the right behavior is to treat
+ // all indents <= the first line indent at 0 (relative to the original
+ // indent)
+ $old_lines = explode("\n", $original);
+ $new_lines = explode("\n", $replacement);
+
+ // first, set the replacement text to a base indent of 0.
+ $new_indent = strlen($new_lines[0]) - strlen(ltrim($new_lines[0]));
+ $updated_new_lines = array();
+ foreach ($new_lines as $line) {
+ $updated_new_lines[] = substr($line, $new_indent);
+ }
+ $new_lines = $updated_new_lines;
+
+ // now, add the indent of the original text
+ $old_indent = strlen($old_lines[0]) - strlen(ltrim($old_lines[0]));
+
+ $updated_new_lines = array();
+ foreach ($new_lines as $idx => $line) {
+ $updated_new_lines[] = str_repeat(' ', $old_indent).$line;
+ }
+ $new_lines = $updated_new_lines;
+
+ return implode("\n", $new_lines);
+ }
+
+ public function getSupportedRevisionControlSystems() {
+ return array('git', 'hg');
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Mar 26, 8:29 PM (2 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7418162
Default Alt Text
D14104.id34088.diff (12 KB)
Attached To
Mode
D14104: arc revise: more collaborative code reviews (NOT FOR UPSTREAM)
Attached
Detach File
Event Timeline
Log In to Comment