首页 > 解决方案 > 跨所有分支的相同全局远程 gitignore 文件

问题描述

我想要实现的是拥有一个 .gitignore 文件(由 git 跟踪),该文件在远程存储库(托管在 GitHub 上)中的所有分支之间同步,因此也在相应的本地分支上。我目前使用的 .gitignore 文件并不完美,所以偶尔(有时每天多次)我必须更新它。那么问题是我必须checkout手动跨所有分支使用 .gitignore 文件,随着创建更多分支,这变得越来越痛苦。因此,对于我所做的每个分支(在分支主控中使用更新的 .gitignore)

git checkout some-outdated-branch
git checkout master .gitignore
git add .gitignore
git rm -r --cached .
git add .
git commit -m "Updated .gitignore and fixed tracked files"

由于这对于多个分支来说相对耗时,我试图寻找一种方法让一个 .gitignore 文件在分支主控(或单独的 gitignore-branch 分支)中自动同步所有分支(本地,以及远程推时)。
这里的问题是我不想使用git config --global core.excludesfile /path/to/local/.gitignore (如此处所建议),因为我希望我的项目合作伙伴也使用该特定的 .gitignore 文件,而不必为此更改git config文件。在此评论中,其他人正在问这个问题,但尚未得到回答。我也无法在 Stack Overflow 上找到关于我的问题的任何答案。

简短摘要
我只想在一个分支上编辑 .gitignore 文件,并以省时省力的方式将该更改与所有其他分支(自动)同步。之后,我想将所有分支中的更改推送到远程存储库(最好只使用一行或几行代码,而不必为每个分支重新提交带有相应提交消息的提交)。

标签: gitgithubgitignore

解决方案


不幸的是,只要.gitignore(或实际上任何文件)被跟踪(意味着在 index 中),该文件的逻辑上独立的副本就会进入您所做的每个提交。这样做的结果是不可能实现你想要的。

正如phd 所提到的,最接近的方法是在每个新提交中存储一个.gitignore符号链接类型的条目(120000Git-internal-ese 中的模式)。然后,即使每个提交都有链接目标路径名的逻辑分离(可能是物理共享)副本,当 Git 去读取它的内容时,.gitignore它也会读取目标路径名的内容,而不是.gitignore工作树文件那只是从您告诉退出的任何提交中复制出来的git checkout

但是,您可以自动化.gitignore跨多个提交更新文件的过程。最简单的方法可能是使用git worktree add创建一个单独的工作树来进行更新。这假设您的 Git 版本至少为 2.5,最好至少为 2.15(以避免git worktree.

以下是一个完全未经测试的脚本,对于每个远程跟踪分支,将使用添加的工作树确保该远程跟踪分支的提示提交包含.gitignore与主存储库中当前分支中的那个匹配的. 它使用分离的 HEAD 模式来实现这一点(并且在适当的时候一次推送多个提交)。它不能正确处理具有单个 URL 的多个远程名称;为此,请删除git fetch --all并取消注释new_remote.

#! /bin/sh
#
# git-update-ignores-across-remote-tracking-branches

. git-sh-setup     # get script goodies, and make sure we're at top level

require_work_tree  # make sure we have a work-tree, too

# Where is our ignore file? (absolute path)
IFILE=$(readlink -f .gitignore) || die "cannot find .gitignore file"

# set up a temporary file; remove it on exit
TF=$(mktemp) || die "cannot create temporary file"
trap "rm -f $TF" 0 1 2 3 15

# Use a work-tree in ../update-ignores
if [ ! -d ../update-ignores ]; then
    [ -e ../update-ignores ] &&
        die "../update-ignores exists but is not a directory"
    git worktree add ../update-ignores --detach ||
        die "unable to create ../update-ignores"
else
    # Should use git worktree list --porcelain to verify that
    # ../update-ignores is an added, detached work-tree, but
    # I leave that to someone else.  It might also be good to
    # leave remote-tracking names for other added work-trees
    # alone, but again, that's for someone else to write.
fi

# Find upstream of current branch, if we're on a branch and there is
# an upstream - we won't attempt to do anything to that one, so as to
# avoid creating headaches for the main work-tree.  Note that this
# sets UPSTREAM="" if the rev-parse fails.
UPSTREAM=$(git rev-parse --symbolic-full-name HEAD@{u} 2>/dev/null)

# Now attempt to update remote-tracking names.  Update all remotes
# first so that we are in sync, then list all names into temporary file.
# From here on, we'll work in the update-ignores work-tree.
cd ../update-ignores
require_clean_work_tree "update ignores"
git fetch --all || die "unable to fetch --all"
git for-each-ref --format='%(refname)' refs/remotes > $TF
REMOTE=
UPDATED=

# Function: push UPDATED to REMOTE.  Set REMOTE to $1 and clear UPDATED.
# Does nothing if UPDATED or REMOTE are empty, so safe to use an extra time.
new_remote() {
    local u="$UPDATED" r="$REMOTE"
    if [ "$u" != "" -a "$r" != "" ]; then
        git push $r $u || die "failed to push!"
    fi
    UPDATED=
    REMOTE=$1
    # [ -z "$REMOTE" ] || git fetch $REMOTE || die "unable to fetch from $REMOTE"
}

while read name; do
    # skip the upstream of the main repo
    [ $name == "$UPSTREAM" ] && continue
    # Update this branch's .gitignore, and remember to push this commit.
    # If we're switching remotes, clean out what we've done so far.
    shortname=${name##refs/remotes/}  # e.g., origin/master or r/feature/X
    remote=${shortname%%/*}           # e.g., origin or r
    branch=${shortname#remote/}       # e.g., master or feature/X

    # if we're changing remotes, clear out the old one
    [ $remote != $REMOTE ] && new_remote $remote

    # switch detached HEAD to commit corresponding to remote-tracking name
    git checkout -q $name || die "unable to check out $name"
    # update .gitignore (but skip all this if it's correct)
    cmp -s .gitignore $IFILE 2>/dev/null && continue
    cp $IFILE .gitignore || die "unable to copy $IFILE to .gitignore"
    git add .gitignore || die "unable to add .gitignore"
    # UGH: terrible commit message below, please fix
    git commit -q -m "update .gitignore" || die "unable to commit"
    commit=$(git rev-parse HEAD) || die "failed to rev-parse HEAD"
    # remember to push this commit (by hash ID) to refs/heads/$shortname
    # on $REMOTE (which is correct because of new_remote above)
    UPDATED="$UPDATED $commit:refs/heads/$shortname"
done < $TF
# push any accumulated commits, or do nothing if none accumulated
new_remote

# and we're done!
exit 0

推荐阅读