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

时间:2017-04-15 14:56:41

标签: 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

之后,我想:

  • 暂存文件的重命名
  • 有选择地对重命名的文件进行修改

但是,git add --patch不提供此选项。它仅提示用户删除foo(旧文件名),并添加bar(新文件名)。

是否有一个命令可用于仅重命名,因此我可以使用git add --patch分别进行修改?

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

1 个答案:

答案 0 :(得分:4)

  

是否有一个命令可用于仅重命名,因此我可以使用git add --interactive分别进行修改?

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

背景

首先,我们应该提一下Git的索引。像任何面向提交的版本控制系统一样,Git同时具有当前提交,Git调用HEAD工作树,这是你的地方将您的文件保存为普通的非版本控制形式,以便所有正常的非版本控制软件都可以使用它们。但Git引入了一个中间步骤,称为索引暂存区域。索引的简短描述是在其中构建下一个提交

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

其次,虽然有一个瓷器命令删除索引条目而不触及工作树,但添加索引条目的瓷器命令是<与更新现有索引条目的瓷器命令相同的。具体做法是:

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&#34;获取name.ext的索引条目并开始将其称为newname.ext而不是#34;。但是我们没有,这就失败了:

$ git mv name.ext newname.ext
fatal: bad source, source=name.ext, destination=newname.ext

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

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

    $ 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
    

    索引存储的内容实际上不是文件的内容,而是存储库中文件的一个特定版本的哈希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