Raw
1 #!/bin/sh
2
3 test_description='test log -L'
4 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
5 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
6
7 . ./test-lib.sh
8
9 test_expect_success 'setup (import history)' '
10 git fast-import < "$TEST_DIRECTORY"/t4211/history.export &&
11 git reset --hard
12 '
13
14 test_expect_success 'basic command line parsing' '
15 # This may fail due to "no such path a.c in commit", or
16 # "-L is incompatible with pathspec", depending on the
17 # order the error is checked. Either is acceptable.
18 test_must_fail git log -L1,1:a.c -- a.c &&
19
20 # -L requires there is no pathspec
21 test_must_fail git log -L1,1:b.c -- b.c 2>error &&
22 test_grep "cannot be used with pathspec" error &&
23
24 # This would fail because --follow wants a single path, but
25 # we may fail due to incompatibility between -L/--follow in
26 # the future. Either is acceptable.
27 test_must_fail git log -L1,1:b.c --follow &&
28 test_must_fail git log --follow -L1,1:b.c &&
29
30 # This would fail because -L wants no pathspec, but
31 # we may fail due to incompatibility between -L/--follow in
32 # the future. Either is acceptable.
33 test_must_fail git log --follow -L1,1:b.c -- b.c
34 '
35
36 canned_test_1 () {
37 test_expect_$1 "$2" "
38 git log $2 >actual &&
39 test_cmp \"\$TEST_DIRECTORY\"/t4211/$(test_oid algo)/expect.$3 actual
40 "
41 }
42
43 canned_test () {
44 canned_test_1 success "$@"
45 }
46 canned_test_failure () {
47 canned_test_1 failure "$@"
48 }
49
50 test_bad_opts () {
51 test_expect_success "invalid args: $1" "
52 test_must_fail git log $1 2>errors &&
53 test_grep '$2' errors
54 "
55 }
56
57 canned_test "-L 4,12:a.c simple" simple-f
58 canned_test "-L 4,+9:a.c simple" simple-f
59 canned_test "-L '/long f/,/^}/:a.c' simple" simple-f
60 canned_test "-L :f:a.c simple" simple-f-to-main
61
62 canned_test "-L '/main/,/^}/:a.c' simple" simple-main
63 canned_test "-L :main:a.c simple" simple-main-to-end
64
65 canned_test "-L 1,+4:a.c simple" beginning-of-file
66
67 canned_test "-L 20:a.c simple" end-of-file
68
69 canned_test "-L '/long f/',/^}/:a.c -L /main/,/^}/:a.c simple" two-ranges
70 canned_test "-L 24,+1:a.c simple" vanishes-early
71
72 canned_test "-M -L '/long f/,/^}/:b.c' move-support" move-support-f
73 canned_test "-M -L ':f:b.c' parallel-change" parallel-change-f-to-main
74
75 canned_test "-L 4,12:a.c -L :main:a.c simple" multiple
76 canned_test "-L 4,18:a.c -L ^:main:a.c simple" multiple-overlapping
77 canned_test "-L :main:a.c -L 4,18:a.c simple" multiple-overlapping
78 canned_test "-L 4:a.c -L 8,12:a.c simple" multiple-superset
79 canned_test "-L 8,12:a.c -L 4:a.c simple" multiple-superset
80
81 canned_test "-L 10,16:b.c -L 18,26:b.c main" no-assertion-error
82
83 test_bad_opts "-L" "switch.*requires a value"
84 test_bad_opts "-L b.c" "argument not .start,end:file"
85 test_bad_opts "-L 1:" "argument not .start,end:file"
86 test_bad_opts "-L 1:nonexistent" "There is no path"
87 test_bad_opts "-L 1:simple" "There is no path"
88 test_bad_opts "-L '/foo:b.c'" "argument not .start,end:file"
89 test_bad_opts "-L 1000:b.c" "has only.*lines"
90 test_bad_opts "-L :b.c" "argument not .start,end:file"
91 test_bad_opts "-L :foo:b.c" "no match"
92
93 test_expect_success '-L X (X == nlines)' '
94 n=$(wc -l <b.c) &&
95 git log -L $n:b.c
96 '
97
98 test_expect_success '-L X (X == nlines + 1)' '
99 n=$(expr $(wc -l <b.c) + 1) &&
100 test_must_fail git log -L $n:b.c
101 '
102
103 test_expect_success '-L X (X == nlines + 2)' '
104 n=$(expr $(wc -l <b.c) + 2) &&
105 test_must_fail git log -L $n:b.c
106 '
107
108 test_expect_success '-L ,Y (Y == nlines)' '
109 n=$(printf "%d" $(wc -l <b.c)) &&
110 git log -L ,$n:b.c
111 '
112
113 test_expect_success '-L ,Y (Y == nlines + 1)' '
114 n=$(expr $(wc -l <b.c) + 1) &&
115 git log -L ,$n:b.c
116 '
117
118 test_expect_success '-L ,Y (Y == nlines + 2)' '
119 n=$(expr $(wc -l <b.c) + 2) &&
120 git log -L ,$n:b.c
121 '
122
123 test_expect_success '-L with --first-parent and a merge' '
124 git checkout parallel-change &&
125 git log --first-parent -L 1,1:b.c
126 '
127
128 test_expect_success '-L with --output' '
129 git checkout parallel-change &&
130 git log --output=log -L :main:b.c >output &&
131 test_must_be_empty output &&
132 test_line_count = 75 log
133 '
134
135 test_expect_success 'range_set_union' '
136 test_seq 500 > c.c &&
137 git add c.c &&
138 git commit -m "many lines" &&
139 test_seq 1000 > c.c &&
140 git add c.c &&
141 git commit -m "modify many lines" &&
142 git log $(for x in $(test_seq 200); do echo -L $((2*x)),+1:c.c || return 1; done)
143 '
144
145 test_expect_success '-s shows only line-log commits' '
146 git log --format="commit %s" -L1,24:b.c >expect.raw &&
147 grep ^commit expect.raw >expect &&
148 git log --format="commit %s" -L1,24:b.c -s >actual &&
149 test_cmp expect actual
150 '
151
152 test_expect_success '-p shows the default patch output' '
153 git log -L1,24:b.c >expect &&
154 git log -L1,24:b.c -p >actual &&
155 test_cmp expect actual
156 '
157
158 test_expect_success '--raw shows mode, oid, status and path' '
159 git log -L1,24:b.c --raw --format= >actual &&
160 test_grep "^:100644 100644 [0-9a-f]\{7\} [0-9a-f]\{7\} M b.c$" actual &&
161 test_grep ! "^diff --git" actual &&
162 test_grep ! "^@@" actual
163 '
164
165 test_expect_success '--name-only shows path' '
166 git log -L1,24:b.c --name-only --format= >actual &&
167 test_grep "^b.c$" actual &&
168 test_grep ! "^diff --git" actual &&
169 test_grep ! "^@@" actual
170 '
171
172 test_expect_success '--name-status shows status and path' '
173 git log -L1,24:b.c --name-status --format= >actual &&
174 test_grep "^M b.c$" actual &&
175 test_grep ! "^diff --git" actual &&
176 test_grep ! "^@@" actual
177 '
178
179 test_expect_success '--dirstat is not supported with -L' '
180 # --dirstat is not supported with -L: its default mode measures
181 # whole-file change, not the tracked lines, and the
182 # --dirstat=lines variant is deferred too, so both forms are
183 # rejected like any other unsupported format.
184 test_must_fail git log -L1,24:b.c --dirstat 2>err &&
185 test_grep "does not support" err &&
186 test_must_fail git log -L1,24:b.c --dirstat=lines 2>err &&
187 test_grep "does not support" err
188 '
189
190 test_expect_success 'setup for checking fancy rename following' '
191 git checkout --orphan moves-start &&
192 git reset --hard &&
193
194 printf "%s\n" 12 13 14 15 b c d e >file-1 &&
195 printf "%s\n" 22 23 24 25 B C D E >file-2 &&
196 git add file-1 file-2 &&
197 test_tick &&
198 git commit -m "Add file-1 and file-2" &&
199 oid_add_f1_f2=$(git rev-parse --short HEAD) &&
200
201 git checkout -b moves-main &&
202 printf "%s\n" 11 12 13 14 15 b c d e >file-1 &&
203 git commit -a -m "Modify file-1 on main" &&
204 oid_mod_f1_main=$(git rev-parse --short HEAD) &&
205
206 printf "%s\n" 21 22 23 24 25 B C D E >file-2 &&
207 git commit -a -m "Modify file-2 on main #1" &&
208 oid_mod_f2_main_1=$(git rev-parse --short HEAD) &&
209
210 git mv file-1 renamed-1 &&
211 git commit -m "Rename file-1 to renamed-1 on main" &&
212
213 printf "%s\n" 11 12 13 14 15 b c d e f >renamed-1 &&
214 git commit -a -m "Modify renamed-1 on main" &&
215 oid_mod_r1_main=$(git rev-parse --short HEAD) &&
216
217 printf "%s\n" 21 22 23 24 25 B C D E F >file-2 &&
218 git commit -a -m "Modify file-2 on main #2" &&
219 oid_mod_f2_main_2=$(git rev-parse --short HEAD) &&
220
221 git checkout -b moves-side moves-start &&
222 printf "%s\n" 12 13 14 15 16 b c d e >file-1 &&
223 git commit -a -m "Modify file-1 on side #1" &&
224 oid_mod_f1_side_1=$(git rev-parse --short HEAD) &&
225
226 printf "%s\n" 22 23 24 25 26 B C D E >file-2 &&
227 git commit -a -m "Modify file-2 on side" &&
228 oid_mod_f2_side=$(git rev-parse --short HEAD) &&
229
230 git mv file-2 renamed-2 &&
231 git commit -m "Rename file-2 to renamed-2 on side" &&
232
233 printf "%s\n" 12 13 14 15 16 a b c d e >file-1 &&
234 git commit -a -m "Modify file-1 on side #2" &&
235 oid_mod_f1_side_2=$(git rev-parse --short HEAD) &&
236
237 printf "%s\n" 22 23 24 25 26 A B C D E >renamed-2 &&
238 git commit -a -m "Modify renamed-2 on side" &&
239 oid_mod_r2_side=$(git rev-parse --short HEAD) &&
240
241 git checkout moves-main &&
242 git merge moves-side &&
243 oid_merge=$(git rev-parse --short HEAD)
244 '
245
246 test_expect_success 'fancy rename following #1' '
247 cat >expect <<-EOF &&
248 $oid_merge Merge branch '\''moves-side'\'' into moves-main
249 $oid_mod_f1_side_2 Modify file-1 on side #2
250 $oid_mod_f1_side_1 Modify file-1 on side #1
251 $oid_mod_r1_main Modify renamed-1 on main
252 $oid_mod_f1_main Modify file-1 on main
253 $oid_add_f1_f2 Add file-1 and file-2
254 EOF
255 git log -L1:renamed-1 --oneline --no-patch >actual &&
256 test_cmp expect actual
257 '
258
259 test_expect_success 'fancy rename following #2' '
260 cat >expect <<-EOF &&
261 $oid_merge Merge branch '\''moves-side'\'' into moves-main
262 $oid_mod_r2_side Modify renamed-2 on side
263 $oid_mod_f2_side Modify file-2 on side
264 $oid_mod_f2_main_2 Modify file-2 on main #2
265 $oid_mod_f2_main_1 Modify file-2 on main #1
266 $oid_add_f1_f2 Add file-1 and file-2
267 EOF
268 git log -L1:renamed-2 --oneline --no-patch >actual &&
269 test_cmp expect actual
270 '
271
272 # Create the following linear history, where each commit does what its
273 # subject line promises:
274 #
275 # * 66c6410 Modify func2() in file.c
276 # * 50834e5 Modify other-file
277 # * fe5851c Modify func1() in file.c
278 # * 8c7c7dd Add other-file
279 # * d5f4417 Add func1() and func2() in file.c
280 test_expect_success 'setup for checking line-log and parent oids' '
281 git checkout --orphan parent-oids &&
282 git reset --hard &&
283
284 cat >file.c <<-\EOF &&
285 int func1()
286 {
287 return F1;
288 }
289
290 int func2()
291 {
292 return F2;
293 }
294 EOF
295 git add file.c &&
296 test_tick &&
297 first_tick=$test_tick &&
298 git commit -m "Add func1() and func2() in file.c" &&
299
300 echo 1 >other-file &&
301 git add other-file &&
302 test_tick &&
303 git commit -m "Add other-file" &&
304
305 sed -e "s/F1/F1 + 1/" file.c >tmp &&
306 mv tmp file.c &&
307 git commit -a -m "Modify func1() in file.c" &&
308
309 echo 2 >other-file &&
310 git commit -a -m "Modify other-file" &&
311
312 sed -e "s/F2/F2 + 2/" file.c >tmp &&
313 mv tmp file.c &&
314 git commit -a -m "Modify func2() in file.c" &&
315
316 head_oid=$(git rev-parse --short HEAD) &&
317 prev_oid=$(git rev-parse --short HEAD^) &&
318 root_oid=$(git rev-parse --short HEAD~4)
319 '
320
321 # Parent oid should be from immediate parent.
322 test_expect_success 'parent oids without parent rewriting' '
323 cat >expect <<-EOF &&
324 $head_oid $prev_oid Modify func2() in file.c
325 $root_oid Add func1() and func2() in file.c
326 EOF
327 git log --format="%h %p %s" --no-patch -L:func2:file.c >actual &&
328 test_cmp expect actual
329 '
330
331 # Parent oid should be from the most recent ancestor touching func2(),
332 # i.e. in this case from the root commit.
333 test_expect_success 'parent oids with parent rewriting' '
334 cat >expect <<-EOF &&
335 $head_oid $root_oid Modify func2() in file.c
336 $root_oid Add func1() and func2() in file.c
337 EOF
338 git log --format="%h %p %s" --no-patch -L:func2:file.c --parents >actual &&
339 test_cmp expect actual
340 '
341
342 test_expect_success 'line-log with --before' '
343 echo $root_oid >expect &&
344 git log --format=%h --no-patch -L:func2:file.c --before=$first_tick >actual &&
345 test_cmp expect actual
346 '
347
348 test_expect_success 'setup tests for zero-width regular expressions' '
349 cat >expect <<-EOF
350 Modify func1() in file.c
351 Add func1() and func2() in file.c
352 EOF
353 '
354
355 test_expect_success 'zero-width regex $ matches any function name' '
356 git log --format="%s" --no-patch "-L:$:file.c" >actual &&
357 test_cmp expect actual
358 '
359
360 test_expect_success 'zero-width regex ^ matches any function name' '
361 git log --format="%s" --no-patch "-L:^:file.c" >actual &&
362 test_cmp expect actual
363 '
364
365 test_expect_success 'zero-width regex .* matches any function name' '
366 git log --format="%s" --no-patch "-L:.*:file.c" >actual &&
367 test_cmp expect actual
368 '
369
370 test_expect_success 'setup for diff pipeline tests' '
371 git checkout parent-oids &&
372
373 head_blob_old=$(git rev-parse --short HEAD^:file.c) &&
374 head_blob_new=$(git rev-parse --short HEAD:file.c) &&
375 root_blob=$(git rev-parse --short HEAD~4:file.c) &&
376 null_blob=$(test_oid zero | cut -c1-7) &&
377 head_blob_old_full=$(git rev-parse HEAD^:file.c) &&
378 head_blob_new_full=$(git rev-parse HEAD:file.c) &&
379 root_blob_full=$(git rev-parse HEAD~4:file.c) &&
380 null_blob_full=$(test_oid zero)
381 '
382
383 test_expect_success '-L diff output includes index and new file mode' '
384 git log -L:func2:file.c --format= >actual &&
385
386 # Output should contain index headers (not present in old code path)
387 test_grep "^index $head_blob_old\.\.$head_blob_new 100644" actual &&
388
389 # Root commit should show new file mode and null index
390 test_grep "^new file mode 100644" actual &&
391 test_grep "^index $null_blob\.\.$root_blob$" actual &&
392
393 # Hunk headers should include funcname context
394 test_grep "^@@ .* @@ int func1()" actual
395 '
396
397 test_expect_success '-L with --word-diff' '
398 cat >expect <<-\EOF &&
399 diff --git a/file.c b/file.c
400 --- a/file.c
401 +++ b/file.c
402 @@ -6,4 +6,4 @@ int func1()
403 int func2()
404 {
405 return [-F2;-]{+F2 + 2;+}
406 }
407 diff --git a/file.c b/file.c
408 new file mode 100644
409 --- /dev/null
410 +++ b/file.c
411 @@ -0,0 +6,4 @@
412 {+int func2()+}
413 {+{+}
414 {+ return F2;+}
415 {+}+}
416 EOF
417 git log -L:func2:file.c --word-diff --format= >actual &&
418 grep -v "^index " actual >actual.filtered &&
419 grep -v "^index " expect >expect.filtered &&
420 test_cmp expect.filtered actual.filtered
421 '
422
423 test_expect_success '-L with --no-prefix' '
424 git log -L:func2:file.c --no-prefix --format= >actual &&
425 test_grep "^diff --git file.c file.c" actual &&
426 test_grep "^--- file.c" actual &&
427 test_grep ! "^--- a/" actual
428 '
429
430 test_expect_success '-L with --full-index' '
431 git log -L:func2:file.c --full-index --format= >actual &&
432 test_grep "^index $head_blob_old_full\.\.$head_blob_new_full 100644" actual &&
433 test_grep "^index $null_blob_full\.\.$root_blob_full$" actual
434 '
435
436 test_expect_success 'setup -L with whitespace change' '
437 git checkout -b ws-change parent-oids &&
438 sed "s/ return F2 + 2;/ return F2 + 2;/" file.c >tmp &&
439 mv tmp file.c &&
440 git commit -a -m "Whitespace change in func2()"
441 '
442
443 test_expect_success '-L with --ignore-all-space suppresses whitespace-only diff' '
444 git log -L:func2:file.c --format= >without_w &&
445 git log -L:func2:file.c --format= -w >with_w &&
446
447 # Without -w: three commits produce diffs (whitespace, modify, root)
448 test $(grep -c "^diff --git" without_w) = 3 &&
449
450 # With -w: whitespace-only commit produces no hunk, so only two diffs
451 test $(grep -c "^diff --git" with_w) = 2
452 '
453
454 test_expect_success 'show line-log with graph' '
455 git checkout parent-oids &&
456 head_blob_old=$(git rev-parse --short HEAD^:file.c) &&
457 head_blob_new=$(git rev-parse --short HEAD:file.c) &&
458 root_blob=$(git rev-parse --short HEAD~4:file.c) &&
459 null_blob=$(test_oid zero | cut -c1-7) &&
460 qz_to_tab_space >expect <<-EOF &&
461 * $head_oid Modify func2() in file.c
462 | diff --git a/file.c b/file.c
463 | index $head_blob_old..$head_blob_new 100644
464 | --- a/file.c
465 | +++ b/file.c
466 | @@ -6,4 +6,4 @@ int func1()
467 | int func2()
468 | {
469 | - return F2;
470 | + return F2 + 2;
471 | }
472 * $root_oid Add func1() and func2() in file.c
473 diff --git a/file.c b/file.c
474 new file mode 100644
475 index $null_blob..$root_blob
476 --- /dev/null
477 +++ b/file.c
478 @@ -0,0 +6,4 @@
479 +int func2()
480 +{
481 + return F2;
482 +}
483 EOF
484 git log --graph --oneline -L:func2:file.c >actual &&
485 test_cmp expect actual
486 '
487
488 test_expect_success 'setup for -L with -G/-S/--find-object and a merge with rename' '
489 git checkout --orphan pickaxe-rename &&
490 git reset --hard &&
491
492 echo content >file &&
493 git add file &&
494 git commit -m "add file" &&
495
496 git checkout -b pickaxe-rename-side &&
497 git mv file renamed-file &&
498 git commit -m "rename file" &&
499
500 git checkout pickaxe-rename &&
501 git commit --allow-empty -m "diverge" &&
502 git merge --no-edit pickaxe-rename-side &&
503
504 git mv renamed-file file &&
505 git commit -m "rename back"
506 '
507
508 test_expect_success '-L -G does not crash with merge and rename' '
509 git log --format="%s" --no-patch -L 1,1:file -G "." >actual
510 '
511
512 test_expect_success '-L -S does not crash with merge and rename' '
513 git log --format="%s" --no-patch -L 1,1:file -S content >actual
514 '
515
516 test_expect_success '-L --find-object does not crash with merge and rename' '
517 git log --format="%s" --no-patch -L 1,1:file \
518 --find-object=$(git rev-parse HEAD:file) >actual
519 '
520
521 test_expect_success '-L -G should filter commits by pattern' '
522 git log --format="%s" --no-patch -L 1,1:file -G "nomatch" >actual &&
523 test_must_be_empty actual
524 '
525
526 test_expect_success '-L -S should filter commits by pattern' '
527 git log --format="%s" --no-patch -L 1,1:file -S "nomatch" >actual &&
528 test_must_be_empty actual
529 '
530
531 test_expect_success '-L --find-object should filter commits by object' '
532 git log --format="%s" --no-patch -L 1,1:file \
533 --find-object=$ZERO_OID >actual &&
534 test_must_be_empty actual
535 '
536
537 test_expect_success '-L with --word-diff-regex' '
538 git checkout parent-oids &&
539 git log -L:func2:file.c --word-diff \
540 --word-diff-regex="[a-zA-Z0-9_]+" --format= >actual &&
541 # Word-diff markers must be present
542 test_grep "{+" actual &&
543 test_grep "+}" actual &&
544 # No line-level +/- markers (word-diff replaces them);
545 # exclude --- header lines from the check
546 test_grep ! "^+[^+]" actual &&
547 test_grep ! "^-[^-]" actual
548 '
549
550 test_expect_success '-L with --src-prefix and --dst-prefix' '
551 git checkout parent-oids &&
552 git log -L:func2:file.c --src-prefix=old/ --dst-prefix=new/ \
553 --format= >actual &&
554 test_grep "^diff --git old/file.c new/file.c" actual &&
555 test_grep "^--- old/file.c" actual &&
556 test_grep "^+++ new/file.c" actual &&
557 test_grep ! "^--- a/" actual
558 '
559
560 test_expect_success '-L with --abbrev' '
561 git checkout parent-oids &&
562 git log -L:func2:file.c --abbrev=4 --format= -1 >actual &&
563 # 4-char abbreviated hashes on index line
564 test_grep "^index [0-9a-f]\{4\}\.\.[0-9a-f]\{4\}" actual
565 '
566
567 test_expect_success '-L with -b suppresses whitespace-only diff' '
568 git checkout ws-change &&
569 git log -L:func2:file.c --format= >without_b &&
570 git log -L:func2:file.c --format= -b >with_b &&
571 test $(grep -c "^diff --git" without_b) = 3 &&
572 test $(grep -c "^diff --git" with_b) = 2
573 '
574
575 test_expect_success '-L with --output-indicator-*' '
576 git checkout parent-oids &&
577 git log -L:func2:file.c --output-indicator-new=">" \
578 --output-indicator-old="<" --output-indicator-context="|" \
579 --format= -1 >actual &&
580 test_grep "^>" actual &&
581 test_grep "^<" actual &&
582 test_grep "^|" actual &&
583 # No standard +/-/space content markers; exclude ---/+++ headers
584 test_grep ! "^+[^+]" actual &&
585 test_grep ! "^-[^-]" actual &&
586 test_grep ! "^ " actual
587 '
588
589 test_expect_success '-L with -R reverses diff' '
590 git checkout parent-oids &&
591 git log -L:func2:file.c -R --format= -1 >actual &&
592 test_grep "^diff --git b/file.c a/file.c" actual &&
593 test_grep "^--- b/file.c" actual &&
594 test_grep "^+++ a/file.c" actual &&
595 # The modification added "F2 + 2", so reversed it is removed
596 test_grep "^-.*F2 + 2" actual &&
597 test_grep "^+.*return F2;" actual
598 '
599
600 test_expect_success 'setup for color-moved test' '
601 git checkout -b color-moved-test parent-oids &&
602 cat >big.c <<-\EOF &&
603 int bigfunc()
604 {
605 int a = 1;
606 int b = 2;
607 int c = 3;
608 return a + b + c;
609 }
610 EOF
611 git add big.c &&
612 git commit -m "add bigfunc" &&
613 sed "s/ / /" big.c >tmp && mv tmp big.c &&
614 git commit -a -m "reindent bigfunc"
615 '
616
617 test_expect_success '-L with --color-moved' '
618 git log -L:bigfunc:big.c --color-moved=zebra \
619 --color-moved-ws=ignore-all-space \
620 --color=always --format= -1 >actual.raw &&
621 test_decode_color <actual.raw >actual &&
622 # Old moved lines: bold magenta; new moved lines: bold cyan
623 test_grep "BOLD;MAGENTA" actual &&
624 test_grep "BOLD;CYAN" actual
625 '
626
627 test_expect_success 'setup for no-newline-at-eof tests' '
628 git checkout --orphan no-newline &&
629 git reset --hard &&
630 printf "int top()\n{\n return 1;\n}\n\nint bot()\n{\n return 2;\n}" >noeol.c &&
631 git add noeol.c &&
632 test_tick &&
633 git commit -m "add noeol.c (no trailing newline)" &&
634 sed "s/return 2/return 22/" noeol.c >tmp && mv tmp noeol.c &&
635 git commit -a -m "modify bot()" &&
636 printf "int top()\n{\n return 1;\n}\n\nint bot()\n{\n return 33;\n}\n" >noeol.c &&
637 git commit -a -m "modify bot() and add trailing newline"
638 '
639
640 # When the tracked function is at the end of a file with no trailing
641 # newline, the "\ No newline at end of file" marker should appear.
642 test_expect_success '-L no-newline-at-eof appears in tracked range' '
643 git log -L:bot:noeol.c --format= -1 HEAD~1 >actual &&
644 test_grep "No newline at end of file" actual
645 '
646
647 # When tracking a function that ends before the no-newline content,
648 # the marker should not appear in the output.
649 test_expect_success '-L no-newline-at-eof suppressed outside range' '
650 git log -L:top:noeol.c --format= >actual &&
651 test_grep ! "No newline at end of file" actual
652 '
653
654 # When a commit removes a no-newline last line and replaces it with
655 # a newline-terminated line, the marker should still appear (on the
656 # old side of the diff).
657 test_expect_success '-L no-newline-at-eof marker with deleted line' '
658 git log -L:bot:noeol.c --format= -1 >actual &&
659 test_grep "No newline at end of file" actual
660 '
661
662 test_expect_success 'setup for range boundary deletion test' '
663 git checkout --orphan range-boundary &&
664 git reset --hard &&
665 cat >boundary.c <<-\EOF &&
666 void above()
667 {
668 return;
669 }
670
671 void tracked()
672 {
673 int x = 1;
674 int y = 2;
675 }
676
677 void below()
678 {
679 return;
680 }
681 EOF
682 git add boundary.c &&
683 test_tick &&
684 git commit -m "add boundary.c" &&
685 cat >boundary.c <<-\EOF &&
686 void above()
687 {
688 return;
689 }
690
691 void tracked()
692 {
693 int x = 1;
694 int y = 2;
695 }
696
697 void below_renamed()
698 {
699 return 0;
700 }
701 EOF
702 git commit -a -m "modify below() only"
703 '
704
705 # When only a function below the tracked range is modified, the
706 # tracked function should not produce a diff.
707 test_expect_success '-L suppresses deletions outside tracked range' '
708 git log -L:tracked:boundary.c --format= >actual &&
709 test $(grep -c "^diff --git" actual) = 1
710 '
711
712 test_expect_success '-L with -S filters to string-count changes' '
713 git checkout parent-oids &&
714 git log -L:func2:file.c -S "F2 + 2" --format= >actual &&
715 # -S searches the whole file, not just the tracked range;
716 # combined with the -L range walk, this selects commits that
717 # both touch func2 and change the count of "F2 + 2" in the file.
718 test $(grep -c "^diff --git" actual) = 1 &&
719 test_grep "F2 + 2" actual
720 '
721
722 test_expect_success '-L with -G filters to diff-text matches' '
723 git checkout parent-oids &&
724 git log -L:func2:file.c -G "F2 [+] 2" --format= >actual &&
725 # -G greps the diff text, and under -L only the lines in the
726 # tracked range (unlike -S above, which searches the whole file);
727 # this selects commits whose change to func2 contains "F2 + 2".
728 test $(grep -c "^diff --git" actual) = 1 &&
729 test_grep "F2 + 2" actual
730 '
731
732 test_expect_success 'setup for trailing deletion test' '
733 git checkout --orphan trailing-del &&
734 git reset --hard &&
735 cat >file.c <<-\EOF &&
736 void tracked()
737 {
738 return 1;
739 }
740 // trailing comment
741 EOF
742 git add file.c &&
743 test_tick &&
744 git commit -m "add file with trailing comment" &&
745 # Modify tracked() AND delete the trailing comment in
746 # one commit, so the commit touches the tracked range
747 # and is not filtered out by the revision walker.
748 cat >file.c <<-\EOF &&
749 void tracked()
750 {
751 return 2;
752 }
753 EOF
754 git commit -a -m "modify tracked and delete trailing comment"
755 '
756
757 test_expect_success '-L does not include deletions past end of tracked range' '
758 git log -L:tracked:file.c --format= -1 -p >actual &&
759 # The trailing comment deletion is outside the tracked
760 # range and should not appear in the patch output.
761 test_grep "return 2" actual &&
762 test_grep ! "trailing comment" actual
763 '
764
765 test_expect_success '-L includes leading deletions resolved by in-range line' '
766 git checkout --orphan leading-del &&
767 git reset --hard &&
768 cat >file.c <<-\EOF &&
769 // leading comment
770 void tracked()
771 {
772 return 1;
773 }
774 EOF
775 git add file.c &&
776 test_tick &&
777 git commit -m "add file with leading comment" &&
778 cat >file.c <<-\EOF &&
779 void tracked()
780 {
781 return 2;
782 }
783 EOF
784 git commit -a -m "modify tracked and delete leading comment" &&
785 git log -L:tracked:file.c --format= -1 -p >actual &&
786 # The leading comment deletion is resolved by the next
787 # non-removal line (void tracked), which is in range: a
788 # removal is classified by the position of the following
789 # line, so it joins the range that line falls in.
790 test_grep "return 2" actual &&
791 test_grep "leading comment" actual
792 '
793
794 test_expect_success 'setup for line-range filter edge cases' '
795 git checkout --orphan filter-edge &&
796 git reset --hard &&
797 cat >file.c <<-\EOF &&
798 void before()
799 {
800 return 0;
801 }
802
803 void tracked()
804 {
805 int a = 1;
806 int b = 2;
807 int c = 3;
808 return a + b + c;
809 }
810
811 void after()
812 {
813 return 9;
814 }
815 EOF
816 git add file.c &&
817 test_tick &&
818 git commit -m "initial"
819 '
820
821 test_expect_success '-L change at exact first line of range' '
822 git checkout filter-edge &&
823 # Change the function signature (first line of range)
824 sed "s/void tracked/int tracked/" file.c >tmp &&
825 mv tmp file.c &&
826 git commit -a -m "change first line" &&
827 git log -L:tracked:file.c -p --format=%s -1 >actual &&
828 test_grep "change first line" actual &&
829 test_grep "+int tracked" actual &&
830 test_grep "\\-void tracked" actual
831 '
832
833 test_expect_success '-L change at exact last line of range' '
834 git checkout filter-edge &&
835 git reset --hard HEAD~1 &&
836 # Change the closing brace line (last line of range)
837 sed "s/^}$/} \/\/ end tracked/" file.c >tmp &&
838 mv tmp file.c &&
839 git commit -a -m "change last line" &&
840 git log -L:tracked:file.c -p --format=%s -1 >actual &&
841 test_grep "change last line" actual &&
842 test_grep "end tracked" actual
843 '
844
845 test_expect_success '-L pure deletion in range (no additions)' '
846 git checkout filter-edge &&
847 git reset --hard HEAD~1 &&
848 # Delete a line inside tracked() without adding anything
849 sed "/int c/d" file.c >tmp &&
850 mv tmp file.c &&
851 git commit -a -m "pure deletion" &&
852 git log -L:tracked:file.c -p --format=%s -1 >actual &&
853 test_grep "pure deletion" actual &&
854 test_grep "\\-.*int c" actual
855 '
856
857 test_expect_success '-L with --diff-filter=M excludes root commit' '
858 git checkout parent-oids &&
859 git log -L:func2:file.c --diff-filter=M --format=%s --no-patch >actual &&
860 # Root commit is an Add (A), not a Modify (M), so it should
861 # be excluded; only the modification commit remains.
862 echo "Modify func2() in file.c" >expect &&
863 test_cmp expect actual
864 '
865
866 test_expect_success '-L with --diff-filter=A shows only root commit' '
867 git checkout parent-oids &&
868 git log -L:func2:file.c --diff-filter=A --format=%s --no-patch >actual &&
869 echo "Add func1() and func2() in file.c" >expect &&
870 test_cmp expect actual
871 '
872
873 test_expect_success '-L with -S suppresses non-matching commits' '
874 git checkout parent-oids &&
875 git log -L:func2:file.c -S "F2 + 2" --format=%s --no-patch >actual &&
876 # Only the commit that changes the count of "F2 + 2" should appear.
877 echo "Modify func2() in file.c" >expect &&
878 test_cmp expect actual
879 '
880
881 test_expect_success '--full-diff is not supported with -L' '
882 test_must_fail git log -L1,24:b.c --full-diff 2>err &&
883 test_grep "does not support" err
884 '
885
886 test_expect_success '-L --oneline has no extra blank line before diff' '
887 git checkout parent-oids &&
888 git log --oneline -L:func2:file.c -1 >actual &&
889 # Oneline header on line 1, diff starts immediately on line 2
890 sed -n 2p actual >line2 &&
891 test_grep "^diff --git" line2
892 '
893
894 test_expect_success 'setup for stat range-scoping tests' '
895 git checkout --orphan stat-scoping &&
896 git reset --hard &&
897 cat >file.c <<-\EOF &&
898 int func1()
899 {
900 return F1;
901 }
902
903 int func2()
904 {
905 return F2;
906 }
907 EOF
908 git add file.c &&
909 test_tick &&
910 git commit -m "Add func1() and func2()" &&
911
912 # Modify both functions in a single commit so that
913 # whole-file stats differ from the counts for the tracked range.
914 sed -e "s/F1/F1 + 1/" -e "s/F2/F2 + 2/" file.c >tmp &&
915 mv tmp file.c &&
916 git commit -a -m "Modify both functions"
917 '
918
919 test_expect_success '--numstat counts only lines in tracked range' '
920 # "Modify both functions" changes one line in func1 and one in
921 # func2. Whole-file numstat would show 2 added, 2 deleted.
922 # numstat for func2 within the tracked range should show only 1 and 1.
923 git log -L:func2:file.c --numstat --format=%s -1 >actual &&
924 test_grep "Modify both functions" actual &&
925 test_grep "^1 1 file.c$" actual &&
926 test_grep ! "^diff --git" actual
927 '
928
929 test_expect_success '--numstat counts only additions for root commit' '
930 # Root commit creates both func1 (4 lines) and func2 (4 lines).
931 # Whole-file numstat would show 9 lines added. numstat for func2
932 # within the tracked range should show only 4.
933 git log -L:func2:file.c --numstat --format=%s >actual &&
934 test_grep "Add func1() and func2()" actual &&
935 test_grep "^4 0 file.c$" actual &&
936 test_grep ! "^diff --git" actual
937 '
938
939 test_expect_success '--stat counts only lines in tracked range' '
940 git log -L:func2:file.c --stat --format=%s -1 >actual &&
941 test_grep "Modify both functions" actual &&
942 test_grep "file.c |" actual &&
943 test_grep "1 insertion" actual &&
944 test_grep "1 deletion" actual &&
945 test_grep ! "^diff --git" actual
946 '
947
948 test_expect_success '--shortstat counts only lines in tracked range' '
949 # --shortstat prints only the summary line: no per-file "file.c |"
950 # line. Counts cover only the tracked range, as for --numstat above.
951 git log -L:func2:file.c --shortstat --format=%s -1 >actual &&
952 test_grep "Modify both functions" actual &&
953 test_grep "1 insertion" actual &&
954 test_grep "1 deletion" actual &&
955 test_grep ! "file.c |" actual &&
956 test_grep ! "^diff --git" actual
957 '
958
959 test_expect_success '--numstat across renames and multiple commits' '
960 # parallel-change carries the tracked function f across an a.c -> b.c
961 # rename and a merge of two parallel histories. With -M, --numstat
962 # follows the rename and reports added/removed counts for f within
963 # the tracked range (not whole-file) per commit; the file column flips from
964 # b.c to a.c at the rename as the walk goes back in time. Commits
965 # that do not change the range of f emit no row (the merge and the
966 # pure file-move produce nothing), so there are fewer rows than
967 # commits.
968 git checkout parallel-change &&
969 git log -M -L ":f:b.c" --format= --numstat >actual &&
970 cat >expect <<-\EOF &&
971 1 1 b.c
972 1 1 a.c
973 1 1 a.c
974 1 1 a.c
975 1 0 a.c
976 13 0 a.c
977 EOF
978 test_cmp expect actual
979 '
980
981 test_expect_success '-L multiple ranges with --numstat excludes untracked change' '
982 git checkout --orphan multi-range &&
983 git reset --hard &&
984 cat >m.c <<-\EOF &&
985 int func1()
986 {
987 return F1;
988 }
989
990 int func2()
991 {
992 return F2;
993 }
994
995 int func3()
996 {
997 return F3;
998 }
999 EOF
1000 git add m.c &&
1001 test_tick &&
1002 git commit -m "add m.c" &&
1003 # Change all three functions but track only func1 and func2.
1004 # Whole-file numstat would be 3 3; a 2 2 result proves the
1005 # untracked func3 change is excluded and the two ranges just sum.
1006 sed -e "s/F1/F1 + 1/" -e "s/F2/F2 + 2/" -e "s/F3/F3 + 3/" m.c >tmp &&
1007 mv tmp m.c &&
1008 git commit -a -m "Modify all three functions" &&
1009 git log -L:func1:m.c -L:func2:m.c --numstat --format=%s -1 >actual &&
1010 test_grep "Modify all three functions" actual &&
1011 test_grep "^2 2 m.c$" actual &&
1012 test_grep ! "^3 3 m.c$" actual
1013 '
1014
1015 test_expect_success '--summary shows new file on root commit' '
1016 git checkout parent-oids &&
1017 git log -L:func2:file.c --summary --format= >actual &&
1018 test_grep "create mode 100644 file.c" actual
1019 '
1020
1021 test_expect_success 'setup for --check test' '
1022 git checkout --orphan check-test &&
1023 git reset --hard &&
1024 cat >check.c <<-\EOF &&
1025 void tracked()
1026 {
1027 return;
1028 }
1029
1030 void other()
1031 {
1032 return;
1033 }
1034 EOF
1035 git add check.c &&
1036 test_tick &&
1037 git commit -m "add check.c" &&
1038 # Introduce trailing whitespace errors in both functions
1039 sed "s/return;/return; /" check.c >check.c.tmp &&
1040 mv check.c.tmp check.c &&
1041 git commit -a -m "introduce trailing whitespace"
1042 '
1043
1044 test_expect_success '--check scoped to tracked range with correct file line' '
1045 # tracked() trailing whitespace is at check.c:3; report it with the
1046 # real file line number, not a count from the start of the range
1047 # hunk. other() at check.c:8 is outside the range and is excluded.
1048 test_must_fail git log -L:tracked:check.c --check --format= >actual &&
1049 test_grep "check.c:3: trailing whitespace" actual &&
1050 test_grep ! "check.c:8:" actual
1051 '
1052
1053 test_expect_success '--check reports each of several tracked ranges' '
1054 # Track both functions as separate ranges. Each range is flushed
1055 # as its own hunk, so the second error must report its real file
1056 # line (check.c:8), not continue the numbering from the first
1057 # range (check.c:3).
1058 test_must_fail git log -L:tracked:check.c -L:other:check.c \
1059 --check --format= >actual &&
1060 test_grep "check.c:3: trailing whitespace" actual &&
1061 test_grep "check.c:8: trailing whitespace" actual
1062 '
1063
1064 test_expect_success '--check line numbers stay correct across a gap in one range' '
1065 git checkout --orphan check-gap &&
1066 git reset --hard &&
1067 cat >gap.c <<-\EOF &&
1068 void tracked()
1069 {
1070 int a = 1;
1071 int b = 2;
1072 int c = 3;
1073 int d = 4;
1074 int e = 5;
1075 int g = 7;
1076 return;
1077 }
1078 EOF
1079 git add gap.c &&
1080 test_tick &&
1081 git commit -m "add gap.c" &&
1082 # Two trailing-whitespace errors within one tracked range,
1083 # separated by clean lines. ctxlen is inflated to the range span,
1084 # so they land in a single xdiff hunk with the gap as context;
1085 # both must report their real file line number, with the context
1086 # lines between them counted.
1087 sed -e "s/int a = 1;/int a = 1; /" -e "s/int g = 7;/int g = 7; /" gap.c >tmp &&
1088 mv tmp gap.c &&
1089 git commit -a -m "ws errors with a gap" &&
1090 test_must_fail git log -L:tracked:gap.c --check --format= >actual &&
1091 test_grep "gap.c:3: trailing whitespace" actual &&
1092 test_grep "gap.c:8: trailing whitespace" actual
1093 '
1094
1095 test_expect_success '--check does not report blank-at-eof outside the range' '
1096 git checkout --orphan check-eof &&
1097 git reset --hard &&
1098 printf "void tracked()\n{\n return;\n}\n\nint tail = 1;\n" >eof.c &&
1099 git add eof.c &&
1100 test_tick &&
1101 git commit -m "add eof.c" &&
1102 # One commit introduces a trailing-whitespace error inside tracked()
1103 # (line 3) and a blank line at end of file (line 7, outside the
1104 # range). The blank-at-eof check scans the whole file, so it must be
1105 # scoped: report the in-range error, not the out-of-range EOF blank.
1106 printf "void tracked()\n{\n return; \n}\n\nint tail = 1;\n\n" >eof.c &&
1107 git commit -a -m "ws in range, blank at eof out of range" &&
1108 test_must_fail git log -L:tracked:eof.c --check --format= >actual &&
1109 test_grep "eof.c:3: trailing whitespace" actual &&
1110 test_grep ! "blank line at EOF" actual
1111 '
1112
1113 test_expect_success '-L -G is scoped to the tracked range' '
1114 git checkout --orphan grep-scope &&
1115 git reset --hard &&
1116 cat >gp.c <<-\EOF &&
1117 int func1()
1118 {
1119 return ALPHA;
1120 }
1121
1122 int func2()
1123 {
1124 return BETA;
1125 }
1126 EOF
1127 git add gp.c &&
1128 test_tick &&
1129 git commit -m "add gp.c" &&
1130 sed -e "s/ALPHA/ALPHA2/" -e "s/BETA/BETA2/" gp.c >tmp &&
1131 mv tmp gp.c &&
1132 git commit -a -m "touch both functions" &&
1133 # The commit changes ALPHA (func1) and BETA (func2). Tracking func2,
1134 # -G BETA matches its in-range change; -G ALPHA must not, since ALPHA
1135 # changes only outside the tracked range.
1136 git log -L:func2:gp.c -G BETA --format=%s >actual &&
1137 test_grep "touch both functions" actual &&
1138 git log -L:func2:gp.c -G ALPHA --format=%s >actual &&
1139 test_grep ! "touch both functions" actual
1140 '
1141
1142 test_expect_success '-L -G searches the whole file under textconv' '
1143 git checkout --orphan grep-textconv &&
1144 git reset --hard &&
1145 cat >tc.c <<-\EOF &&
1146 int func1()
1147 {
1148 return F1;
1149 }
1150
1151 int func2()
1152 {
1153 return F2;
1154 }
1155 EOF
1156 git add tc.c &&
1157 test_tick &&
1158 git commit -m "add tc.c" &&
1159 # One commit changes func1 and func2; MAGIC lands only in the
1160 # func2 change, outside func1.
1161 sed -e "s/F1/F1 + 1/" -e "s/return F2/return MAGIC/" tc.c >tmp &&
1162 mv tmp tc.c &&
1163 git commit -a -m "change both funcs" &&
1164 echo "tc.c diff=tc" >.gitattributes &&
1165
1166 # Without a textconv driver, -G is scoped to func1, so MAGIC (only
1167 # in the func2 change) does not select the commit.
1168 git log -L:func1:tc.c -G MAGIC --format=%s --no-patch >actual &&
1169 test_must_be_empty actual &&
1170
1171 # A textconv driver makes the range (original-file line numbers)
1172 # meaningless against the driver output, so -G falls back to the
1173 # whole file and MAGIC now selects the commit.
1174 git config diff.tc.textconv cat &&
1175 git log -L:func1:tc.c -G MAGIC --format=%s --no-patch >actual &&
1176 test_grep "change both funcs" actual
1177 '
1178
1179 test_expect_success 'get_commit_action() does not mutate a not-yet-walked commit' '
1180 git init peek &&
1181 (
1182 cd peek &&
1183 test_write_lines 1 2 3 4 5 >f.c &&
1184 git add f.c && test_tick && git commit -m base &&
1185 test_write_lines 1 two 3 4 5 >f.c &&
1186 test_tick && git commit -am change &&
1187
1188 # Peek HEAD^, which the walk has not reached (the out-of-order
1189 # call a lookahead makes), and confirm get_commit_action() leaves
1190 # it untouched. A side effect is invisible in the commit list
1191 # (range merges are idempotent), so the helper reports whether the
1192 # call mutated the peeked commit at all.
1193 echo "mutated 0" >expect &&
1194 test-tool revision-walking line-log-peek HEAD^ 1,3:f.c HEAD >actual &&
1195 test_cmp expect actual
1196 )
1197 '
1198
1199 test_done