是否有一个命令可以用来只暂存重命名,然后我可以使用git add --interactive 单独暂存修改?
没有很好的面向用户的命令,Git 称之为 porcelain 命令,为此。 (Mercurial 有一个——hg mv --after——在git mv 中游说一个--after 选项来给你这个选项并不是不合理的。)你可以使用一个 plumbing 命令,尽管;事实上,你可以使用这个实现自己的git mv-after,我已经这样做了。
背景
首先,我们应该提到 Git 的 index。与任何面向提交的版本控制系统一样,Git 有一个 当前提交,Git 称之为 HEAD,还有一个 work-tree,这是你拥有你的以普通的、非版本控制的形式文件,以便所有普通的非版本控制软件都可以使用它们。但是 Git 引入了一个中间步骤,称为 index 或 staging area。索引的简短描述是它是构建下一次提交的地方。
在重命名文件时,这里有几个相互交织的问题。首先是 Git 实际上根本不跟踪重命名。相反,它重构(即猜测)在您请求差异时重命名,包括git show、git log -p,甚至git status 命令.这意味着您需要做的是告诉 Git 删除旧路径名的现有索引条目,并为新路径名添加 new 索引条目。
其次,虽然有一个瓷器命令删除一个索引条目而不接触工作树,但添加一个索引条目的瓷器命令是相同的 作为瓷器命令来更新现有的 索引条目。具体来说:
git rm --cached path/to/file.ext
删除索引条目而不触及工作树,因此可以删除不再具有相应工作树文件的索引条目。但是:
git add path/to/newname.ext
不仅创建新文件的索引条目,它还通过将文件的当前内容复制到索引中来实现。 (这有点误导,我们稍后会看到,但它是的问题。)所以如果文件被重命名和被某些GUI或IDE修改或其他非 Git 程序,并且您同时使用两个 Git 命令,这会很好地删除旧的索引条目,但它会以新名称写入文件的 new 数据,而不是复制旧数据来自旧的索引条目。
如果我们只有git mv --after,我们可能会这样使用它:
$ git status
$ program-that-renames-file-and-modifies-it
$ git status --short
D name.ext
?? newname.ext
$ git mv --after name.ext newname.ext
告诉 Git“获取name.ext 的索引条目并开始调用它newname.ext”。但我们没有,这失败了:
$ git mv name.ext newname.ext
fatal: bad source, source=name.ext, destination=newname.ext
有一个简单但笨拙的解决方法:
- 从索引中提取旧文件,以其旧名称作为旧版本。
- 将新文件移开。
- 使用
git mv 更新索引。
- 将新文件移回原位。
因此:
$ git checkout -- name.ext && \
mv newname.ext temp-save-it && \
git mv name.ext newname.ext && \
mv temp-save-it newname.ext
成功了,但我们必须发明一个临时名称 (temp-save-it) 并保证它是唯一的。
实现git mv-after
如果我们运行git ls-files --stage,我们会看到索引中的确切内容:
$ git ls-files --stage
100644 038d718da6a1ebbc6a7780a96ed75a70cc2ad6e2 0 README
100644 77df059b7ea5adaf8c7e238fe2a9ce8b18b9a6a6 0 name.ext
索引存储的实际上并不是文件的内容,而是存储库中某个特定版本文件的hash ID。 (另外,在阶段号 0 和路径名之间是文字 ASCII TAB 字符,字符代码 9;这很重要。)
我们需要做的就是在新名称下添加一个具有相同模式和哈希 ID(以及阶段编号 0)的新索引条目,同时删除旧的索引条目。有一个管道命令可以做到这一点,git update-index。使用--index-info,该命令读取其标准输入,其格式应与git ls-files --stage 写入它的方式完全相同。
执行此操作的脚本有点长,所以我把它放在下面和in my "published scripts" repository now。但它正在发挥作用:
$ git mv-after name.ext newname.ext
$ git status --short
RM name.ext -> newname.ext
脚本可能需要做更多的工作——例如,文件名中的 control-A 会混淆最后的 sed——但它确实有效。将脚本放置在路径中的某个位置(在我的情况下,它位于我的~/scripts/ 目录中),将其命名为git-mv-after,并将其调用为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