1<?php
2
3abstract class ArcanistRepositoryLocalState
4  extends Phobject {
5
6  private $repositoryAPI;
7  private $shouldRestore;
8  private $stashRef;
9  private $workflow;
10
11  final public function setWorkflow(ArcanistWorkflow $workflow) {
12    $this->workflow = $workflow;
13    return $this;
14  }
15
16  final public function getWorkflow() {
17    return $this->workflow;
18  }
19
20  final public function setRepositoryAPI(ArcanistRepositoryAPI $api) {
21    $this->repositoryAPI = $api;
22    return $this;
23  }
24
25  final public function getRepositoryAPI() {
26    return $this->repositoryAPI;
27  }
28
29  final public function saveLocalState() {
30    $api = $this->getRepositoryAPI();
31
32    $working_copy_display = tsprintf(
33      "    %s: %s\n",
34      pht('Working Copy'),
35      $api->getPath());
36
37    $conflicts = $api->getMergeConflicts();
38    if ($conflicts) {
39      echo tsprintf(
40        "\n%!\n%W\n\n%s\n",
41        pht('MERGE CONFLICTS'),
42        pht('You have merge conflicts in this working copy.'),
43        $working_copy_display);
44
45      $lists = array();
46
47      $lists[] = $this->newDisplayFileList(
48        pht('Merge conflicts in working copy:'),
49        $conflicts);
50
51      $this->printFileLists($lists);
52
53      throw new PhutilArgumentUsageException(
54        pht(
55          'Resolve merge conflicts before proceeding.'));
56    }
57
58    $externals = $api->getDirtyExternalChanges();
59    if ($externals) {
60      $message = pht(
61        '%s submodule(s) have uncommitted or untracked changes:',
62        new PhutilNumber(count($externals)));
63
64      $prompt = pht(
65        'Ignore the changes to these %s submodule(s) and continue?',
66        new PhutilNumber(count($externals)));
67
68      $list = id(new PhutilConsoleList())
69        ->setWrap(false)
70        ->addItems($externals);
71
72      id(new PhutilConsoleBlock())
73        ->addParagraph($message)
74        ->addList($list)
75        ->draw();
76
77      $ok = phutil_console_confirm($prompt, $default_no = false);
78      if (!$ok) {
79        throw new ArcanistUserAbortException();
80      }
81    }
82
83    $uncommitted = $api->getUncommittedChanges();
84    $unstaged = $api->getUnstagedChanges();
85    $untracked = $api->getUntrackedChanges();
86
87    // We already dealt with externals.
88    $unstaged = array_diff($unstaged, $externals);
89
90    // We only want files which are purely uncommitted.
91    $uncommitted = array_diff($uncommitted, $unstaged);
92    $uncommitted = array_diff($uncommitted, $externals);
93
94    if ($untracked || $unstaged || $uncommitted) {
95      echo tsprintf(
96        "\n%!\n%W\n\n%s\n",
97        pht('UNCOMMITTED CHANGES'),
98        pht('You have uncommitted changes in this working copy.'),
99        $working_copy_display);
100
101      $lists = array();
102
103      $lists[] = $this->newDisplayFileList(
104        pht('Untracked changes in working copy:'),
105        $untracked);
106
107      $lists[] = $this->newDisplayFileList(
108        pht('Unstaged changes in working copy:'),
109        $unstaged);
110
111      $lists[] = $this->newDisplayFileList(
112        pht('Uncommitted changes in working copy:'),
113        $uncommitted);
114
115      $this->printFileLists($lists);
116
117      if ($untracked) {
118        $hints = $this->getIgnoreHints();
119        foreach ($hints as $hint) {
120          echo tsprintf("%?\n", $hint);
121        }
122      }
123
124      if ($this->canStashChanges()) {
125
126        $query = pht('Stash these changes and continue?');
127
128        $this->getWorkflow()
129          ->getPrompt('arc.state.stash')
130          ->setQuery($query)
131          ->execute();
132
133        $stash_ref = $this->saveStash();
134
135        if ($stash_ref === null) {
136          throw new Exception(
137            pht(
138              'Expected a non-null return from call to "%s->saveStash()".',
139              get_class($this)));
140        }
141
142        $this->stashRef = $stash_ref;
143      } else {
144        throw new PhutilArgumentUsageException(
145          pht(
146            'You can not continue with uncommitted changes. Commit or '.
147            'discard them before proceeding.'));
148      }
149    }
150
151    $this->executeSaveLocalState();
152    $this->shouldRestore = true;
153
154    // TODO: Detect when we're in the middle of a rebase.
155    // TODO: Detect when we're in the middle of a cherry-pick.
156
157    return $this;
158  }
159
160  final public function restoreLocalState() {
161    $this->shouldRestore = false;
162
163    $this->executeRestoreLocalState();
164    $this->applyStash();
165    $this->executeDiscardLocalState();
166
167    return $this;
168  }
169
170  final public function discardLocalState() {
171    $this->shouldRestore = false;
172
173    $this->applyStash();
174    $this->executeDiscardLocalState();
175
176    return $this;
177  }
178
179  final public function __destruct() {
180    if ($this->shouldRestore) {
181      $this->restoreLocalState();
182    } else {
183      $this->discardLocalState();
184    }
185  }
186
187  final public function getRestoreCommandsForDisplay() {
188    return $this->newRestoreCommandsForDisplay();
189  }
190
191  protected function canStashChanges() {
192    return false;
193  }
194
195  protected function saveStash() {
196    throw new PhutilMethodNotImplementedException();
197  }
198
199  protected function restoreStash($ref) {
200    throw new PhutilMethodNotImplementedException();
201  }
202
203  protected function discardStash($ref) {
204    throw new PhutilMethodNotImplementedException();
205  }
206
207  private function applyStash() {
208    if ($this->stashRef === null) {
209      return;
210    }
211    $stash_ref = $this->stashRef;
212    $this->stashRef = null;
213
214    $this->restoreStash($stash_ref);
215    $this->discardStash($stash_ref);
216  }
217
218  abstract protected function executeSaveLocalState();
219  abstract protected function executeRestoreLocalState();
220  abstract protected function executeDiscardLocalState();
221  abstract protected function newRestoreCommandsForDisplay();
222
223  protected function getIgnoreHints() {
224    return array();
225  }
226
227  final protected function newDisplayFileList($title, array $files) {
228    if (!$files) {
229      return null;
230    }
231
232    $items = array();
233    $items[] = tsprintf("%s\n\n", $title);
234    foreach ($files as $file) {
235      $items[] = tsprintf(
236        "    %s\n",
237        $file);
238    }
239
240    return $items;
241  }
242
243  final protected function printFileLists(array $lists) {
244    $lists = array_filter($lists);
245
246    $last_key = last_key($lists);
247    foreach ($lists as $key => $list) {
248      foreach ($list as $item) {
249        echo tsprintf('%B', $item);
250      }
251      if ($key !== $last_key) {
252        echo tsprintf("\n\n");
253      }
254    }
255
256    echo tsprintf("\n");
257  }
258
259}
260