@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Utility class which encapsulates some shared behavior between different
5 * applications which render diffs.
6 *
7 * @task config Configuring the Engine
8 * @task diff Generating Diffs
9 */
10final class PhabricatorDifferenceEngine extends Phobject {
11
12
13 private $oldName;
14 private $newName;
15 private $normalize;
16
17
18/* -( Configuring the Engine )--------------------------------------------- */
19
20
21 /**
22 * Set the name to identify the old file with. Primarily cosmetic.
23 *
24 * @param string $old_name Old file name.
25 * @return $this
26 * @task config
27 */
28 public function setOldName($old_name) {
29 $this->oldName = $old_name;
30 return $this;
31 }
32
33
34 /**
35 * Set the name to identify the new file with. Primarily cosmetic.
36 *
37 * @param string $new_name New file name.
38 * @return $this
39 * @task config
40 */
41 public function setNewName($new_name) {
42 $this->newName = $new_name;
43 return $this;
44 }
45
46
47 public function setNormalize($normalize) {
48 $this->normalize = $normalize;
49 return $this;
50 }
51
52 public function getNormalize() {
53 return $this->normalize;
54 }
55
56
57/* -( Generating Diffs )--------------------------------------------------- */
58
59
60 /**
61 * Generate a raw diff from two raw files. This is a lower-level API than
62 * @{method:generateChangesetFromFileContent}, but may be useful if you need
63 * to use a custom parser configuration, as with Diffusion.
64 *
65 * @param string $old Entire previous file content.
66 * @param string $new Entire current file content.
67 * @return string Raw diff between the two files.
68 * @task diff
69 */
70 public function generateRawDiffFromFileContent($old, $new) {
71
72 $options = array();
73
74 // Generate diffs with full context.
75 $options[] = '-U65535';
76
77 $old_name = nonempty($this->oldName, '/dev/universe').' 9999-99-99';
78 $new_name = nonempty($this->newName, '/dev/universe').' 9999-99-99';
79
80 $options[] = '-L';
81 $options[] = $old_name;
82 $options[] = '-L';
83 $options[] = $new_name;
84
85 $normalize = $this->getNormalize();
86 if ($normalize) {
87 $old = $this->normalizeFile($old);
88 $new = $this->normalizeFile($new);
89 }
90
91 $old_tmp = new TempFile();
92 $new_tmp = new TempFile();
93
94 Filesystem::writeFile($old_tmp, $old);
95 Filesystem::writeFile($new_tmp, $new);
96 list($err, $diff) = exec_manual(
97 'diff %Ls %s %s',
98 $options,
99 $old_tmp,
100 $new_tmp);
101
102 if (!$err) {
103 // This indicates that the two files are the same. Build a synthetic,
104 // changeless diff so that we can still render the raw, unchanged file
105 // instead of being forced to just say "this file didn't change" since we
106 // don't have the content.
107
108 $entire_file = explode("\n", $old);
109 foreach ($entire_file as $k => $line) {
110 $entire_file[$k] = ' '.$line;
111 }
112
113 $len = count($entire_file);
114 $entire_file = implode("\n", $entire_file);
115
116 // TODO: If both files were identical but missing newlines, we probably
117 // get this wrong. Unclear if it ever matters.
118
119 // This is a bit hacky but the diff parser can handle it.
120 $diff = "--- {$old_name}\n".
121 "+++ {$new_name}\n".
122 "@@ -1,{$len} +1,{$len} @@\n".
123 $entire_file."\n";
124 }
125
126 return $diff;
127 }
128
129
130 /**
131 * Generate an @{class:DifferentialChangeset} from two raw files. This is
132 * principally useful because you can feed the output to
133 * @{class:DifferentialChangesetParser} in order to render it.
134 *
135 * @param string $old Entire previous file content.
136 * @param string $new Entire current file content.
137 * @return DifferentialChangeset Synthetic changeset.
138 * @task diff
139 */
140 public function generateChangesetFromFileContent($old, $new) {
141 $diff = $this->generateRawDiffFromFileContent($old, $new);
142
143 $changes = id(new ArcanistDiffParser())->parseDiff($diff);
144 $diff = DifferentialDiff::newEphemeralFromRawChanges(
145 $changes);
146 return head($diff->getChangesets());
147 }
148
149 private function normalizeFile($corpus) {
150 // We can freely apply any other transformations we want to here: we have
151 // no constraints on what we need to preserve. If we normalize every line
152 // to "cat", the diff will still work, the alignment of the "-" / "+"
153 // lines will just be very hard to read.
154
155 // In general, we'll make the diff better if we normalize two lines that
156 // humans think are the same.
157
158 // We'll make the diff worse if we normalize two lines that humans think
159 // are different.
160
161
162 // Strip all whitespace present anywhere in the diff, since humans never
163 // consider whitespace changes to alter the line into a "different line"
164 // even when they're semantic (e.g., in a string constant). This covers
165 // indentation changes, trailing whitepspace, and formatting changes
166 // like "+/-".
167 $corpus = preg_replace('/[ \t]/', '', $corpus);
168
169 return $corpus;
170 }
171
172 public static function applyIntralineDiff($str, $intra_stack) {
173 $buf = '';
174 $p = $s = $e = 0; // position, start, end
175 $highlight = $tag = $ent = false;
176 $highlight_o = '<span class="bright">';
177 $highlight_c = '</span>';
178
179 $depth_in = '<span class="depth-in">';
180 $depth_out = '<span class="depth-out">';
181
182 $is_html = false;
183 if ($str instanceof PhutilSafeHTML) {
184 $is_html = true;
185 $str = $str->getHTMLContent();
186 }
187
188 $n = strlen($str);
189 for ($i = 0; $i < $n; $i++) {
190
191 if ($p == $e) {
192 do {
193 if (empty($intra_stack)) {
194 $buf .= substr($str, $i);
195 break 2;
196 }
197 $stack = array_shift($intra_stack);
198 $s = $e;
199 $e += $stack[1];
200 } while ($stack[0] === 0);
201
202 switch ($stack[0]) {
203 case '>':
204 $open_tag = $depth_in;
205 break;
206 case '<':
207 $open_tag = $depth_out;
208 break;
209 default:
210 $open_tag = $highlight_o;
211 break;
212 }
213 }
214
215 if (!$highlight && !$tag && !$ent && $p == $s) {
216 $buf .= $open_tag;
217 $highlight = true;
218 }
219
220 if ($str[$i] == '<') {
221 $tag = true;
222 if ($highlight) {
223 $buf .= $highlight_c;
224 }
225 }
226
227 if (!$tag) {
228 if ($str[$i] == '&') {
229 $ent = true;
230 }
231 if ($ent && $str[$i] == ';') {
232 $ent = false;
233 }
234 if (!$ent) {
235 $p++;
236 }
237 }
238
239 $buf .= $str[$i];
240
241 if ($tag && $str[$i] == '>') {
242 $tag = false;
243 if ($highlight) {
244 $buf .= $open_tag;
245 }
246 }
247
248 if ($highlight && ($p == $e || $i == $n - 1)) {
249 $buf .= $highlight_c;
250 $highlight = false;
251 }
252 }
253
254 if ($is_html) {
255 return phutil_safe_html($buf);
256 }
257
258 return $buf;
259 }
260
261}