重命名后有选择地暂存文件修改

Oli*_*Ash 5 git

通常在使用Git时,我会重命名文件然后修改它:

# 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
Run Code Online (Sandbox Code Playgroud)

之后,我想:

  • 暂存文件的重命名
  • 选择性地修改重命名的文件

但是,git add --patch不提供此选项.它只会提示用户暂存foo(旧文件名),并添加bar(新文件名).

是否有一个命令我只能用于重命名,所以我可以git add --patch单独使用阶段修改?


注意:我理解git mv在这里提供一些帮助,因为它重命名文件并立即分阶段删除/添加,因此未来的交互式git adds将只包括修改差异.但是,这并不总是实用的 - 有时重命名发生在我的控制之外,例如在使用IDE时.

tor*_*rek 5

是否有一个命令我只能用于暂存重命名,以便我可以git add --interactive单独使用暂存修改?

为此,没有很好的面向用户的命令,Git 称之为瓷器命令。(Mercurial 有一个hg mv --after————游说 中的一个--after选项git mv,给你那个选项,这不是不合理的。)不过,你可以使用一个管道命令;事实上,你可以git mv-after使用它来实现你自己的,我已经这样做了。

背景

首先,我们应该提到 Git 的index。与任何面向提交的版本控制系统一样,Git既有当前提交(Git 调用)HEAD,也有工作树,在那里你可以将文件以普通的、非版本控制的形式存放,这样你所有的正常非-version-control 软件可以使用它们。但是 Git 引入了一个中间步骤,称为indexstaging area。索引的简短描述是它是您构建下一次提交的地方

在重命名文件时,这里有几个相互交织的问题。首先是 Git 实际上根本不跟踪重命名。相反,它重建(即猜测-AT)重命名你要求一个diff的时间,其中包括git showgit log -p甚至git status命令。这意味着您需要做的是告诉 Git删除旧路径名的现有索引条目,并为新路径名添加索引条目。

其次,虽然有一个瓷器命令可以在不接触工作树的情况下删除索引条目,但添加索引条目的瓷器命令与更新现有索引条目的瓷器命令相同。具体来说:

git rm --cached path/to/file.ext
Run Code Online (Sandbox Code Playgroud)

删除索引条目而不触及工作树,因此可以删除不再具有相应工作树文件的索引条目。但:

git add path/to/newname.ext
Run Code Online (Sandbox Code Playgroud)

不仅为新文件创建索引条目,它还通过文件的当前内容复制到索引中来实现。(这有点误导,我们稍后会看到,但这就是问题所在。)因此,如果某个 GUI 或 IDE 或其他非 Git 程序对文件进行了重命名修改,并且您同时使用了两个 Git 命令,这可以很好地删除旧索引条目,但它会以新名称写入文件的数据,而不是从旧索引条目复制旧数据。

如果我们有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
Run Code Online (Sandbox Code Playgroud)

告诉 Git“获取索引条目name.ext并开始调用它newname.ext”。但我们没有,这失败了:

$ git mv name.ext newname.ext
fatal: bad source, source=name.ext, destination=newname.ext
Run Code Online (Sandbox Code Playgroud)

有一个简单但笨拙的解决方法:

  1. 从索引中以旧名称提取旧文件作为旧版本。
  2. 将新文件移开。
  3. 使用git mv更新索引。
  4. 将新文件移回原位。

因此:

$ git checkout -- name.ext && \
  mv newname.ext temp-save-it && \
  git mv name.ext newname.ext && \
  mv temp-save-it newname.ext
Run Code Online (Sandbox Code Playgroud)

有诀窍,但我们必须发明一个临时名称 ( temp-save-it) 并保证它是唯一的。

实施 git mv-after

如果我们运行git ls-files --stage,我们会确切地看到索引中的内容

$ git ls-files --stage
100644 038d718da6a1ebbc6a7780a96ed75a70cc2ad6e2 0   README
100644 77df059b7ea5adaf8c7e238fe2a9ce8b18b9a6a6 0   name.ext
Run Code Online (Sandbox Code Playgroud)

索引存储的实际上不是文件的内容,而是存储库中文件的一个特定版本的哈希 ID。(此外,阶段编号0和路径名之间是文字 ASCII TAB 字符,字符代码 9;这很重要。)

我们需要做的就是在新名称下添加一个具有相同模式和哈希 ID(以及阶段编号 0)的新索引条目,同时删除旧索引条目。有一个管道命令可以做到这一点,git update-index。使用--index-info,命令读取其标准输入,其格式应与git ls-files --stage写入它的方式完全相同。

执行此操作的脚本有点长,所以我现在将它放在下面 我的“已发布脚本”存储库中。但它正在行动:

$ git mv-after name.ext newname.ext
$ git status --short
RM name.ext -> newname.ext
Run Code Online (Sandbox Code Playgroud)

脚本可能需要做更多的工作——例如,文件名中的 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
Run Code Online (Sandbox Code Playgroud)