whitespace: symbolic links usually lack LF at the end
For a patch that touches a symbolic link, it is perfectly normal that the contents ends with "\ No newline at end of file". The checks introduced recently to detect incomplete lines (i.e., a text file that lack the newline on its final line) should not trigger. Disable the check early for symbolic links, both in "git apply" and "git diff" and test them. For "git apply", we check only when the postimage is a symbolic link regardless of the preimage, and we only care about preimage when applying in reverse. Similarly, "git diff" would warn only when the postimage is a symbolic link, or the preimage when running "git diff -R". Signed-off-by: Junio C Hamano <gitster@pobox.com>
Junio C Hamano committed
Feb 4, 2026 at 13:23 UTC
6a41481c6de1cedb059930a710896c5ab2508a12
4 files changed
+152
-2
apply.c
+20
@@ -1725,6 +1725,26 @@ static int parse_fragment(struct apply_state *state,
1725
unsigned long oldlines, newlines;
1726
unsigned long leading, trailing;
1727
1728
+ /* do not complain a symbolic link being an incomplete line */
1729
+ if (patch->ws_rule & WS_INCOMPLETE_LINE) {
1730
+ /*
1731
+ * We want to figure out if the postimage is a
1732
+ * symbolic link when applying the patch normally, or
1733
+ * if the preimage is a symbolic link when applying
1734
+ * the patch in reverse. A normal patch only has
1735
+ * old_mode without new_mode. If it changes the
1736
+ * filemode, new_mode has value, which is different
1737
+ * from old_mode.
1738
+ */
1739
+ unsigned mode = (state->apply_in_reverse
1740
+ ? patch->old_mode
1741
+ : patch->new_mode
1742
+ ? patch->new_mode
1743
+ : patch->old_mode);
1744
+ if (mode && S_ISLNK(mode))
1745
+ patch->ws_rule &= ~WS_INCOMPLETE_LINE;
1746
+ }
1747
+
1748
offset = parse_fragment_header(line, len, fragment);
1749
if (offset < 0)
1750
return -1;
diff.c
+20
-2
@@ -1834,6 +1834,7 @@ static void emit_rewrite_diff(const char *name_a,
1834
const char *a_prefix, *b_prefix;
1835
char *data_one, *data_two;
1836
size_t size_one, size_two;
1837
+ unsigned ws_rule;
1838
struct emit_callback ecbdata;
1839
struct strbuf out = STRBUF_INIT;
1840
@@ -1856,9 +1857,15 @@ static void emit_rewrite_diff(const char *name_a,
1857
size_one = fill_textconv(o->repo, textconv_one, one, &data_one);
1858
size_two = fill_textconv(o->repo, textconv_two, two, &data_two);
1859
1860
+ ws_rule = whitespace_rule(o->repo->index, name_b);
1861
+
1862
+ /* symlink being an incomplete line is not a news */
1863
+ if (DIFF_FILE_VALID(two) && S_ISLNK(two->mode))
1864
+ ws_rule &= ~WS_INCOMPLETE_LINE;
1865
+
1866
memset(&ecbdata, 0, sizeof(ecbdata));
1867
ecbdata.color_diff = o->use_color;
1861
- ecbdata.ws_rule = whitespace_rule(o->repo->index, name_b);
1868
+ ecbdata.ws_rule = ws_rule;
1869
ecbdata.opt = o;
1870
if (ecbdata.ws_rule & WS_BLANK_AT_EOF) {
1871
mmfile_t mf1, mf2;
@@ -3762,6 +3769,7 @@ static void builtin_diff(const char *name_a,
3769
xpparam_t xpp;
3770
xdemitconf_t xecfg;
3771
struct emit_callback ecbdata;
3772
+ unsigned ws_rule;
3773
const struct userdiff_funcname *pe;
3774
3775
if (must_show_header) {
@@ -3773,6 +3781,12 @@ static void builtin_diff(const char *name_a,
3781
mf1.size = fill_textconv(o->repo, textconv_one, one, &mf1.ptr);
3782
mf2.size = fill_textconv(o->repo, textconv_two, two, &mf2.ptr);
3783
3784
+ ws_rule = whitespace_rule(o->repo->index, name_b);
3785
+
3786
+ /* symlink being an incomplete line is not a news */
3787
+ if (DIFF_FILE_VALID(two) && S_ISLNK(two->mode))
3788
+ ws_rule &= ~WS_INCOMPLETE_LINE;
3789
+
3790
pe = diff_funcname_pattern(o, one);
3791
if (!pe)
3792
pe = diff_funcname_pattern(o, two);
@@ -3784,7 +3798,7 @@ static void builtin_diff(const char *name_a,
3798
lbl[0] = NULL;
3799
ecbdata.label_path = lbl;
3800
ecbdata.color_diff = o->use_color;
3787
- ecbdata.ws_rule = whitespace_rule(o->repo->index, name_b);
3801
+ ecbdata.ws_rule = ws_rule;
3802
if (ecbdata.ws_rule & WS_BLANK_AT_EOF)
3803
check_blank_at_eof(&mf1, &mf2, &ecbdata);
3804
ecbdata.opt = o;
@@ -3991,6 +4005,10 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
4005
data.ws_rule = whitespace_rule(o->repo->index, attr_path);
4006
data.conflict_marker_size = ll_merge_marker_size(o->repo->index, attr_path);
4007
4008
+ /* symlink being an incomplete line is not a news */
4009
+ if (DIFF_FILE_VALID(two) && S_ISLNK(two->mode))
4010
+ data.ws_rule &= ~WS_INCOMPLETE_LINE;
4011
+
4012
if (fill_mmfile(o->repo, &mf1, one) < 0 ||
4013
fill_mmfile(o->repo, &mf2, two) < 0)
4014
die("unable to read files to diff");
t/t4015-diff-whitespace.sh
+26
@@ -90,6 +90,32 @@ test_expect_success "new incomplete line in post-image" '
90
git -c core.whitespace=incomplete diff -R --check x
91
'
92
93
+test_expect_success SYMLINKS "incomplete-line error is disabled for symlinks" '
94
+ test_when_finished "git reset --hard" &&
95
+ test_when_finished "rm -f mylink" &&
96
+
97
+ # a regular file with an incomplete line
98
+ printf "%s" one >mylink &&
99
+ git add mylink &&
100
+
101
+ # a symbolic link
102
+ rm mylink &&
103
+ ln -s two mylink &&
104
+
105
+ git -c diff.color=always -c core.whitespace=incomplete \
106
+ diff mylink >forward.raw &&
107
+ test_decode_color >forward <forward.raw &&
108
+ test_grep ! "<BRED>\\\\ No newline at end of file<RESET>" forward &&
109
+
110
+ git -c diff.color=always -c core.whitespace=incomplete \
111
+ diff -R mylink >reverse.raw &&
112
+ test_decode_color >reverse <reverse.raw &&
113
+ test_grep "<BRED>\\\\ No newline at end of file<RESET>" reverse &&
114
+
115
+ git -c core.whitespace=incomplete diff --check mylink &&
116
+ test_must_fail git -c core.whitespace=incomplete diff --check -R mylink
117
+'
118
+
119
test_expect_success "Ray Lehtiniemi's example" '
120
cat <<-\EOF >x &&
121
do {
t/t4124-apply-ws-rule.sh
+86
@@ -743,4 +743,90 @@ test_expect_success 'incomplete line modified at the end (error)' '
743
test_cmp sample target
744
'
745
746
+test_expect_success "incomplete-line error is disabled for symlinks" '
747
+ test_when_finished "git reset" &&
748
+ test_when_finished "rm -f patch.txt" &&
749
+ oneblob=$(printf "one" | git hash-object --stdin -w -t blob) &&
750
+ twoblob=$(printf "two" | git hash-object --stdin -w -t blob) &&
751
+
752
+ oneshort=$(git rev-parse --short $oneblob) &&
753
+ twoshort=$(git rev-parse --short $twoblob) &&
754
+
755
+ cat >patch0.txt <<-EOF &&
756
+ diff --git a/mylink b/mylink
757
+ index $oneshort..$twoshort 120000
758
+ --- a/mylink
759
+ +++ b/mylink
760
+ @@ -1 +1 @@
761
+ -one
762
+ \ No newline at end of file
763
+ +two
764
+ \ No newline at end of file
765
+ EOF
766
+
767
+ # the index has the preimage symlink
768
+ git update-index --add --cacheinfo "120000,$oneblob,mylink" &&
769
+
770
+ # check the patch going forward and reverse
771
+ git -c core.whitespace=incomplete apply --cached --check \
772
+ --whitespace=error patch0.txt &&
773
+
774
+ git update-index --add --cacheinfo "120000,$twoblob,mylink" &&
775
+ git -c core.whitespace=incomplete apply --cached --check \
776
+ --whitespace=error -R patch0.txt &&
777
+
778
+ # the patch turns it into the postimage symlink
779
+ git update-index --add --cacheinfo "120000,$oneblob,mylink" &&
780
+ git -c core.whitespace=incomplete apply --cached --whitespace=error \
781
+ patch0.txt &&
782
+
783
+ # and then back.
784
+ git -c core.whitespace=incomplete apply --cached -R --whitespace=error \
785
+ patch0.txt &&
786
+
787
+ # a text file turns into a symlink
788
+ cat >patch1.txt <<-EOF &&
789
+ diff --git a/mylink b/mylink
790
+ deleted file mode 100644
791
+ index $oneshort..0000000
792
+ --- a/mylink
793
+ +++ /dev/null
794
+ @@ -1 +0,0 @@
795
+ -one
796
+ \ No newline at end of file
797
+ diff --git a/mylink b/mylink
798
+ new file mode 120000
799
+ index 0000000..$twoshort
800
+ --- /dev/null
801
+ +++ b/mylink
802
+ @@ -0,0 +1 @@
803
+ +two
804
+ \ No newline at end of file
805
+ EOF
806
+
807
+ # the index has the preimage text
808
+ git update-index --cacheinfo "100644,$oneblob,mylink" &&
809
+
810
+ # check
811
+ git -c core.whitespace=incomplete apply --cached \
812
+ --check --whitespace=error patch1.txt &&
813
+
814
+ # reverse, leaving an incomplete text file, should error
815
+ git update-index --cacheinfo "120000,$twoblob,mylink" &&
816
+ test_must_fail git -c core.whitespace=incomplete \
817
+ apply --cached --check --whitespace=error -R patch1.txt &&
818
+
819
+ # apply to create a symbolic link
820
+ git update-index --cacheinfo "100644,$oneblob,mylink" &&
821
+ git -c core.whitespace=incomplete apply --cached --whitespace=error \
822
+ patch1.txt &&
823
+
824
+ # turning it back into an incomplete text file is an error
825
+ test_must_fail git -c core.whitespace=incomplete \
826
+ apply --cached --whitespace=error -R patch1.txt
827
+
828
+
829
+
830
+'
831
+
832
test_done