Page MenuHomePhabricator

D14104.id34088.diff
No OneTemporary

D14104.id34088.diff

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

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)

Event Timeline