34

Is it possible to configure Git to use my configured difftool with git add --patch?

I would like to pick the changes to add to the index via my own difftool.

6
  • Is Git even covered on StackOverflow? I would think this would be a better question for SuperUser. Commented Jan 26, 2012 at 19:30
  • You might be right. Is there a migrate button to move it over ? Commented Jan 26, 2012 at 19:32
  • Nope, gotta let a mod do it, or just ask again if you don't feel like waiting. Commented Jan 26, 2012 at 19:32
  • 3
    @SpikeX: Questions about programming tools are appropriate to Stack Overflow. Commented Jan 26, 2012 at 19:36
  • @SpikeX: See the faq; the scope includes "software tools commonly used by programmers". Click the git tag up there and you'll see the thousands of previous questions. (You'll also see questions about other VCS/SCMs, programming editors, debuggers, profilers, compilers...) Please at least read the faq thoroughly before passing judgments about topicality of questions. Commented Jan 26, 2012 at 19:39

5 Answers 5

12

No, unfortunately.

I suppose I can see that working - Git generates a temporary file based on what's currently in the index, hands it to the difftool along with a copy of the current work tree version (to protect you from making further changes), lets you use the difftool to move some of the changes to the index version, then once you save and quit, stages whatever content is in that modified index version. Note that this would require the difftool to also be a bit of an editor, and not all valid difftools are; some of them are just for viewing diffs. Note also that this is basically bypassing all of git add -p. You wouldn't have any of the normal interface from it for moving between hunks, splitting hunks, and so on. The difftool would be entirely responsible for all of that.

If your difftool is fully-featured enough to do this sort of thing, then I suppose you could write a script to do it. An outline, without really any error protection, handling of special cases (binary files?), and completely untested:

#!/bin/bash tmpdir=$(mktemp -d) git diff --name-only | while read file; do cp "$file" $tmpdir # this has your changes in it work_tree_version="$tmpdir/$file" # this has the pristine version index_version=$(git checkout-index --temp "$file") # and now you bring changes from the work tree version into the index version, # within the difftool, and save the index version and quit when done my_difftool "$work_tree_version" "$index_version" # swap files around to run git add mv "$file" "$work_tree_version" mv "$index_version" "$file" git add "$file" mv "$work_tree_version" "$file" # you could also do this by calculating the diff and applying it directly to the index # git diff --no-index -- "$file" "$original_index_version" | git apply --cached rm -r $tmpdir 

Probably a lot of ways to improve that; sorry I don't have time to be careful and thorough with it right now.

Sign up to request clarification or add additional context in comments.

9 Comments

I think maybe I asked it wrong. Can I use my difftool (They are the same for me) ? This way I can pick all the changes I want to add to the index. I'll update the question.
That's a really cool idea. I could make this a script and then add a git alias git diffadd or something. I'll try cleaning up your code a little bit and make it a bit more robust. Thanks!
@HaxElit: If you come up with something solid, please feel free to edit it into my answer, or post your own!
I have updated the code with the version I use. It's works pretty good. I can't imagine doing a commit with out using it now.
The while loop is missing done. Also git checkout-index returns temp filename, TAB char, then original filename so I used index_version=$(git checkout-index --temp "$file" | cut -f1)
|
4

Here's my script for this, which opens kdiff3 for you to perform a 2-file merge. If you don't like kdiff3, provide your own values for MERGETOOL and MERGECMD (but you'd be crazy not to like kdiff3).

To avoid surprises, this script tries to mimic git add -p as far as arguments and error codes. (It handles both lists of files and directories.)

Plus, it properly handles various corner cases, including:

  • The user tries to run the script in a non-git directory (abort with an error)
  • The user hits Ctrl+C before finishing (quit early)
  • The user declines to save the merge result within the difftool (then don't use it, but move on to the next file)
  • The difftool has an unexpected error (stop early)

Example usage:

$ ## With kdiff3 (default): $ add-with-mergetool myfile1.txt $ add-with-mergetool some-directory $ ## ...or with custom mergetool: $ export MERGETOOL='opendiff' $ export MERGECMD='$MERGETOOL $LOCAL $REMOTE -merge $MERGED' $ add-with-mergetool some-directory/*.py 
#!/bin/bash # # add-with-mergetool # Author: Stuart Berg (http://github.com/stuarteberg) # # This little script is like 'git add --patch', except that # it launches a merge-tool to perform the merge. # TODO: For now, this script hard-codes MERGETOOL and MERGECMD for kdiff3. # Modify those variables for your own tool if you wish. # In the future, it would be nice if we could somehow read # MERGETOOL and MERGECMD from the user's git-config. # Configure for kdiff3 # (and hide warnings on about modalSession, from kdiff3 on OSX) MERGETOOL=${MERGETOOL-kdiff3} MERGECMD=${MERGECMD-'"${MERGETOOL}" "${LOCAL}" "${REMOTE}" -o "${MERGED}"'\ 2>&1 | grep -iv modalSession} main() { check_for_errors "$@" process_all "$@" } check_for_errors() { which "${MERGETOOL}" > /dev/null if [[ $? == 1 ]]; then echo "Error: Can't find mergetool: '${MERGETOOL}'" 1>&2 exit 1 fi if [[ "$1" == "-h" ]]; then echo "Usage: $(basename $0) [<pathspec>...]" 1>&2 exit 0 fi # Exit early if we're not in a git repo git status > /dev/null || exit $? } process_all() { repo_toplevel=$(git rev-parse --show-toplevel) # If no args given, add everything (like 'git add -p') if [[ $# == 0 ]]; then set -- "$repo_toplevel" fi # For each given file/directory... args=( "$@" ) for arg in "${args[@]}" do # Find the modified file(s) changed_files=( $(git diff --name-only -- "$arg") ) ( # Switch to toplevel, to easily handle 'git diff' output cd "$repo_toplevel" # For each modified file... for f in "${changed_files[@]}" do if [[ $startmsg_shown != "yes" ]]; then echo "Starting $(basename $0). Use Ctrl+C to stop early." echo "To skip a file, quit ${MERGETOOL} without saving." echo startmsg_shown="yes" fi # This is where the magic happens. patch_file_and_add "$f" done ) || exit $? # exit early if loop body failed done } # This helper function launches the mergetool for a single file, # and then adds it to the git index (if the user saved the new file). patch_file_and_add() { f="$1" git show :"$f" > "$f.from_index" # Copy from the index ( set -e trap "echo && exit 130" INT # Ctrl+C should trigger abnormal exit # Execute 2-file merge echo "Launching ${MERGETOOL} for '$f'." LOCAL="$f.from_index" REMOTE="$f" MERGED="$f.to_add" eval "${MERGECMD}" if [[ -e "$f.to_add" ]]; then mv "$f" "$f.from_working" # Backup original from working-tree mv "$f.to_add" "$f" # Replace with patched version git add "$f" # Add to the index mv "$f.from_working" "$f" # Restore the working-tree version fi ) status=$? rm "$f.from_index" # Discard the old index version if [ $status == 130 ]; then echo "User interrupted." 1>&2 exit $status elif [ $status != 0 ]; then echo "Error: Interactive add-patch stopped early!" 1>&2 exit $status fi } main "$@" 

1 Comment

Should you change the line right below "Find the modified file(s)" to changed_files=( "$(git diff --name-only -- "$arg")" )? There could be a space in the path.
2

I like the answers from Cascabel and Stuart because they show how to script a cover that accomplishes the desired task. They use git add instead of git add --patch as the question stated, but are still in the spirit of the question. Another alternative is to use git add --edit which has the advantage that it does not need to modify the working tree at all. My answer is logically a small change to Stuart's script.

The issue I see with git add --patch is that it is inherently interactive, making it hard to cover with a script. Stuart's approach is to use a diff tool to determine the desired full content of the index and then use git add to make it so. My approach differs in that instead of modifying the working tree before and after calling git add I take the desired full content and turn it into a patch that can be applied to the index to make it so. Then EDITOR="mv \"$PATCH\"" git add --edit can be used to make it so. This avoids modifications to the working tree.

To use this approach, start with Stuart's script and replace the definition of patch_file_and_add with this:

patch_file_and_add() { f="$1" base=$(basename "$f") dir=$(mktemp -d) mkdir "$dir/working" mkdir "$dir/index" mkdir "$dir/merged" LOCAL="$dir/working/$base" REMOTE="$dir/index/$base" MERGED="$dir/merged/$base" PATCH1="$dir/head.patch" PATCH="$dir/full.patch" git show :"$f" > "$REMOTE" # Copy from the index ( set -e trap "echo && exit 130" INT # Ctrl+C should trigger abnormal exit # Execute 2-file merge echo "Launching ${MERGETOOL} for '$f'." cp "$f" "$LOCAL" eval "${MERGECMD}" if [[ -e "$MERGED" ]]; then git diff -- "$f" > "$PATCH1" 2> /dev/null git diff --staged -- "$f" >> "$PATCH1" 2> /dev/null # We need both of the above in case one is empty. head -4 "$PATCH1" > "$PATCH" diff --unified=7 "$REMOTE" "$MERGED" | tail -n +3 >> "$PATCH" # Now we have the patch we want to apply to the index. EDITOR="mv \"$PATCH\"" git add -e -- "$f" fi rm -rf "$dir" ) status=$? if [ $status == 130 ]; then echo "User interrupted." 1>&2 exit $status elif [ $status != 0 ]; then echo "Error: Interactive add-patch stopped early!" 1>&2 exit $status fi } 

Strictly speaking LOCAL could be set to $f instead of using mv to put it next to the others. But I seem to remember that some 3rd party diff programs allow hiding the common initial part of the path, so this approach can take advantage of that feature.

Thanks Cascabel and Stuart, for excellent answers, and HaxElit for an excellent question.

Comments

2

If vimdiff is your difftool in .gitconfig:

[diff] tool = vimdiff 

you could also execute the command below while standing in the file screen:

:!git add % 

Comments

0

Unfortunately not.

The only UI I know of at the moment is part of git-gui when invoked as

git gui citool 

The other UI is the interactive console UI when invoked as

git add -i 

git difftool allows some different tool options, but not the add interface.

1 Comment

I'm not sure I see the relevance of this; the OP is asking about git add --patch|-p, which lets you selectively choose hunks of the patch to stage. git gui citool definitely doesn't do that, so it's irrelevant. And git add -i is capable of invoking git add -p, so it's a roundabout way to do what the OP already knows about, and otherwise doesn't do what he wants. So the substance of your answer is "no", which I feel I pretty well covered.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.