1<?php
2
3/**
4 * Represents a change to an individual path.
5 */
6final class ArcanistDiffChange extends Phobject {
7
8  protected $metadata = array();
9
10  protected $oldPath;
11  protected $currentPath;
12  protected $awayPaths = array();
13
14  protected $oldProperties = array();
15  protected $newProperties = array();
16
17  protected $commitHash;
18  protected $type = ArcanistDiffChangeType::TYPE_CHANGE;
19  protected $fileType = ArcanistDiffChangeType::FILE_TEXT;
20
21  protected $hunks = array();
22
23  private $needsSyntheticGitHunks;
24
25  private $currentFileData;
26  private $originalFileData;
27
28  public function setOriginalFileData($original_file_data) {
29    $this->originalFileData = $original_file_data;
30    return $this;
31  }
32
33  public function getOriginalFileData() {
34    return $this->originalFileData;
35  }
36
37  public function setCurrentFileData($current_file_data) {
38    $this->currentFileData = $current_file_data;
39    return $this;
40  }
41
42  public function getCurrentFileData() {
43    return $this->currentFileData;
44  }
45
46  public function toDictionary() {
47    $hunks = array();
48    foreach ($this->hunks as $hunk) {
49      $hunks[] = $hunk->toDictionary();
50    }
51
52    return array(
53      'metadata'      => $this->metadata,
54      'oldPath'       => $this->oldPath,
55      'currentPath'   => $this->currentPath,
56      'awayPaths'     => $this->awayPaths,
57      'oldProperties' => $this->oldProperties,
58      'newProperties' => $this->newProperties,
59      'type'          => $this->type,
60      'fileType'      => $this->fileType,
61      'commitHash'    => $this->commitHash,
62      'hunks'         => $hunks,
63    );
64  }
65
66  public static function newFromDictionary(array $dict) {
67    $hunks = array();
68    foreach ($dict['hunks'] as $hunk) {
69      $hunks[] = ArcanistDiffHunk::newFromDictionary($hunk);
70    }
71
72    $obj = new ArcanistDiffChange();
73    $obj->metadata = $dict['metadata'];
74    $obj->oldPath = $dict['oldPath'];
75    $obj->currentPath = $dict['currentPath'];
76    // TODO: The backend is shipping down some bogus data, e.g. diff 199453.
77    // Should probably clean this up.
78    $obj->awayPaths     = nonempty($dict['awayPaths'],     array());
79    $obj->oldProperties = nonempty($dict['oldProperties'], array());
80    $obj->newProperties = nonempty($dict['newProperties'], array());
81    $obj->type = $dict['type'];
82    $obj->fileType = $dict['fileType'];
83    $obj->commitHash = $dict['commitHash'];
84    $obj->hunks = $hunks;
85
86    return $obj;
87  }
88
89  public static function newFromConduit(array $dicts) {
90    $changes = array();
91    foreach ($dicts as $dict) {
92      $changes[] = self::newFromDictionary($dict);
93    }
94    return $changes;
95  }
96
97  public function getChangedLines($type) {
98    $lines = array();
99    foreach ($this->hunks as $hunk) {
100      $lines += $hunk->getChangedLines($type);
101    }
102    return $lines;
103  }
104
105  public function getAllMetadata() {
106    return $this->metadata;
107  }
108
109  public function setMetadata($key, $value) {
110    $this->metadata[$key] = $value;
111    return $this;
112  }
113
114  public function getMetadata($key) {
115    return idx($this->metadata, $key);
116  }
117
118  public function setCommitHash($hash) {
119    $this->commitHash = $hash;
120    return $this;
121  }
122
123  public function getCommitHash() {
124    return $this->commitHash;
125  }
126
127  public function addAwayPath($path) {
128    $this->awayPaths[] = $path;
129    return $this;
130  }
131
132  public function getAwayPaths() {
133    return $this->awayPaths;
134  }
135
136  public function setFileType($type) {
137    $this->fileType = $type;
138    return $this;
139  }
140
141  public function getFileType() {
142    return $this->fileType;
143  }
144
145  public function setType($type) {
146    $this->type = $type;
147    return $this;
148  }
149
150  public function getType() {
151    return $this->type;
152  }
153
154  public function setOldProperty($key, $value) {
155    $this->oldProperties[$key] = $value;
156    return $this;
157  }
158
159  public function setNewProperty($key, $value) {
160    $this->newProperties[$key] = $value;
161    return $this;
162  }
163
164  public function getOldProperties() {
165    return $this->oldProperties;
166  }
167
168  public function getNewProperties() {
169    return $this->newProperties;
170  }
171
172  public function setCurrentPath($path) {
173    $this->currentPath = $this->filterPath($path);
174    return $this;
175  }
176
177  public function getCurrentPath() {
178    return $this->currentPath;
179  }
180
181  public function setOldPath($path) {
182    $this->oldPath = $this->filterPath($path);
183    return $this;
184  }
185
186  public function getOldPath() {
187    return $this->oldPath;
188  }
189
190  public function addHunk(ArcanistDiffHunk $hunk) {
191    $this->hunks[] = $hunk;
192    return $this;
193  }
194
195  public function dropHunks() {
196    $this->hunks = array();
197    return $this;
198  }
199
200  public function getHunks() {
201    return $this->hunks;
202  }
203
204  /**
205   * @return array $old => array($new, )
206   */
207  public function buildLineMap() {
208    $line_map = array();
209    $old = 1;
210    $new = 1;
211    foreach ($this->getHunks() as $hunk) {
212      for ($n = $old; $n < $hunk->getOldOffset(); $n++) {
213        $line_map[$n] = array($n + $new - $old);
214      }
215      $old = $hunk->getOldOffset();
216      $new = $hunk->getNewOffset();
217      $olds = array();
218      $news = array();
219      $lines = explode("\n", $hunk->getCorpus());
220      foreach ($lines as $line) {
221        $type = substr($line, 0, 1);
222        if ($type == '-' || $type == ' ') {
223          $olds[] = $old;
224          $old++;
225        }
226        if ($type == '+' || $type == ' ') {
227          $news[] = $new;
228          $new++;
229        }
230        if ($type == ' ' || $type == '') {
231          $line_map += array_fill_keys($olds, $news);
232          $olds = array();
233          $news = array();
234        }
235      }
236    }
237    return $line_map;
238  }
239
240  public function convertToBinaryChange(ArcanistRepositoryAPI $api) {
241
242    // Fill in the binary data from the working copy.
243
244    $this->setOriginalFileData(
245      $api->getOriginalFileData(
246        $this->getOldPath()));
247
248    $this->setCurrentFileData(
249      $api->getCurrentFileData(
250        $this->getCurrentPath()));
251
252    $this->hunks = array();
253    $this->setFileType(ArcanistDiffChangeType::FILE_BINARY);
254    return $this;
255  }
256
257  protected function filterPath($path) {
258    if ($path == '/dev/null') {
259      return null;
260    }
261    return $path;
262  }
263
264  public function renderTextSummary() {
265
266    $type = $this->getType();
267    $file = $this->getFileType();
268
269    $char = ArcanistDiffChangeType::getSummaryCharacterForChangeType($type);
270    $attr = ArcanistDiffChangeType::getShortNameForFileType($file);
271    if ($attr) {
272      $attr = '('.$attr.')';
273    }
274
275    $summary = array();
276    $summary[] = sprintf(
277      '%s %5.5s %s',
278      $char,
279      $attr,
280      $this->getCurrentPath());
281    if (ArcanistDiffChangeType::isOldLocationChangeType($type)) {
282      foreach ($this->getAwayPaths() as $path) {
283        $summary[] = '             to: '.$path;
284      }
285    }
286    if (ArcanistDiffChangeType::isNewLocationChangeType($type)) {
287      $summary[] = '             from: '.$this->getOldPath();
288    }
289
290    return implode("\n", $summary);
291  }
292
293  public function getSymlinkTarget() {
294    if ($this->getFileType() != ArcanistDiffChangeType::FILE_SYMLINK) {
295      throw new Exception(pht('Not a symlink!'));
296    }
297    $hunks = $this->getHunks();
298    $hunk = reset($hunks);
299    $corpus = $hunk->getCorpus();
300    $match = null;
301    if (!preg_match('/^\+(?:link )?(.*)$/m', $corpus, $match)) {
302      throw new Exception(pht('Failed to extract link target!'));
303    }
304    return trim($match[1]);
305  }
306
307  public function setNeedsSyntheticGitHunks($needs_synthetic_git_hunks) {
308    $this->needsSyntheticGitHunks = $needs_synthetic_git_hunks;
309    return $this;
310  }
311
312  public function getNeedsSyntheticGitHunks() {
313    return $this->needsSyntheticGitHunks;
314  }
315
316}
317