Quite often when using Git, I will rename a file and then modify it:
# Create file and commit
echo 1 > foo
git add .
git commit -m "A"
# Later, rename it
mv foo bar
# Later, modify it
echo 2 >> bar
Afterwards, I want to:
However, git add --patch
does not provide this option. It only prompts the user to stage the deletion of foo
(old filename), and the addition of bar
(new filename).
Is there a command I can use to only stage the rename, so I can then use git add --patch
to stage modifications separately?
Note: I understand git mv
provides some help here, as it renames the file and immediately stages the deletion/addition, so future interactive git add
s will only include modification diffs. However, that's not always practical—sometimes renames happen outside of my control, such as when using an IDE.
The When a file is modified (properties only) and When a file is modified should both work, and should both fire when the file is modified. Since renaming is a kind of modification, it should work, and I just tested it and it also works.
Add a second stage file action to read individual records of each file, modify or transform them, and write the modified records into a new file. On the right side of the canvas, click Actions and drag and drop the Stage File action inside the For_Each_InputRow action.
More importantly, you can use any tool you like to rename a file, and address the add/rm later, before you commit.
Stage Files to Prepare for Commit 1 Enter one of the following commands, depending on what you want to do: Stage all files: git add . ... 2 Check the status again by entering the following command: git status 3 You should see there are changes ready to be committed.
Is there a command I can use to only stage the rename, so I can then use
git add --interactive
to stage modifications separately?
There is no nice user oriented command, which Git calls porcelain commands, for this. (Mercurial has one—hg mv --after
—and it would not be unreasonable to lobby for an --after
option in git mv
, to give you that.) There is a plumbing command you can use, though; in fact, you can implement your own git mv-after
using this, and I have done so.
First, we should mention Git's index. Git, like any commit-oriented version control system, has both a current commit, which Git calls HEAD
, and a work-tree, which is where you have your files in their ordinary, non-version-controlled form so that all your normal non-version-control software can use them. But Git introduces an intermediate step, called the index or staging area. The short description of the index is that it is where you build the next commit.
When it comes to renaming files, there are a couple of intertwined issues here. The first is that Git does not actually track renames at all. Instead, it reconstructs (i.e., guesses-at) renames at the time you request a diff, including the git show
, git log -p
, and even git status
commands. This means that what you need to do is tell Git to remove the existing index entry for the old path name, and add a new index entry for the new path name.
Second, while there's a porcelain command to remove an index entry without touching the work-tree, the porcelain command to add an index entry is the same as the porcelain command to update an existing index entry. Specifically:
git rm --cached path/to/file.ext
removes the index entry without touching the work-tree at all, and hence can remove an index entry that no longer has a corresponding work-tree file. But:
git add path/to/newname.ext
not only creates an index entry for the new file, it does so by copying the current contents of the file into the index. (This is slightly misleading, as we will see in a moment, but it is the problem.) So if the file has been both renamed and modified by some GUI or IDE or other non-Git program, and you use both Git commands, this removes the old index entry just fine, but it writes the new data for the file under its new name, rather than copying the old data from the old index entry.
If only we had git mv --after
, we might use it like this:
$ git status
$ program-that-renames-file-and-modifies-it
$ git status --short
D name.ext
?? newname.ext
$ git mv --after name.ext newname.ext
to tell Git "take the index entry for name.ext
and start calling it newname.ext
instead". But we don't, and this fails:
$ git mv name.ext newname.ext
fatal: bad source, source=name.ext, destination=newname.ext
There is a simple but clunky workaround:
git mv
to update the index.Hence:
$ git checkout -- name.ext && \
mv newname.ext temp-save-it && \
git mv name.ext newname.ext && \
mv temp-save-it newname.ext
does the trick, but we must invent a temporary name (temp-save-it
) and guarantee that it's unique.
git mv-after
If we run git ls-files --stage
, we see exactly what's in the index:
$ git ls-files --stage
100644 038d718da6a1ebbc6a7780a96ed75a70cc2ad6e2 0 README
100644 77df059b7ea5adaf8c7e238fe2a9ce8b18b9a6a6 0 name.ext
What the index stores is not actually the file's content, but rather the hash ID of one particular version of the file in the repository. (Also, between the stage number 0
and the path name is a literal ASCII TAB character, character-code 9; this matters.)
All we need to do is add a new index entry that has the same mode and hash ID (and stage number 0) under the new name, while removing the old index entry. There is a plumbing command to do just this, git update-index
. With the --index-info
, the command reads its standard input, which should be formatted exactly the same way git ls-files --stage
writes it.
The script to do this is a bit long, so I have it below and in my "published scripts" repository now. But here it is in action:
$ git mv-after name.ext newname.ext
$ git status --short
RM name.ext -> newname.ext
The script could probably use a bit more work—for instance, a control-A in the file name will confuse the final sed
—but it does function. Place the script somewhere in your path (in my case, it is in my ~/scripts/
directory), name it git-mv-after
, and invoke it as git mv-after
.
#! /bin/sh
#
# mv-after: script to rename a file in the index
. git-sh-setup # for die() etc
TAB=$'\t'
# should probably use OPTIONS_SPEC, but not yet
usage()
{
echo "usage: git mv-after oldname newname"
echo "${TAB}oldname must exist in the index; newname must not"
}
case $# in
2) ;;
*) usage 1>&2; exit 1;;
esac
# git ls-files --stage does not test whether the entry is actually
# in the index; it exits with status 0 even if not. But it outputs
# nothing so we can test that.
#
# We do, however, want to make sure that the file is at stage zero
# (only).
getindex()
{
local output extra
output="$(git ls-files --stage -- "$1")"
[ -z "$output" ] && return 1
extra="$(echo "$output" | sed 1d)"
[ -z "$extra" ] || return 1
set -- $output
[ $3 == 0 ] || return 1
printf '%s\n' "$output"
}
# check mode of index entry ($1) against arguments $2...$n
# return true if it matches one of them
check_mode()
{
local i mode=$(echo "$1" | sed 's/ .*//')
shift
for i do
[ "$mode" = "$i" ] && return 0
done
return 1
}
# make sure first entry exists
entry="$(getindex "$1")" || die "fatal: cannot find $1"
# make sure second entry does not
getindex "$2" >/dev/null && die "fatal: $2 already in index"
# make sure the mode is 100644 or 100755, it's not clear
# whether this works for anything else and it's clearly
# a bad idea to shuffle a gitlink this way.
check_mode "$entry" 100644 100755 || die "fatal: $1 is not a regular file"
# use git update-index to change the name. Replace the first
# copy's mode with 0, and the second copy's name with the new name.
# XXX we can't use / as the delimiter in the 2nd sed; use $'\1' as
# an unlikely character
CTLA=$'\1'
printf '%s\n%s\n' "$entry" "$entry" |
sed -e "1s/100[67][45][45]/000000/" -e "2s$CTLA$TAB.*$CTLA$TAB$2$CTLA" |
git update-index --index-info
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With