1<?php 2 3final class ArcanistAmendWorkflow 4 extends ArcanistArcWorkflow { 5 6 public function getWorkflowName() { 7 return 'amend'; 8 } 9 10 public function getWorkflowInformation() { 11 $help = pht(<<<EOTEXT 12Amend the working copy, synchronizing the local commit message from 13Differential. 14 15Supported in Mercurial 2.2 and newer. 16EOTEXT 17 ); 18 19 return $this->newWorkflowInformation() 20 ->setSynopsis( 21 pht('Amend the working copy, synchronizing the local commit message.')) 22 ->addExample('**amend** [options] -- ') 23 ->setHelp($help); 24 } 25 26 public function getWorkflowArguments() { 27 return array( 28 $this->newWorkflowArgument('show') 29 ->setHelp( 30 pht( 31 'Show the amended commit message, without modifying the '. 32 'working copy.')), 33 $this->newWorkflowArgument('revision') 34 ->setParameter('id') 35 ->setHelp( 36 pht( 37 'Use the message from a specific revision. If you do not specify '. 38 'a revision, arc will guess which revision is in the working '. 39 'copy.')), 40 ); 41 } 42 43 protected function newPrompts() { 44 return array( 45 $this->newPrompt('arc.amend.unrelated') 46 ->setDescription( 47 pht( 48 'Confirms use of a revision that does not appear to be '. 49 'present in the working copy.')), 50 $this->newPrompt('arc.amend.author') 51 ->setDescription( 52 pht( 53 'Confirms use of a revision that you are not the author '. 54 'of.')), 55 $this->newPrompt('arc.amend.immutable') 56 ->setDescription( 57 pht( 58 'Confirms history mutation in a working copy marked as '. 59 'immutable.')), 60 ); 61 } 62 63 public function runWorkflow() { 64 $symbols = $this->getSymbolEngine(); 65 66 $is_show = $this->getArgument('show'); 67 68 $repository_api = $this->getRepositoryAPI(); 69 if (!$is_show) { 70 $this->requireAmendSupport($repository_api); 71 } 72 73 $revision_symbol = $this->getArgument('revision'); 74 75 // We only care about the local working copy state if we need it to 76 // figure out which revision we're operating on, or we're planning to 77 // mutate it. If the caller is running "arc amend --show --revision X", 78 // the local state does not matter. 79 80 $need_state = 81 ($revision_symbol === null) || 82 (!$is_show); 83 84 if ($need_state) { 85 $state_ref = $repository_api->getCurrentWorkingCopyStateRef(); 86 87 $this->loadHardpoints( 88 $state_ref, 89 ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); 90 91 $revision_refs = $state_ref->getRevisionRefs(); 92 } 93 94 if ($revision_symbol === null) { 95 $revision_ref = $this->selectRevisionRef($revision_refs); 96 } else { 97 $revision_ref = $symbols->loadRevisionForSymbol($revision_symbol); 98 if (!$revision_ref) { 99 throw new PhutilArgumentUsageException( 100 pht( 101 'Revision "%s" does not exist, or you do not have permission '. 102 'to see it.', 103 $revision_symbol)); 104 } 105 } 106 107 if (!$is_show) { 108 echo tsprintf( 109 "%s\n\n%s\n", 110 pht('Amending commit message to reflect revision:'), 111 $revision_ref->newRefView()); 112 113 $this->confirmAmendAuthor($revision_ref); 114 $this->confirmAmendNotFound($revision_ref, $state_ref); 115 } 116 117 $this->loadHardpoints( 118 $revision_ref, 119 ArcanistRevisionRef::HARDPOINT_COMMITMESSAGE); 120 121 $message = $revision_ref->getCommitMessage(); 122 123 if ($is_show) { 124 echo tsprintf( 125 "%B\n", 126 $message); 127 } else { 128 $repository_api->amendCommit($message); 129 } 130 131 return 0; 132 } 133 134 private function requireAmendSupport(ArcanistRepositoryAPI $api) { 135 if (!$api->supportsAmend()) { 136 if ($api instanceof ArcanistMercurialAPI) { 137 throw new PhutilArgumentUsageException( 138 pht( 139 '"arc amend" is only supported under Mercurial 2.2 or newer. '. 140 'Older versions of Mercurial do not support the "--amend" flag '. 141 'to "hg commit ...", which this workflow requires.')); 142 } 143 144 throw new PhutilArgumentUsageException( 145 pht( 146 '"arc amend" must be run from inside a working copy of a '. 147 'repository using a version control system that supports '. 148 'amending commits, like Git or Mercurial.')); 149 } 150 151 if ($this->isHistoryImmutable()) { 152 echo tsprintf( 153 "%!\n\n%W\n", 154 pht('IMMUTABLE WORKING COPY'), 155 pht( 156 'This working copy is configured to have an immutable local '. 157 'history, using the "history.immutable" configuration option. '. 158 'Amending the working copy will mutate local history.')); 159 160 $prompt = pht('Are you sure you want to mutate history?'); 161 162 $this->getPrompt('arc.amend.immutable') 163 ->setQuery($prompt) 164 ->execute(); 165 } 166 167 return; 168 169 if ($api->getUncommittedChanges()) { 170 // TODO: Make this class of error show the uncommitted changes. 171 172 // TODO: This only needs to check for staged-but-uncommitted changes. 173 // We can safely amend with untracked and unstaged changes. 174 175 throw new PhutilArgumentUsageException( 176 pht( 177 'You have uncommitted changes in this working copy. Commit or '. 178 'revert them before proceeding.')); 179 } 180 } 181 182 private function selectRevisionRef(array $revisions) { 183 if (!$revisions) { 184 throw new PhutilArgumentUsageException( 185 pht( 186 'No revision specified with "--revision", and no revisions found '. 187 'that match the current working copy state. Use "--revision <id>" '. 188 'to specify which revision you want to amend.')); 189 } 190 191 if (count($revisions) > 1) { 192 echo tsprintf( 193 "%!\n%W\n\n%B\n", 194 pht('MULTIPLE REVISIONS IN WORKING COPY'), 195 pht('More than one revision was found in the working copy:'), 196 mpull($revisions, 'newRefView')); 197 198 throw new PhutilArgumentUsageException( 199 pht( 200 'Use "--revision <id>" to specify which revision you want '. 201 'to amend.')); 202 } 203 204 return head($revisions); 205 } 206 207 private function confirmAmendAuthor(ArcanistRevisionRef $revision_ref) { 208 $viewer = $this->getViewer(); 209 $viewer_phid = $viewer->getPHID(); 210 211 $author_phid = $revision_ref->getAuthorPHID(); 212 213 if ($viewer_phid === $author_phid) { 214 return; 215 } 216 217 $symbols = $this->getSymbolEngine(); 218 $author_ref = $symbols->loadUserForSymbol($author_phid); 219 if (!$author_ref) { 220 // If we don't have any luck loading the author, skip this warning. 221 return; 222 } 223 224 echo tsprintf( 225 "%!\n\n%W\n\n%s", 226 pht('NOT REVISION AUTHOR'), 227 array( 228 pht( 229 'You are amending the working copy using information from '. 230 'a revision you are not the author of.'), 231 "\n\n", 232 pht( 233 'The author of this revision (%s) is:', 234 $revision_ref->getMonogram()), 235 ), 236 $author_ref->newRefView()); 237 238 $prompt = pht( 239 'Amend working copy using revision owned by %s?', 240 $author_ref->getMonogram()); 241 242 $this->getPrompt('arc.amend.author') 243 ->setQuery($prompt) 244 ->execute(); 245 } 246 247 private function confirmAmendNotFound( 248 ArcanistRevisionRef $revision_ref, 249 ArcanistWorkingCopyStateRef $state_ref) { 250 251 $local_refs = $state_ref->getRevisionRefs(); 252 $local_refs = mpull($local_refs, null, 'getPHID'); 253 254 $revision_phid = $revision_ref->getPHID(); 255 $is_local = isset($local_refs[$revision_phid]); 256 257 if ($is_local) { 258 return; 259 } 260 261 echo tsprintf( 262 "%!\n\n%W\n", 263 pht('UNRELATED REVISION'), 264 pht( 265 'You are amending the working copy using information from '. 266 'a revision that does not appear to be associated with the '. 267 'current state of the working copy.')); 268 269 $prompt = pht( 270 'Amend working copy using unrelated revision %s?', 271 $revision_ref->getMonogram()); 272 273 $this->getPrompt('arc.amend.unrelated') 274 ->setQuery($prompt) 275 ->execute(); 276 } 277 278} 279