1<?php
2
3final class ArcanistBaseCommitParser extends Phobject {
4
5  private $api;
6  private $try;
7  private $verbose = false;
8
9  public function __construct(ArcanistRepositoryAPI $api) {
10    $this->api = $api;
11  }
12
13  private function tokenizeBaseCommitSpecification($raw_spec) {
14    if (!$raw_spec) {
15      return array();
16    }
17
18    $spec = preg_split('/\s*,\s*/', $raw_spec);
19    $spec = array_filter($spec);
20
21    foreach ($spec as $rule) {
22      if (strpos($rule, ':') === false) {
23        throw new ArcanistUsageException(
24          pht(
25            "Rule '%s' is invalid, it must have a type and name like '%s'.",
26            $rule,
27            'arc:upstream'));
28      }
29    }
30
31    return $spec;
32  }
33
34  private function log($message) {
35    if ($this->verbose) {
36      fwrite(STDERR, $message."\n");
37    }
38  }
39
40  public function resolveBaseCommit(array $specs) {
41    $specs += array(
42      'runtime' => '',
43      'local'   => '',
44      'project' => '',
45      'user'    => '',
46      'system'  => '',
47    );
48
49    foreach ($specs as $source => $spec) {
50      $specs[$source] = self::tokenizeBaseCommitSpecification($spec);
51    }
52
53    $this->try = array(
54      'runtime',
55      'local',
56      'project',
57      'user',
58      'system',
59    );
60
61    while ($this->try) {
62      $source = head($this->try);
63
64      if (!idx($specs, $source)) {
65        $this->log(pht("No rules left from source '%s'.", $source));
66        array_shift($this->try);
67        continue;
68      }
69
70      $this->log(pht("Trying rules from source '%s'.", $source));
71
72      $rules = &$specs[$source];
73      while ($rule = array_shift($rules)) {
74        $this->log(pht("Trying rule '%s'.", $rule));
75
76        $commit = $this->resolveRule($rule, $source);
77
78        if ($commit === false) {
79          // If a rule returns false, it means to go to the next ruleset.
80          break;
81        } else if ($commit !== null) {
82          $this->log(pht(
83            "Resolved commit '%s' from rule '%s'.",
84            $commit,
85            $rule));
86          return $commit;
87        }
88      }
89    }
90
91    return null;
92  }
93
94  /**
95   * Handle resolving individual rules.
96   */
97  private function resolveRule($rule, $source) {
98    // NOTE: Returning `null` from this method means "no match".
99    // Returning `false` from this method means "stop current ruleset".
100
101    list($type, $name) = explode(':', $rule, 2);
102    switch ($type) {
103      case 'literal':
104        return $name;
105      case 'git':
106      case 'hg':
107        return $this->api->resolveBaseCommitRule($rule, $source);
108      case 'arc':
109        return $this->resolveArcRule($rule, $name, $source);
110      default:
111        throw new ArcanistUsageException(
112          pht(
113            "Base commit rule '%s' (from source '%s') ".
114            "is not a recognized rule.",
115            $rule,
116            $source));
117    }
118  }
119
120
121  /**
122   * Handle resolving "arc:*" rules.
123   */
124  private function resolveArcRule($rule, $name, $source) {
125    $name = $this->updateLegacyRuleName($name);
126
127    switch ($name) {
128      case 'verbose':
129        $this->verbose = true;
130        $this->log(pht('Enabled verbose mode.'));
131        break;
132      case 'prompt':
133        $reason = pht('it is what you typed when prompted.');
134        $this->api->setBaseCommitExplanation($reason);
135        $result = phutil_console_prompt(pht('Against which commit?'));
136        if (!strlen($result)) {
137          // Allow the user to continue to the next rule by entering no
138          // text.
139          return null;
140        }
141        return $result;
142      case 'local':
143      case 'user':
144      case 'project':
145      case 'runtime':
146      case 'system':
147        // Push the other source on top of the list.
148        array_unshift($this->try, $name);
149        $this->log(pht("Switching to source '%s'.", $name));
150        return false;
151      case 'yield':
152        // Cycle this source to the end of the list.
153        $this->try[] = array_shift($this->try);
154        $this->log(pht("Yielding processing of rules from '%s'.", $source));
155        return false;
156      case 'halt':
157        // Dump the whole stack.
158        $this->try = array();
159        $this->log(pht('Halting all rule processing.'));
160        return false;
161      case 'skip':
162        return null;
163      case 'empty':
164      case 'upstream':
165      case 'outgoing':
166      case 'bookmark':
167      case 'amended':
168      case 'this':
169        return $this->api->resolveBaseCommitRule($rule, $source);
170      default:
171        $matches = null;
172        if (preg_match('/^exec\((.*)\)$/', $name, $matches)) {
173          $root = $this->api->getWorkingCopyIdentity()->getProjectRoot();
174          $future = new ExecFuture('%C', $matches[1]);
175          $future->setCWD($root);
176          list($err, $stdout) = $future->resolve();
177          if (!$err) {
178            return trim($stdout);
179          } else {
180            return null;
181          }
182        } else if (preg_match('/^nodiff\((.*)\)$/', $name, $matches)) {
183          return $this->api->resolveBaseCommitRule($rule, $source);
184        }
185
186        throw new ArcanistUsageException(
187          pht(
188            "Base commit rule '%s' (from source '%s') ".
189            "is not a recognized rule.",
190            $rule,
191            $source));
192    }
193  }
194
195  private function updateLegacyRuleName($name) {
196    $updated = array(
197      'global' => 'user',
198      'args'   => 'runtime',
199    );
200    $new_name = idx($updated, $name);
201    if ($new_name) {
202      $this->log(pht("Translating legacy name '%s' to '%s'", $name, $new_name));
203      return $new_name;
204    }
205    return $name;
206  }
207
208}
209