mergetool(vimdiff): allow paths to contain spaces again
PATCH 1/1 mergetool(vimdiff): allow paths to contain spaces again
From: Johannes Schindelin <[email protected]>
In 0041797449d (vimdiff: new implementation with layout support, 2022-03-30), we introduced a completely new implementation of the vimdiff backend for git mergetool.
In this implementation, we no longer call vim directly but we accumulate in the variable FINAL_CMD an arbitrary number of commands for vim to execute, which necessitates the use of eval to split the commands properly into multiple command-line arguments.
That same eval command also needs to pass the paths to vim, and while it looks as if they are quoted correctly, that quoting only reaches the eval instruction and is lost after that, therefore paths that contain whitespace characters (or other characters that are interpreted by the POSIX shell) are handled incorrectly.
This is a simple reproducer:
git init -b main bam-merge-fail
cd bam-merge-fail
echo a>"a file.txt"
git add "a file.txt"
git commit -m "added 'a file.txt'"
echo b>"a file.txt"
git add "a file.txt"
git commit -m "diverged b 'a file.txt'"
git checkout -b c HEAD~
echo c>"a file.txt"
git add "a file.txt"
git commit -m "diverged c 'a file.txt'"
git checkout main
git merge c
git mergetool --tool=vimdiff
With Git v2.37.0/v2.37.1, this will open 7 buffers, not four, and not display the correct contents at all.
To fix this, let's not expand the variables containing the path parameters before passing them to the eval command, but let that command expand the variables instead.
This fixes https://github.com/git-for-windows/git/issues/3945
---
mergetool(vimdiff): allow paths to contain spaces again
In https://github.com/git-for-windows/git/issues/3945, it was reported that as of v2.37.0, git mergetool --tool=vimdiff fails to handle paths containing spaces. Let's fix that.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1287%2Fdscho%2Ffix-vimdiff-with-spaces-in-paths-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1287/dscho/fix-vimdiff-with-spaces-in-paths-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1287
mergetools/vimdiff | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-)
mergetools/vimdiff +35 -4
@@ -414,8 +414,8 @@ merge_cmd () { if $base_present then - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"' else # If there is no BASE (example: a merge conflict in a new file # with the same name created in both braches which didn't exist
@@ -424,8 +424,8 @@ merge_cmd () { FINAL_CMD=$(echo "$FINAL_CMD" | \ sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g') - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"' fi ret="$?"
@@ -614,6 +614,37 @@ run_unit_tests () { fi done + # verify that `merge_cmd` handles paths with spaces + record_parameters () { + >actual + for arg + do + echo "$arg" >>actual + done + } + + base_present=1 + LOCAL='lo cal' + BASE='ba se' + REMOTE="' '" + MERGED='mer ged' + merge_tool_path=record_parameters + + merge_cmd vimdiff || at_least_one_ko=true + + cat >expect <<-\EOF + -f + -c + echo | split | vertical split | 1b | wincmd l | vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis + -c + tabfirst + lo cal + ' ' + mer ged + EOF + + diff -u expect actual || at_least_one_ko=true + if test "$at_least_one_ko" = "true" then return 255
base-commit: 980145f7470e20826ca22d7343494712eda9c81d
Signature · 1 line
gitgitgadget
"Johannes Schindelin via GitGitGadget" <[email protected]>
Quoted 3 lines · writes:
To fix this, let's not expand the variables containing the path parameters before passing them to the
evalcommand, but let that command expand the variables instead.
Yup, that is exactly the right approach to fix this kind of breakage.
Thanks for a clear description of the problem and the solution.
Fernando? Ack?
Quoted 3 lines · Junio C Hamano
Thanks for a clear description of the problem and the solution.
Fernando? Ack?
Yes. 100% Ack'ed.
Thanks Johannes for the bug report and the patch!
Quoted 8 lines · Fernando Ramos
Quoted 3 lines · Junio C Hamano
Thanks for a clear description of the problem and the solution.
Fernando? Ack?
Yes. 100% Ack'ed.
Thanks Johannes for the bug report and the patch!
Funny that we now seem to fail t7609 mergetool --tool=vimdiff tests, e.g.
https://github.com/git/git/actions/runs/2665800752
"Johannes Schindelin via GitGitGadget" <[email protected]>
mergetools/vimdiff merge_cmd () {
Quoted 4 lines · writes:
mergetools/vimdiff +1
-0if $base_present then + base_present=1
I think s/1/true/ or something is in order, perhaps?
Quoted 26 lines
mergetools/vimdiff +21
-0+ LOCAL='lo cal' + BASE='ba se' + REMOTE="' '" + MERGED='mer ged' + merge_tool_path=record_parameters + + merge_cmd vimdiff || at_least_one_ko=true + + cat >expect <<-\EOF + -f + -c + echo | split | vertical split | 1b | wincmd l | vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis + -c + tabfirst + lo cal + ' ' + mer ged + EOF + + diff -u expect actual || at_least_one_ko=true + if test "$at_least_one_ko" = "true" then return 255base-commit:
980145f7470e20826ca22d7343494712eda9c81d
Quoted 3 lines · Junio C Hamano
I think s/1/true/ or something is in order, perhaps?
Yes. I was just looking into that.
For one, as you said, "1" should be "true". That also changes the expected output.
Then, in addition, the expected output needs to be re-adjusted once again if we plan to apply this patch on top of the other one from two days ago (the one that adds the "leftabove" keyword to split subcommands).
After these changes, this is how the original patch from Johannes needs to be updated:
mergetools/vimdiff +3 -2
@@ -623,7 +623,7 @@ run_unit_tests () { done } - base_present=1 + base_present=true LOCAL='lo cal' BASE='ba se' REMOTE="' '"
@@ -635,10 +635,11 @@ run_unit_tests () { cat >expect <<-\EOF -f -c - echo | split | vertical split | 1b | wincmd l | vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis + echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis -c tabfirst lo cal + ba se ' ' mer ged EOF
Signature · 1 line
2.37.0
Quoted 9 lines · Fernando Ramos
Quoted 3 lines · Junio C Hamano
I think s/1/true/ or something is in order, perhaps?
Yes. I was just looking into that.
For one, as you said, "1" should be "true". That also changes the expected output.
OK, because "1" fails to execute, the expected output Dscho had (which is for the case without base) would become invalid when we use "true".
Perhaps we should use "false" instead? Or do we need to test both?
Quoted 34 lines
Then, in addition, the expected output needs to be re-adjusted once again if we plan to apply this patch on top of the other one from two days ago (the one that adds the "leftabove" keyword to split subcommands).
After these changes, this is how the original patch from Johannes needs to be updated:
mergetools/vimdiff +3
-2@@ -623,7 +623,7 @@ run_unit_tests () { done } - base_present=1 + base_present=true LOCAL='lo cal' BASE='ba se' REMOTE="' '"@@ -635,10 +635,11 @@ run_unit_tests () { cat >expect <<-\EOF -f -c - echo | split | vertical split | 1b | wincmd l | vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis + echo | -tabnew | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnext | -tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnext | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | tabdo windo diffthis -c tabfirst lo cal + ba se ' ' mer ged EOF
Quoted 8 lines · Junio C Hamano
Quoted 2 lines
For one, as you said, "1" should be "true". That also changes the expected output.
OK, because "1" fails to execute, the expected output Dscho had (which is for the case without base) would become invalid when we use "true".
Perhaps we should use "false" instead? Or do we need to test both?
How confident are you with the "leftabove" stuff? Is it solid enough that it is safe to assume that we can queue this fix on top of it?
This is how it would look like on top of the "leftabove" topic.
mergetools/vimdiff | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-)
mergetools/vimdiff +35 -4
@@ -414,8 +414,8 @@ merge_cmd () { if $base_present then - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"' else # If there is no BASE (example: a merge conflict in a new file # with the same name created in both braches which didn't exist
@@ -424,8 +424,8 @@ merge_cmd () { FINAL_CMD=$(echo "$FINAL_CMD" | \ sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g') - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"' fi ret="$?"
@@ -614,6 +614,37 @@ run_unit_tests () { fi done + # verify that `merge_cmd` handles paths with spaces + record_parameters () { + >actual + for arg + do + echo "$arg" >>actual + done + } + + base_present=false + LOCAL='lo cal' + BASE='ba se' + REMOTE="' '" + MERGED='mer ged' + merge_tool_path=record_parameters + + merge_cmd vimdiff || at_least_one_ko=true + + cat >expect <<-\EOF + -f + -c + echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis + -c + tabfirst + lo cal + ' ' + mer ged + EOF + + diff -u expect actual || at_least_one_ko=true + if test "$at_least_one_ko" = "true" then return 255
Quoted 7 lines · Junio C Hamano
OK, because "1" fails to execute, the expected output Dscho had (which is for the case without base) would become invalid when we use "true".
Perhaps we should use "false" instead? Or do we need to test both?
I think testing both is not really needed as the unit test is just making sure that filenames with spaces are processed correctly. Whatever comes before (which changes depending on the value of "base_present") doesn't really matter as long as there is something.
If we have to select one ("true" or "false") using "true" seems more practical, as that way the BASE file is also included, and thus an extra argument is tested.
Quoted 5 lines · Junio C Hamano
How confident are you with the "leftabove" stuff? Is it solid enough that it is safe to assume that we can queue this fix on top of it?
I would say yes. I have been testing it since without problems.
But it is also true that the "leftabove" patch fixes a corner case that can wait until the next release.
I will keep testing it in any case and let you know if I find a case where it doesn't work.
Quoted 6 lines · Fernando Ramos
Quoted 5 lines · Junio C Hamano
OK, because "1" fails to execute, the expected output Dscho had (which is for the case without base) would become invalid when we use "true".
Quoted 6 lines
Quoted 2 lines
Perhaps we should use "false" instead? Or do we need to test both?
I think testing both is not really needed as the unit test is just making sure that filenames with spaces are processed correctly. Whatever comes before (which changes depending on the value of "
base_present") doesn't really matter as long as there is something.
As the quoting glitch are distributed on both sides
if $base_present then eval "$merge_tool_path" \ -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED" else # If there is no BASE (example: a merge conflict in a new file # with the same name created in both braches which didn't exist # before), close all BASE windows using vim's "quit" command FINAL_CMD=$(echo "$FINAL_CMD" | \ sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g') eval "$merge_tool_path" \ -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED" fi
I suspect you can fix only one, and carefully choose which side to test, and leave the other side broken ;-)
In any case, to minimize disruption to Dscho's patch, I replaced the "1" with "false" (which will make his patch pass the new test without other stuff) and tweak its merge into 'seen' to adjust for the "leftabove" topic.
Thanks.
PATCH v2 1/1 mergetool(vimdiff): allow paths to contain spaces again
In reply to Johannes Schindelin via GitGitGadgetFrom: Johannes Schindelin <[email protected]>
In 0041797449d (vimdiff: new implementation with layout support, 2022-03-30), we introduced a completely new implementation of the vimdiff backend for git mergetool.
In this implementation, we no longer call vim directly but we accumulate in the variable FINAL_CMD an arbitrary number of commands for vim to execute, which necessitates the use of eval to split the commands properly into multiple command-line arguments.
That same eval command also needs to pass the paths to vim, and while it looks as if they are quoted correctly, that quoting only reaches the eval instruction and is lost after that, therefore paths that contain whitespace characters (or other characters that are interpreted by the POSIX shell) are handled incorrectly.
This is a simple reproducer:
git init -b main bam-merge-fail
cd bam-merge-fail
echo a>"a file.txt"
git add "a file.txt"
git commit -m "added 'a file.txt'"
echo b>"a file.txt"
git add "a file.txt"
git commit -m "diverged b 'a file.txt'"
git checkout -b c HEAD~
echo c>"a file.txt"
git add "a file.txt"
git commit -m "diverged c 'a file.txt'"
git checkout main
git merge c
git mergetool --tool=vimdiff
With Git v2.37.0/v2.37.1, this will open 7 buffers, not four, and not display the correct contents at all.
To fix this, let's not expand the variables containing the path parameters before passing them to the eval command, but let that command expand the variables instead.
This fixes https://github.com/git-for-windows/git/issues/3945
---
mergetool(vimdiff): allow paths to contain spaces again
In https://github.com/git-for-windows/git/issues/3945, it was reported that as of v2.37.0, git mergetool --tool=vimdiff fails to handle paths containing spaces. Let's fix that.
Changes since v1:
* Using base_present=false instead of the misleading base_present=1 (thanks Junio & Fernando!)
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1287%2Fdscho%2Ffix-vimdiff-with-spaces-in-paths-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1287/dscho/fix-vimdiff-with-spaces-in-paths-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1287
Range-diff vs v1:
1: dde9c7cd2ea ! 1: 7de381b6913 mergetool(vimdiff): allow paths to contain spaces again
@@ mergetools/vimdiff: run_unit_tests () {
+ done
+ }
+
-+ base_present=1
++ base_present=false
+ LOCAL='lo cal'
+ BASE='ba se'
+ REMOTE="' '"mergetools/vimdiff | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-)
mergetools/vimdiff +35 -4
@@ -414,8 +414,8 @@ merge_cmd () { if $base_present then - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"' else # If there is no BASE (example: a merge conflict in a new file # with the same name created in both braches which didn't exist
@@ -424,8 +424,8 @@ merge_cmd () { FINAL_CMD=$(echo "$FINAL_CMD" | \ sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g') - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"' fi ret="$?"
@@ -614,6 +614,37 @@ run_unit_tests () { fi done + # verify that `merge_cmd` handles paths with spaces + record_parameters () { + >actual + for arg + do + echo "$arg" >>actual + done + } + + base_present=false + LOCAL='lo cal' + BASE='ba se' + REMOTE="' '" + MERGED='mer ged' + merge_tool_path=record_parameters + + merge_cmd vimdiff || at_least_one_ko=true + + cat >expect <<-\EOF + -f + -c + echo | split | vertical split | 1b | wincmd l | vertical split | quit | wincmd l | 2b | wincmd j | 3b | tabdo windo diffthis + -c + tabfirst + lo cal + ' ' + mer ged + EOF + + diff -u expect actual || at_least_one_ko=true + if test "$at_least_one_ko" = "true" then return 255
base-commit: 980145f7470e20826ca22d7343494712eda9c81d
Signature · 1 line
gitgitgadget
"Johannes Schindelin via GitGitGadget" <[email protected]>
Quoted 10 lines · writes:
mergetools/vimdiff +2
-2if $base_present then - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$BASE" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$BASE"' '"$REMOTE"' '"$MERGED"' else # If there is no BASE (example: a merge conflict in a new file # with the same name created in both braches which didn't exist
mergetools/vimdiff merge_cmd () { · line 429
Quoted 8 lines
mergetools/vimdiff +2
-2@@ -424,8 +424,8 @@ merge_cmd () { FINAL_CMD=$(echo "$FINAL_CMD" | \ sed -e 's:2b:quit:g' -e 's:3b:2b:g' -e 's:4b:3b:g') - eval "$merge_tool_path" \ - -f "$FINAL_CMD" "$LOCAL" "$REMOTE" "$MERGED" + eval '"$merge_tool_path"' \ + -f "$FINAL_CMD" '"$LOCAL"' '"$REMOTE"' '"$MERGED"' fi
If there were another syntactic fix we need in the future, we may by mistake fix one but not the other, and the test we add in this patch checks only one side but not the other. In a follow-up we may want to unify the two eval invocations to make the testing of this part more robust.
But as a minimum and obvious fix, stopping at the above is appropriate for this patch.
Review merge_cmd () { · line 431
Quoted 2 lines
(quoted diff) +2
-0+ ... + diff -u expect actual || at_least_one_ko=true
I wonder if we still should care about platforms that need to set GIT_TEST_CMP_USE_COPIED_CONTEXT while running our tests. If we use "diff -u" hardcoded like this here, we are declaring that they are now unwelcome to run our tests.
It might be just the matter of using "cmp -s" here (or run "diff" without any option). Do we care about emitting the difference in the output? I doubt it.
mergetools/vimdiff run_unit_tests () { · line 650
Quoted 3 lines
mergetools/vimdiff +0
-0if test "$at_least_one_ko" = "true" then return 255
Thanks.