~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to src/configuration/ArcanistConfiguration.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-11-01 23:20:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: package-import@ubuntu.com-20141101232006-mvlnp0cil67tsboe
Tags: upstream-0~git20141101/arcanist
Import upstream version 0~git20141101, component arcanist

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Runtime workflow configuration. In Arcanist, commands you type like
 
5
 * "arc diff" or "arc lint" are called "workflows". This class allows you to add
 
6
 * new workflows (and extend existing workflows) by subclassing it and then
 
7
 * pointing to your subclass in your project configuration.
 
8
 *
 
9
 * When specified as the **arcanist_configuration** class in your project's
 
10
 * ##.arcconfig##, your subclass will be instantiated (instead of this class)
 
11
 * and be able to handle all the method calls. In particular, you can:
 
12
 *
 
13
 *    - create, replace, or disable workflows by overriding buildWorkflow()
 
14
 *      and buildAllWorkflows();
 
15
 *    - add additional steps before or after workflows run by overriding
 
16
 *      willRunWorkflow() or didRunWorkflow() or didAbortWorkflow(); and
 
17
 *    - add new flags to existing workflows by overriding
 
18
 *      getCustomArgumentsForCommand().
 
19
 *
 
20
 * @concrete-extensible
 
21
 */
 
22
class ArcanistConfiguration {
 
23
 
 
24
  public function buildWorkflow($command) {
 
25
    if ($command == '--help') {
 
26
      // Special-case "arc --help" to behave like "arc help" instead of telling
 
27
      // you to type "arc help" without being helpful.
 
28
      $command = 'help';
 
29
    } else if ($command == '--version') {
 
30
      // Special-case "arc --version" to behave like "arc version".
 
31
      $command = 'version';
 
32
    }
 
33
 
 
34
    return idx($this->buildAllWorkflows(), $command);
 
35
  }
 
36
 
 
37
  public function buildAllWorkflows() {
 
38
    $workflows_by_name = array();
 
39
 
 
40
    $workflows_by_class_name = id(new PhutilSymbolLoader())
 
41
      ->setAncestorClass('ArcanistWorkflow')
 
42
      ->loadObjects();
 
43
    foreach ($workflows_by_class_name as $class => $workflow) {
 
44
      $name = $workflow->getWorkflowName();
 
45
 
 
46
      if (isset($workflows_by_name[$name])) {
 
47
        $other = get_class($workflows_by_name[$name]);
 
48
        throw new Exception(
 
49
          "Workflows {$class} and {$other} both implement workflows named ".
 
50
          "{$name}.");
 
51
      }
 
52
 
 
53
      $workflows_by_name[$name] = $workflow;
 
54
    }
 
55
 
 
56
    return $workflows_by_name;
 
57
  }
 
58
 
 
59
  final public function isValidWorkflow($workflow) {
 
60
    return (bool)$this->buildWorkflow($workflow);
 
61
  }
 
62
 
 
63
  public function willRunWorkflow($command, ArcanistWorkflow $workflow) {
 
64
    // This is a hook.
 
65
  }
 
66
 
 
67
  public function didRunWorkflow($command, ArcanistWorkflow $workflow, $err) {
 
68
 
 
69
    // This is a hook.
 
70
  }
 
71
 
 
72
  public function didAbortWorkflow($command, $workflow, Exception $ex) {
 
73
    // This is a hook.
 
74
  }
 
75
 
 
76
  public function getCustomArgumentsForCommand($command) {
 
77
    return array();
 
78
  }
 
79
 
 
80
  final public function selectWorkflow(
 
81
    &$command,
 
82
    array &$args,
 
83
    ArcanistConfigurationManager $configuration_manager,
 
84
    PhutilConsole $console) {
 
85
 
 
86
    // First, try to build a workflow with the exact name provided. We always
 
87
    // pick an exact match, and do not allow aliases to override it.
 
88
    $workflow = $this->buildWorkflow($command);
 
89
    if ($workflow) {
 
90
      return $workflow;
 
91
    }
 
92
 
 
93
    // If the user has an alias, like 'arc alias dhelp diff help', look it up
 
94
    // and substitute it. We do this only after trying to resolve the workflow
 
95
    // normally to prevent you from doing silly things like aliasing 'alias'
 
96
    // to something else.
 
97
    $aliases = ArcanistAliasWorkflow::getAliases($configuration_manager);
 
98
    list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases(
 
99
      $command,
 
100
      $this,
 
101
      $args,
 
102
      $configuration_manager);
 
103
 
 
104
    $full_alias = idx($aliases, $command, array());
 
105
    $full_alias = implode(' ', $full_alias);
 
106
 
 
107
    // Run shell command aliases.
 
108
    if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) {
 
109
      $shell_cmd = substr($full_alias, 1);
 
110
 
 
111
      $console->writeLog(
 
112
        "[alias: 'arc %s' -> $ %s]",
 
113
        $command,
 
114
        $shell_cmd);
 
115
 
 
116
      if ($args) {
 
117
        $err = phutil_passthru('%C %Ls', $shell_cmd, $args);
 
118
      } else {
 
119
        $err = phutil_passthru('%C', $shell_cmd);
 
120
      }
 
121
 
 
122
      exit($err);
 
123
    }
 
124
 
 
125
    // Run arc command aliases.
 
126
    if ($new_command) {
 
127
      $workflow = $this->buildWorkflow($new_command);
 
128
      if ($workflow) {
 
129
        $console->writeLog(
 
130
          "[alias: 'arc %s' -> 'arc %s']\n",
 
131
          $command,
 
132
          $full_alias);
 
133
        $command = $new_command;
 
134
        return $workflow;
 
135
      }
 
136
    }
 
137
 
 
138
    $all = array_keys($this->buildAllWorkflows());
 
139
 
 
140
    // We haven't found a real command or an alias, so try to locate a command
 
141
    // by unique prefix.
 
142
    $prefixes = $this->expandCommandPrefix($command, $all);
 
143
 
 
144
    if (count($prefixes) == 1) {
 
145
      $command = head($prefixes);
 
146
      return $this->buildWorkflow($command);
 
147
    } else if (count($prefixes) > 1) {
 
148
      $this->raiseUnknownCommand($command, $prefixes);
 
149
    }
 
150
 
 
151
 
 
152
    // We haven't found a real command, alias, or unique prefix. Try similar
 
153
    // spellings.
 
154
    $corrected = self::correctCommandSpelling($command, $all, 2);
 
155
    if (count($corrected) == 1) {
 
156
      $console->writeErr(
 
157
        pht(
 
158
          "(Assuming '%s' is the British spelling of '%s'.)",
 
159
          $command,
 
160
          head($corrected))."\n");
 
161
      $command = head($corrected);
 
162
      return $this->buildWorkflow($command);
 
163
    } else if (count($corrected) > 1) {
 
164
      $this->raiseUnknownCommand($command, $corrected);
 
165
    }
 
166
 
 
167
    $this->raiseUnknownCommand($command);
 
168
  }
 
169
 
 
170
  private function raiseUnknownCommand($command, array $maybe = array()) {
 
171
    $message = pht("Unknown command '%s'. Try 'arc help'.", $command);
 
172
    if ($maybe) {
 
173
      $message .= "\n\n".pht('Did you mean:')."\n";
 
174
      sort($maybe);
 
175
      foreach ($maybe as $other) {
 
176
        $message .= "    ".$other."\n";
 
177
      }
 
178
    }
 
179
    throw new ArcanistUsageException($message);
 
180
  }
 
181
 
 
182
  private function expandCommandPrefix($command, array $options) {
 
183
    $is_prefix = array();
 
184
    foreach ($options as $option) {
 
185
      if (strncmp($option, $command, strlen($command)) == 0) {
 
186
        $is_prefix[$option] = true;
 
187
      }
 
188
    }
 
189
 
 
190
    return array_keys($is_prefix);
 
191
  }
 
192
 
 
193
  public static function correctCommandSpelling(
 
194
    $command,
 
195
    array $options,
 
196
    $max_distance) {
 
197
 
 
198
    // Adjust to the scaled edit costs we use below, so "2" roughly means
 
199
    // "2 edits".
 
200
    $max_distance = $max_distance * 3;
 
201
 
 
202
    // These costs are somewhat made up, but the theory is that it is far more
 
203
    // likely you will mis-strike a key ("lans" for "land") or press two keys
 
204
    // out of order ("alnd" for "land") than omit keys or press extra keys.
 
205
    $matrix = id(new PhutilEditDistanceMatrix())
 
206
      ->setInsertCost(4)
 
207
      ->setDeleteCost(4)
 
208
      ->setReplaceCost(3)
 
209
      ->setTransposeCost(2);
 
210
 
 
211
    return self::correctSpelling($command, $options, $matrix, $max_distance);
 
212
  }
 
213
 
 
214
  public static function correctArgumentSpelling($command, array $options) {
 
215
    $max_distance = 1;
 
216
 
 
217
    // We are stricter with arguments - we allow only one inserted or deleted
 
218
    // character. It is mainly to handle cases like --no-lint versus --nolint
 
219
    // or --reviewer versus --reviewers.
 
220
    $matrix = id(new PhutilEditDistanceMatrix())
 
221
      ->setInsertCost(1)
 
222
      ->setDeleteCost(1)
 
223
      ->setReplaceCost(10);
 
224
 
 
225
    return self::correctSpelling($command, $options, $matrix, $max_distance);
 
226
  }
 
227
 
 
228
  public static function correctSpelling(
 
229
    $input,
 
230
    array $options,
 
231
    PhutilEditDistanceMatrix $matrix,
 
232
    $max_distance) {
 
233
 
 
234
    $distances = array();
 
235
    $inputv = str_split($input);
 
236
    foreach ($options as $option) {
 
237
      $optionv = str_split($option);
 
238
      $matrix->setSequences($optionv, $inputv);
 
239
      $distances[$option] = $matrix->getEditDistance();
 
240
    }
 
241
 
 
242
    asort($distances);
 
243
    $best = min($max_distance, reset($distances));
 
244
    foreach ($distances as $option => $distance) {
 
245
      if ($distance > $best) {
 
246
        unset($distances[$option]);
 
247
      }
 
248
    }
 
249
 
 
250
    // Before filtering, check if we have multiple equidistant matches and
 
251
    // return them if we do. This prevents us from, e.g., matching "alnd" with
 
252
    // both "land" and "amend", then dropping "land" for being too short, and
 
253
    // incorrectly completing to "amend".
 
254
    if (count($distances) > 1) {
 
255
      return array_keys($distances);
 
256
    }
 
257
 
 
258
    foreach ($distances as $option => $distance) {
 
259
      if (strlen($option) < $distance) {
 
260
        unset($distances[$option]);
 
261
      }
 
262
    }
 
263
 
 
264
    return array_keys($distances);
 
265
  }
 
266
 
 
267
}