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