1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-22 06:42:41 +01:00

Improve handling of commit message on --auto/--update/--create workflows

Summary:
Our failure behavior currently sucks. In particular:

  - If lint or unit fail (expectedly or unexpectedly), we throw away your
message. omglol sux 2 b u
  - If there's a parsing error, we dump the message to 'arc-commit-message' in
CWD and tell you to go deal with it.

This is awful. Improve the behavior:

  - Once we read a message, save it in .arc/commit-message so we always have it
if anything goes wrong.
  - If that file exists, prompt to read it without any "-F" nonsense.
  - If there's a parsing error, tell the user and prompt them to just edit the
file again, showing what they need to fix.

Test Plan: Ran "arc diff --auto" and made a bunch of intentional errors; arc
never screwed me over and was generally fairly pleasant.

Reviewers: btrahan, Makinde

Reviewed By: btrahan

CC: aran, epriestley

Maniphest Tasks: T614, T895

Differential Revision: https://secure.phabricator.com/D1656
This commit is contained in:
epriestley 2012-02-21 12:35:39 -08:00
parent 4a0bd01550
commit 964bdb63ba
2 changed files with 241 additions and 27 deletions

View file

@ -37,7 +37,16 @@
* verified information about the user identity by calling @{method:getUserPHID}
* or @{method:getUserName} after authentication occurs.
*
* = Scratch Files =
*
* Arcanist workflows can read and write 'scratch files', which are temporary
* files stored in the project that persist across commands. They can be useful
* if you want to save some state, or keep a copy of a long message the user
* entered in something goes wrong..
*
*
* @task conduit Conduit
* @task scratch Scratch Files
* @group workflow
* @stable
*/
@ -601,6 +610,16 @@ abstract class ArcanistBaseWorkflow {
$untracked = $api->getUntrackedChanges();
if ($this->shouldRequireCleanUntrackedFiles()) {
// Exempt ".arc/" scratch files from this warning so that things work
// a little more smoothly if no one has gotten around to adding .arc to
// the ignore list.
foreach ($untracked as $key => $path) {
if (preg_match('@\.arc/@', $path)) {
unset($untracked[$key]);
}
}
if (!empty($untracked)) {
echo "You have untracked files in this working copy.\n\n".
$working_copy_desc.
@ -1002,4 +1021,125 @@ abstract class ArcanistBaseWorkflow {
return implode('', $list);
}
/* -( Scratch Files )------------------------------------------------------ */
/**
* Try to read a scratch file, if it exists and is readable.
*
* @param string Scratch file name.
* @return mixed String for file contents, or false for failure.
* @task scratch
*/
protected function readScratchFile($path) {
$full_path = $this->getScratchFilePath($path);
if (!$full_path) {
return false;
}
if (!Filesystem::pathExists($full_path)) {
return false;
}
try {
$result = Filesystem::readFile($full_path);
} catch (FilesystemException $ex) {
return false;
}
return $result;
}
/**
* Try to write a scratch file, if there's somewhere to put it and we can
* write there.
*
* @param string Scratch file name to write.
* @param string Data to write.
* @return bool True on success, false on failure.
* @task scratch
*/
protected function writeScratchFile($path, $data) {
$dir = $this->getScratchFilePath('');
if (!$dir) {
return false;
}
if (!Filesystem::pathExists($dir)) {
try {
execx('mkdir %s', $dir);
} catch (Exception $ex) {
return false;
}
}
try {
Filesystem::writeFile($this->getScratchFilePath($path), $data);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
/**
* Try to remove a scratch file.
*
* @param string Scratch file name to remove.
* @return bool True if the file was removed successfully.
* @task scratch
*/
protected function removeScratchFile($path) {
$full_path = $this->getScratchFilePath($path);
if (!$full_path) {
return false;
}
try {
Filesystem::remove($full_path);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
/**
* Get a human-readable description of the scratch file location.
*
* @param string Scratch file name.
* @return mixed String, or false on failure.
* @task scratch
*/
protected function getReadableScratchFilePath($path) {
$full_path = $this->getScratchFilePath($path);
if ($full_path) {
return Filesystem::readablePath(
$full_path,
$this->getRepositoryAPI()->getPath());
} else {
return false;
}
}
/**
* Get the path to a scratch file, if possible.
*
* @param string Scratch file name.
* @return mixed File path, or false on failure.
* @task scratch
*/
protected function getScratchFilePath($path) {
if (!$this->repositoryAPI) {
return false;
}
$repository_api = $this->getRepositoryAPI();
return $repository_api->getPath('.arc/'.$path);
}
}

View file

@ -463,6 +463,8 @@ EOTEXT
ob_get_clean();
}
$this->removeScratchFile('create-message');
return 0;
}
@ -1185,37 +1187,109 @@ EOTEXT
private function getCommitMessageFromUser() {
$conduit = $this->getConduit();
$template = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => null,
'edit' => true,
));
$template = null;
$template =
$template.
"\n\n".
"# Describe this revision.".
"\n";
$template = id(new PhutilInteractiveEditor($template))
->setName('new-commit')
->editInteractively();
$template = preg_replace('/^\s*#.*$/m', '', $template);
$saved = $this->readScratchFile('create-message');
if ($saved) {
$where = $this->getReadableScratchFilePath('create-message');
try {
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$template);
$message->pullDataFromConduit($conduit);
$this->validateCommitMessage($message);
} catch (Exception $ex) {
$path = Filesystem::writeUniqueFile('arc-commit-message', $template);
$preview = explode("\n", $saved);
$preview = array_shift($preview);
$preview = trim($preview);
$preview = phutil_utf8_shorten($preview, 64);
echo phutil_console_wrap(
"\n".
"Exception while parsing commit message! Message saved to ".
"'{$path}'. Use -F <file> to specify a commit message file.\n");
if ($preview) {
$preview = "Message begins:\n\n {$preview}\n\n";
} else {
$preview = null;
}
throw $ex;
echo
"You have a saved revision message in '{$where}'.\n".
"{$preview}".
"You can use this message, or discard it.";
$use = phutil_console_confirm(
"Do you want to use this message?",
$default_no = false);
if ($use) {
$template = $saved;
} else {
$this->removeScratchFile('create-message');
}
}
$template_is_default = false;
if (!$template) {
$template = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => null,
'edit' => true,
));
$template_is_default = true;
}
$issues = array('Describe this revision.');
$done = false;
while (!$done) {
$template = rtrim($template)."\n\n";
foreach ($issues as $issue) {
$template .= '# '.$issue."\n";
}
$template .= "\n";
$new_template = id(new PhutilInteractiveEditor($template))
->setName('new-commit')
->editInteractively();
if ($template_is_default && ($new_template == $template)) {
throw new ArcanistUsageException(
"Template not edited.");
}
$template = preg_replace('/^\s*#.*$/m', '', $new_template);
$template = rtrim($template)."\n";
$wrote = $this->writeScratchFile('create-message', $template);
$where = $this->getReadableScratchFilePath('create-message');
try {
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$template);
$message->pullDataFromConduit($conduit);
$this->validateCommitMessage($message);
$done = true;
} catch (ArcanistDifferentialCommitMessageParserException $ex) {
echo "Commit message has errors:\n\n";
$issues = array('Resolve these errors:');
foreach ($ex->getParserErrors() as $error) {
echo " - ".$error."\n";
$issues[] = ' - '.$error;
}
echo "\n";
echo "You must resolve these errors to continue.";
$again = phutil_console_confirm(
"Do you want to edit the message?",
$default_no = false);
if ($again) {
// Keep going.
} else {
$saved = null;
if ($wrote) {
$saved = "A copy was saved to '{$where}'.";
}
throw new ArcanistUsageException(
'Message has unresolved errrors. {$saved}');
}
} catch (Exception $ex) {
if ($wrote) {
echo phutil_console_wrap("(Commit messaged saved to '{$where}'.)");
}
throw $ex;
}
}
return $message;