wt-status.c: handle worktree renames

Before 425a28e0a4 (diff-lib: allow ita entries treated as "not yet exist in index" - 2016-10-24) there are never "new files" in the index, which essentially disables rename detection because we only detect renames when a new file appears in a diff pair. After that commit, an i-t-a entry can appear as a new file in "git diff-files". But the diff callback function in wt-status.c does not handle this case and produces incorrect status output. PS. The reader may notice that this patch adds a new xstrdup() but not a free(). Yes we leak memory (the same for head_path). But wt_status so far has been short lived, this leak should not matter in practice. Noticed-by: Alex Vandiver <alexmv@dropbox.com> Helped-by: Igor Djordjevic <igor.d.djordjevic@gmail.com> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Dec 27, 2017 at 17:18 UTC 176ea747930908669200520ae14f9dbc61cf0d40
3 files changed +92 -13
Documentation/git-status.txt
+13 -10
@@ -125,14 +125,15 @@ the status.relativePaths config option below.
125 Short Format
126 ~~~~~~~~~~~~
127
128 -In the short-format, the status of each path is shown as
128 +In the short-format, the status of each path is shown as one of these
129 +forms
130
130 - XY PATH1 -> PATH2
131 + XY PATH
132 + XY ORIG_PATH -> PATH
133
132 -where `PATH1` is the path in the `HEAD`, and the " `-> PATH2`" part is
133 -shown only when `PATH1` corresponds to a different path in the
134 -index/worktree (i.e. the file is renamed). The `XY` is a two-letter
135 -status code.
134 +where `ORIG_PATH` is where the renamed/copied contents came
135 +from. `ORIG_PATH` is only shown when the entry is renamed or
136 +copied. The `XY` is a two-letter status code.
137
138 The fields (including the `->`) are separated from each other by a
139 single space. If a filename contains whitespace or other nonprintable
@@ -168,6 +169,8 @@ in which case `XY` are `!!`.
169 [MARC] index and work tree matches
170 [ MARC] M work tree changed since index
171 [ MARC] D deleted in work tree
172 + [ D] R renamed in work tree
173 + [ D] C copied in work tree
174 -------------------------------------------------
175 D D unmerged, both deleted
176 A U unmerged, added by us
@@ -285,13 +288,13 @@ Renamed or copied entries have the following format:
288 of similarity between the source and target of the
289 move or copy). For example "R100" or "C75".
290 <path> The pathname. In a renamed/copied entry, this
288 - is the path in the index and in the working tree.
291 + is the target path.
292 <sep> When the `-z` option is used, the 2 pathnames are separated
293 with a NUL (ASCII 0x00) byte; otherwise, a tab (ASCII 0x09)
294 byte separates them.
292 - <origPath> The pathname in the commit at HEAD. This is only
293 - present in a renamed/copied entry, and tells
294 - where the renamed/copied contents came from.
295 + <origPath> The pathname in the commit at HEAD or in the index.
296 + This is only present in a renamed/copied entry, and
297 + tells where the renamed/copied contents came from.
298 --------------------------------------------------------
299
300 Unmerged entries have the following format; the first character is
t/t2203-add-intent.sh
+60
@@ -162,5 +162,65 @@ test_expect_success 'commit: ita entries ignored in empty commit check' '
162 )
163 '
164
165 +test_expect_success 'rename detection finds the right names' '
166 + git init rename-detection &&
167 + (
168 + cd rename-detection &&
169 + echo contents >first &&
170 + git add first &&
171 + git commit -m first &&
172 + mv first third &&
173 + git add -N third &&
174 +
175 + git status | grep -v "^?" >actual.1 &&
176 + test_i18ngrep "renamed: *first -> third" actual.1 &&
177 +
178 + git status --porcelain | grep -v "^?" >actual.2 &&
179 + cat >expected.2 <<-\EOF &&
180 + R first -> third
181 + EOF
182 + test_cmp expected.2 actual.2 &&
183 +
184 + hash=12f00e90b6ef79117ce6e650416b8cf517099b78 &&
185 + git status --porcelain=v2 | grep -v "^?" >actual.3 &&
186 + cat >expected.3 <<-EOF &&
187 + 2 .R N... 100644 100644 100644 $hash $hash R100 third first
188 + EOF
189 + test_cmp expected.3 actual.3
190 + )
191 +'
192 +
193 +test_expect_success 'double rename detection in status' '
194 + git init rename-detection-2 &&
195 + (
196 + cd rename-detection-2 &&
197 + echo contents >first &&
198 + git add first &&
199 + git commit -m first &&
200 + git mv first second &&
201 + mv second third &&
202 + git add -N third &&
203 +
204 + git status | grep -v "^?" >actual.1 &&
205 + test_i18ngrep "renamed: *first -> second" actual.1 &&
206 + test_i18ngrep "renamed: *second -> third" actual.1 &&
207 +
208 + git status --porcelain | grep -v "^?" >actual.2 &&
209 + cat >expected.2 <<-\EOF &&
210 + R first -> second
211 + R second -> third
212 + EOF
213 + test_cmp expected.2 actual.2 &&
214 +
215 + hash=12f00e90b6ef79117ce6e650416b8cf517099b78 &&
216 + git status --porcelain=v2 | grep -v "^?" >actual.3 &&
217 + cat >expected.3 <<-EOF &&
218 + 2 R. N... 100644 100644 100644 $hash $hash R100 second first
219 + 2 .R N... 100644 100644 100644 $hash $hash R100 third second
220 + EOF
221 + test_cmp expected.3 actual.3
222 + )
223 +'
224 +
225 test_done
226
wt-status.c
+19 -3
@@ -361,8 +361,6 @@ static void wt_longstatus_print_change_data(struct wt_status *s,
361 switch (change_type) {
362 case WT_STATUS_UPDATED:
363 status = d->index_status;
364 - if (d->rename_source)
365 - one_name = d->rename_source;
364 break;
365 case WT_STATUS_CHANGED:
366 if (d->new_submodule_commits || d->dirty_submodule) {
@@ -383,6 +381,14 @@ static void wt_longstatus_print_change_data(struct wt_status *s,
381 change_type);
382 }
383
384 + /*
385 + * Only pick up the rename it's relevant. If the rename is for
386 + * the changed section and we're printing the updated section,
387 + * ignore it.
388 + */
389 + if (d->rename_status == status)
390 + one_name = d->rename_source;
391 +
392 one = quote_path(one_name, s->prefix, &onebuf);
393 two = quote_path(two_name, s->prefix, &twobuf);
394
@@ -434,7 +440,7 @@ static void wt_status_collect_changed_cb(struct diff_queue_struct *q,
440 struct wt_status_change_data *d;
441
442 p = q->queue[i];
437 - it = string_list_insert(&s->change, p->one->path);
443 + it = string_list_insert(&s->change, p->two->path);
444 d = it->util;
445 if (!d) {
446 d = xcalloc(1, sizeof(*d));
@@ -461,6 +467,14 @@ static void wt_status_collect_changed_cb(struct diff_queue_struct *q,
467 /* mode_worktree is zero for a delete. */
468 break;
469
470 + case DIFF_STATUS_COPIED:
471 + case DIFF_STATUS_RENAMED:
472 + if (d->rename_status)
473 + die("BUG: multiple renames on the same target? how?");
474 + d->rename_source = xstrdup(p->one->path);
475 + d->rename_score = p->score * 100 / MAX_SCORE;
476 + d->rename_status = p->status;
477 + /* fallthru */
478 case DIFF_STATUS_MODIFIED:
479 case DIFF_STATUS_TYPE_CHANGED:
480 case DIFF_STATUS_UNMERGED:
@@ -532,6 +546,8 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
546
547 case DIFF_STATUS_COPIED:
548 case DIFF_STATUS_RENAMED:
549 + if (d->rename_status)
550 + die("BUG: multiple renames on the same target? how?");
551 d->rename_source = xstrdup(p->one->path);
552 d->rename_score = p->score * 100 / MAX_SCORE;
553 d->rename_status = p->status;