如何在git中进行“仅本地提交”?

54
我正在使用 git,并希望能够创建一个与远程仓库不同步的提交。这样的提交必须“浮动”在本地仓库中所有其他提交之上,以避免影响历史记录。我可以使用这样的提交来存储特定于本地的更改(配置更改、调试标志、本地解决方法等)。
目前,我手动变基以重新排序提交并使用“HEAD^”推送以避免推送本地更改。我还考虑将更改放在存储区,但这不太方便,因为它排除了存储区的正常使用。另一种选择是简单地将所有这些本地更改保持未暂存状态,并在每次想要提交时使用“git add -p”。然而,对于大量微不足道的本地更改,这变得很麻烦。
以下是我的当前工作流程示例:
我的代码库最初如下:
A---B---C---F master

这里的"F"是我的浮动提交。

我进行了一次提交:

A---B---C---F---D master

然后 git rebase -i HEAD~2 命令以重新排序:

A---B---C---D---F master

然后git push remote HEAD~1...来推送除本地提交F以外的所有内容。

更改 F 包含对现有版本文件的更改,并且可能包含任意数量的更改。(如果我可以使多个提交“浮动”,那将更好,因为我可以隔离我的本地更改)。


创建一个辅助脚本(在.bashrc中的函数)对您来说可接受吗?我有一些想法... - André Puel
是的,这是可以接受的。 - nneonneo
9个回答

22

你可以将这些更改放入一个本地分支中,然后定期从主要开发分支进行变基/合并操作。这样就不会有将它们提交到上游的危险了。


我喜欢这个想法,因为它更安全(+1),但似乎与当前的变基和重新排序策略具有相同数量的维护开销。 - nneonneo
我不确定,但我认为这会带来问题,因为它不是完整的文件,而只是其中的一部分。如果有人更改了那段代码,而你在此期间没有进行更新,Git 将采用最新版本。因此,当你合并时,你的代码将会消失。 - fonZ
1
如果有人在那些确切的地方进行更改,git 将标记冲突,您将不得不解决它 - 这正是您想要的,我猜。 - koljaTM
1
使用以下命令创建一些脚本: • 切换到主分支,提交更改,变基设置,切换回设置分支 • 推送主分支 • 拉取主分支,变基设置 - Chronial
OP会在哪个分支上工作?假设我需要本地提交才能使事情正常运行。因此,我在本地分支上工作,在那里提交东西。测试完所有内容后,一切似乎都很好,所以我想将这些非本地的新提交推送到上游。怎么做呢? - User 10482

19

看起来你想要两件事:

  • 有些提交应该保持私有(例如在本地分支上),永远不要推送或合并,它们应该留在共享提交之后。

  • 这应该对你来说大部分是透明的;你想要在主分支上工作,并自动维护本地分支。你只需要决定哪些提交是本地的,与远程存储库交互的命令将忽略这些提交。

因此,你想编写一个脚本(将其命名为git-something并将其放在你的路径上,以便它成为额外的git命令)来识别和处理这些提交。你需要一些触发器让脚本识别本地提交。简单的方法是在提交说明中放置一个魔法词--你永远不会在真实/共享的提交中使用--让脚本识别。(如果这听起来太不可靠了,你也可以在提交的树中使用一个特殊文件,比如.THIS_COMMIT_IS_LOCAL_ONLY;我没有在示例中使用它,因为它有点难。)

你需要一个命令从当前索引/工作目录创建本地提交;这很容易,它只是调用git commit $@ -m "__LOCAL_COMMIT_ONLY__"(这是一个示例;重点是它会做一些标记正在创建的提交为本地专用,然后转到git commit)。你还需要一个命令来临时弹出所有本地提交,执行一些其他的git命令(pull、push、fetch、merge等),然后重新应用本地提交。你也将使用这个命令来创建你打算共享的本地提交,以便它们始终出现在你的历史记录中的本地专用提交之下。

下面是一个同时提供这两种功能的脚本示例:

#!/bin/sh
if [[ $1 eq 'new' ]]; then
  shift
  exec git commit $@ -m "__LOCAL_COMMIT_ONLY__"
elif [[ $1 eq

OLD_HEAD=$(git rev-parse HEAD)
OLD_REAL_HEAD="$(git rev-list HEAD --grep=__LOCAL_COMMIT_ONLY__ | tail -n1)^"
git reset --soft $OLD_REAL_HEAD
git $@
git rebase --onto HEAD $OLD_REAL_HEAD $OLD_HEAD

假设您将脚本命名为git-local,则使用git local new从索引创建新的本地提交(或者使用git local new -a从工作目录中修改的文件创建),使用git local commit(遗憾的是名称不太完美)创建一个新的“真实”提交,git local push进行推送,git local pull进行拉取等操作。
主要缺点是需要您记住大多数命令现在都要加前缀local。如果您忘记这样做一次,可能会稍微有些麻烦,但不至于太糟糕——一个快速的git rebase -i就可以让您轻松地将本地提交移回顶部,您又可以继续运行了。最大的风险是您意外使用git push而不是git local push并将所有私有更改发送到上游,这会惹怒所有人。为此,您可能需要编写一个小型封装脚本来代替git本身(将其称为~/bin/git并确保~/bin在您的路径中)。
#!/bin/sh
if [[ $1 = 'push' ]]; then
  if /usr/bin/git rev-list HEAD --grep=__LOCAL_COMMIT_ONLY__ | grep -q .; then
    echo "Can't push with local changes still active!"
    echo "Try using `git local push' instead."
    exit 1
  fi
fi
exec /usr/bin/git "$@"

您还可以在服务器上创建一个 pre-receive 钩子,自动拒绝任何提交消息中包含 __LOCAL_COMMIT_ONLY__ 的提交。

15

你可以使用补丁文件,这可以从Git生成。

# git diff > local.patch

当你想提交时,反向打补丁:

# git apply -R local.patch

提交后,恢复补丁:

# git apply local.patch

在创建补丁文件之前,您只需要确保本地更改已经存在于工作目录中。创建补丁文件后,您可以将其添加到.gitignore中,以便不会将其提交。


2
这太完美了,这种功能通常只对少量提交或几个更改才有必要,只需应用/撤消补丁... 太棒了! - niken
2
这绝对是我见过的最简单的解决方案。而且如此干净! - RoboticRenaissance

12

在之前的工作中,每个人都有自己的本地 settings 分支,我们将个人设置提交到该分支。这个分支是基于 master 的;任何主题分支都是从 settings 派生出来的。当主题准备好集成时,我们将它们变基到 master 上。这似乎是 @koljaTM 建议的做法。

A---B---C  master
         \
          F  settings
           \
            D  topic

rebase --onto master settings topic

A---B---C  master
        |\
        | F  settings
         \
          D  topic

当最新更改应用到master时,我们将会基于master重新设置settings,然后将我们正在处理的任何主题基于settings进行重新设置。

诚然,这并不是一个一步到位的“永远悬浮”的解决方案,但它足够简洁和清晰。


这正是我所需要的,但过于复杂了。感谢您的答案。 - kodu

2
假设你有一个只包含本地特定更改的文件,没有其他需要提交的更改,那么你可以通过 git update-index 命令让 git 忽略该文件中的更改。 具体来说,你需要使用 --assume-unchanged 或者 --skip-worktree 参数;虽然它们基本上做的是相同的事情(将文件标记为在 git 索引中未更改),但它们具有一些非常微妙的特点,这些特点在这里有很好的描述。

1

如果您想要防止实际提交继续前进,那么您正在正确的轨道上 - 使用

git rebase -i

并重新排序所有“个人提交”以成为最新的提交,然后

git push <remotename> <latest commit SHA you want to push>:<remotebranchname>

这将推送包括您提供的SHA在内的所有提交,但不会推送其后的“个人提交”。


这正是我现在正在做的,使用 git rebase -i HEAD~2 来重新排序最后两个提交,然后使用 git push remote HEAD~1... 推送除了最后一个提交以外的所有内容。 - nneonneo
缺点:如果您使用监视文件系统的IDE,则必须退出,执行git操作并重新启动。对于大型项目,这可能需要几分钟的时间成本。 - Aaron Digulla

0

你可以在git中忽略某些文件,以便它们不会被推送到远程仓库。我通常的做法是将这些文件保留在本地,并通过忽略它们来避免将它们推送到仓库。

以下是gitignore的一些解释。

https://help.github.com/articles/ignoring-files


1
我本来会做那个的,但很多更改都涉及到我正在处理的现有版本文件(例如,在头文件中更改日志级别、为我的本地配置添加解决方法或添加半永久调试打印)。 - nneonneo
我明白了,你想要本地调试输出而不是远程的,是吗? - fonZ
我正在处理一个大系统的一个组件,其他开发人员(在其他组件上)看到我的调试信息会很不方便。我也想在本地保留它们,因为它们对我来说一直很有用,但对其他人可能没有用处。(任何对他人有用的内容都应该被正确提交)。 - nneonneo
嗯,我明白你的意思。你可以编写一个脚本来解析文件,删除调试打印信息,本地提交后再恢复打印信息。但是对于Git,我不知道该怎么做。 - fonZ

0

我经常遇到这个问题,通常是将IDE设置等从共享代码库中分离出来。这是一个足够常见的用例,你可能认为它早已被解决了,但实际上并没有。我不认为git可以按照你想要的方式处理这个问题。

问题的本质是,你希望两个检出(checkout)共享一个目录。为了使其工作,需要有一些数据结构,可由用户操作,标记哪些文件属于哪个检出。而且这不是一对一的关系;某个文件属于多个检出是完全合理的。在所有情况下,您都需要一种命名不同检出以支持消除歧义的方法。为了基本的健全性,您需要一个目录列表器,用其检查文件与其检出关联。

特别说明一下,考虑.gitignore。那是一个适用于“所有”检出的单个文件,隐含地意味着“唯一的检出”。为了使.gitignore正常工作,您需要每个检出一个文件,并找到区分每个检出中不同文件的方法。然而,在git中没有任何这种基础设施的迹象。

我解决这类问题的“标准”方式是将每个目录设置为一个checkout。我创建一个名为“custom”(或类似名称)的目录,并将该目录放入父目录的.gitignore中。在custom目录中,我从不同的存储库和/或不同的位置进行checkout。这意味着需要两个提交来捕获所有更改;在实践中,这很少是一个问题。

如果这激发了任何人对git进行hack,可以添加选项以创建“命名checkout”,并将现有行为保留为匿名checkout。您可以拥有[0..1]个匿名checkout和[0..*)个命名checkout。基于此,您可能想要的其他大部分内容都会自然而然地出现。


1
现在大多数IDE都使用唯一的扩展名(例如.xcuserdata、.suo等)存储其用户特定(本地)设置,因此很容易将这些文件添加到.gitignore中。在您的情况下,似乎“custom”也可以成为一个子模块。 - nneonneo

0

我知道这个问题已经有答案了,但是我遇到了类似的问题,最终配置了我的~/.gitconfig添加了三个有用的命令。 如果其他解决方案不行,这可能对某些人有所帮助。

[alias]
    local-export = "![ ${#} -eq 1 ] && CURRENT_BRANCH=$(git branch --show-current) && git checkout -b \"local/${1}\" && git commit --allow-empty -m ${1} && git checkout \"${CURRENT_BRANCH}\" || echo 'You must provide <local_branch_name>' #"
    local-import = "![ ${#} -eq 1 ] && CURRENT_BRANCH=$(git branch --show-current) && git merge \"local/${1}\" --no-ff --commit --no-edit && git reset HEAD^ || echo 'You must provide <local_branch_name>' #"
    local-delete = "![ ${#} -eq 1 ] && git branch -D \"local/${1}\" || echo 'You must provide <local_branch_name>' #"

这些命令可以实现以下功能:

  • git local-export <name> 将创建一个包含当前暂存内容的单一提交的分支。该分支将被命名为“local/<name>”,提交信息将为“<name>”。
  • git local-import <name> 将使用单个提交(无快进,无提示,全部自动)将“local/<name>”合并到当前分支中,并重置为初始状态。这样可以将本地分支修改导入工作目录,而不对当前分支历史记录进行任何操作。
  • git local-delete <name> 将强制删除“local/<name>”分支,以便清理本地分支。

希望这对某人有用。

谢谢。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接