我可以在不修改工作副本的情况下对 Git 分支进行变基吗?

发布于 2024-10-16 03:02:50 字数 932 浏览 6 评论 0 原文

假设我检查了我的“主”分支。我已经对“master”进行了一些生产更改,现在我想将我的“实验”分支重新设置到最新的 master 上。但是,我想在不修改工作副本中的任何文件的情况下执行此操作。本质上,我希望所有的魔法都发生在 .git 目录中,而不触及工作副本。

如果不是为了“不要修改我的工作副本”要求,这只是一个做的问题:

# current branch is master
git checkout experimental
git rebase master
git checkout master

我真正的问题是这会修改我的工作副本中的时间戳,即使我最后检查完全相同的内容我开始于。一旦我运行“git checkoutexperimental”,任何在experimental分支中包含更改的文件都会将其mtime设置为当前时间——自上次我重新基于experimental以来在master中更改的任何文件也会如此。因为时间已经改变,像构建工具这样的东西会认为他们需要再次做一些工作,尽管当我完成时,文件的内容实际上并没有改变。 (就我而言,如果项目文件的时间戳发生更改,Visual Studio 认为它需要花费大量时间卸载和重新加载项目。)我想避免这种情况。

有没有一种方法可以一步完成上述所有操作,而无需修改工作副本中的任何内容 (假设变基期间没有冲突)< /em>?

(如果存在冲突,我的偏好是显示错误,然后中止整个操作,而不修改任何时间戳。但这只是我的偏好,不是一个硬性要求——我不知道什么都是可能的。)

当然,我可以编写一个脚本来捕获 mtime,运行 git,然后重置 mtime;但 Git 似乎已经有一种方法可以在不影响工作副本的情况下完成诸如 rebase 之类的事情,因为 rebase 实际上是关于增量,而不是文件的实际内容。

Suppose I have my "master" branch checked out. I've committed some production changes to "master", and now I want to rebase my "experimental" branch onto the latest master. But, I want to do this without modifying any files in my working copy. Essentially, I want all the magic to happen inside the .git directory, without touching the working copy.

If not for the "don't modify my working copy" requirement, this would just be a matter of doing:

# current branch is master
git checkout experimental
git rebase master
git checkout master

My real problem is that this modifies timestamps in my working copy, even though I'm ending by checking out the exact same content I started with. As soon as I run "git checkout experimental", any files that contain changes in the experimental branch will get their mtime set to the current time -- and so will any files that were changed in master since the last time I rebased experimental. Because the mtimes have changed, things like build tools get the idea that there's work they need to do again, even though, by the time I'm done, the files' contents haven't actually changed. (In my case, it's that if a project file's timestamp changes, Visual Studio thinks it needs to spend a lot of time unloading and reloading the project.) I want to avoid that.

Is there a way to do all of the above in one step, without ever modifying anything in the working copy (assuming there are no conflicts during the rebase)?

(If there are conflicts, my preference would be to show the error and then abort the entire operation, without ever modifying any timestamps. But that's just my preference, not a hard requirement -- I don't know what all is possible.)

Of course I can write a script to capture the mtimes, run git, and then reset the mtimes; but it seems likely that Git would already have a way to do things like rebase without bothering the working copy, since the rebase is really about the deltas, not the files' actual contents.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

似梦非梦 2024-10-23 03:02:50

git 2.5 开始,更好的解决方案是使用第二个工作树

git 存储库可以支持多个工作树,允许您一次签出多个分支。

$ git worktree add ../second-copy experimental
$ cd ../second-copy/
$ git rebase master experimental

就是这样。之后,如果需要,您可以rm -rf secondary-copy,或者保留它以供将来进行更多变基。

$ git rebase master experimental

Since git 2.5, an even better solution is to use a second worktree.

A git repository can support multiple working trees, allowing you to check out more than one branch at a time.

$ git worktree add ../second-copy experimental
$ cd ../second-copy/
$ git rebase master experimental

And that's it. Afterwards, you can rm -rf second-copy if you want, or keep it for more rebases in the future.

$ git rebase master experimental
快乐很简单 2024-10-23 03:02:50

有没有一种方法可以一步完成上述所有操作,而无需修改工作副本中的任何内容?

不幸的是,这是不可能的(如果不创建工作副本的可修改副本 - 另请参阅 Petr 的答案),因为 git 执行所有合并-y 工作树上的操作(真正的合并、精选、变基、补丁应用)。之前多次提到过这一点,例如知识渊博的 Jakub Narębski 的回答之一

如果不接触工作目录(和索引),合并(或变基)就无法工作,因为可能存在必须使用工作目录(和/或索引)解决的合并冲突。

是的,这是一个设计决策,但这是一个非常可以理解的决策 - 构建尝试在内存中合并所需的所有结构,然后一旦发生冲突,将所有内容转储到内存中,这会是一件苦差事。工作树,而您首先可以简单地在工作树中完成它。 (我不是 git 开发人员;不要将此视为绝对完整的事实。可能还有其他原因。)

我的建议是,不要编写脚本来执行所有 mtime 操作,而是简单地克隆存储库,执行克隆中的变基,然后将其推回到原始存储库中:

git clone project project-for-rebase
cd project-for-rebase
git branch experimental origin/experimental
git rebase master experimental
git push origin experimental

当然,这是假设您的原始存储库中没有签出实验性的。如果是,您可以执行类似 git fetch ../project-for-rebaseexperimental; 的操作,而不是推送。 git reset --hard FETCH_HEAD 或更具可读性,git remote add for-rebase ../project-for-rebase; git fetch for-rebase; git reset --hard for-rebase/experimental。这自然会触及原始实验分支和重新基础实验分支之间不同的任何文件,但这绝对是正确的行为。 (当然,这不是您给出的示例,但我希望这些说明是通用的!)

Is there a way to do all of the above in one step, without ever modifying anything in the working copy?

This is unfortunately impossible (without creating a modifiable copy of the working copy - see also Petr's answer), because git performs all merge-y operations (real merges, cherry-picks, rebases, patch application) on the work tree. This is mentioned several times before, for example in one of the knowledgeable Jakub Narębski's answers:

There is no way that merge (or rebase) can work without touching the working directory (and index), as there can be merge conflicts that have to be resolved using working directory (and/or index).

Yes, it's a design decision, but it's a pretty understandable one - it'd be a bit of a chore to build up all the structure necessary to attempt a merge in memory, then as soon as it hits a conflict, dump everything into the work tree, when instead you could simply do it in the work tree in the first place. (I'm not a git developer; don't take this as absolute complete truth. There could be other reasons.)

My suggestion, rather than writing a script to do all that mtime manipulation, would be simply to clone the repository, perform the rebase in the clone, then push it back into your original repository:

git clone project project-for-rebase
cd project-for-rebase
git branch experimental origin/experimental
git rebase master experimental
git push origin experimental

That of course assumes that experimental isn't checked out in your original repo. If it is, instead of the push, you'd do something like git fetch ../project-for-rebase experimental; git reset --hard FETCH_HEAD or more readable, git remote add for-rebase ../project-for-rebase; git fetch for-rebase; git reset --hard for-rebase/experimental. That will naturally touch whatever files differ between the original and rebased experimental branches, but that's definitely correct behavior. (This wasn't the example you gave, of course, but I want these instructions to be general!)

笑脸一如从前 2024-10-23 03:02:50

我创建了一个小脚本来在 Linux 上执行此操作。它基于 Jefromi 的答案和一些补充(主要是设置替代项,以便不复制对象数据库,并且仅拉取所需的分支)。你们中的一些人可能会发现它很有用:
https://github.com/encukou/bin/blob/master/oot-rebase

如果它不能完全满足您的要求,欢迎提出拉取请求:)

I've created a small script to do this on linux. It's based on Jefromi's answer and a few additions (mainly, setting up alternates so the object database isn't copied, and only pulling the needed branches). Some of you may find it useful:
https://github.com/encukou/bin/blob/master/oot-rebase

If it doesn't do quite what you like, pull requests are welcome :)

挽袖吟 2024-10-23 03:02:50

更新,自 git 2.5 起,这个答案被内置的取代
机制“worktree”基本相同。参见上面的回答:
https://stackoverflow.com/a/12481546/1499102

与创建存储库的克隆类似,我发现它是使用多个工作目录来做这样的事情要整洁得多。此外,克隆会使用大量磁盘空间,而这几乎不会使用任何磁盘空间。

https://github.com/git/git/ blob/master/contrib/workdir/git-new-workdir

您可以像这样创建一个新的工作目录:

git-new-workdir project-dir new-workdir branch

然后您可以将其视为克隆,除了原始工作目录中的获取和提交将反映在这里(尽管不在工作分支上而无需重新签出)。唯一的例外是如果您有子模块,因为默认情况下这些子模块是为每个工作目录单独完成的。坦率地说,我从未研究过这一点,因为我尝试避免使用子模块。

所以基本上只是:

cd new-workdir
git checkout experimental
git rebase master

不完全是一个命令,但非常简单。

与下面的存储方法相比,此方法(如克隆方法)的优点是,如果您当前正在工作目录中执行代码(或由某些进程使用),则该代码不会被中断。


此处未提及的另一个选项是在当前工作目录中执行此操作,但存储更改,以便您可以立即恢复工作目录状态。

# current branch is master (with changes to working state)
git stash -u
git checkout experimental
git rebase master
git checkout master
git stash pop

如果您有任何新文件,请确保使用 stash -u,否则它们将不会被隐藏。再说一次,不是一步,但非常干净和简单。

Update, since git 2.5 this answer is superseded by the in-built
mechanism "worktree" which is basically the same. See above answer:
https://stackoverflow.com/a/12481546/1499102

Similar to creating a clone of your repository, I find that it's much tidier to make use of multiple workdirs to do things like this. Also a clone will use a lot of disk space, this will use almost none.

https://github.com/git/git/blob/master/contrib/workdir/git-new-workdir

You create a new workdir like this:

git-new-workdir project-dir new-workdir branch

Then you can treat that as if it was a clone except that fetches and commits in your original workdir will be reflected here (although not on the working branch without recheckout). The only exception to this is if you have submodules as those are done separately for each workdir by default. Frankly I've never looked in to that because I try and avoid submodules.

So basically just:

cd new-workdir
git checkout experimental
git rebase master

Not exactly a single command, but pretty simple.

An advantage of this approach (like the clone approach) over the stash approach below is that if you have code currently executing (or otherwise being used by some processes) from your working directory, it isn't interrupted.


The other option which isn't mentioned here is to do it in your current working directory, but stash your changes so that you can then instantly restore your working directory state.

# current branch is master (with changes to working state)
git stash -u
git checkout experimental
git rebase master
git checkout master
git stash pop

Make sure to use stash -u if you have any new files as otherwise they will not be stashed. Again, not one step, but pretty clean and simple.

云淡月浅 2024-10-23 03:02:50

正如其他人所说,在不触及工作目录的情况下不可能对分支进行变基(即使是建议的替代方案,例如创建新的克隆或工作树也无法改变这一事实;这些替代方案确实不会触及您当前的工作目录,但只能通过创建一个新的工作树)。

对于要更新的​​分支要基于当前工作树(或其父级)重新建立基础的特殊情况,可以“重新建立”另一个分支,而无需不必要地接触文件。
如果您有一个 git 工作流程,其中您正在处理许多分支,这些分支都是从主“master”分支(定期更新到远程主分支)分支,那么这种特殊情况经常会发生。

为了说明这一点,假设一个具有以下结构的 Git 存储库:

repo
- commitA
- commitB
- commitC <-- master <-- OtherBranch based on master
  - commitD <-- First commit in otherBranch
  - commitE <-- Second commit in OtherBranch
- commitD <-- Unrelated commit in current working tree

为了举例说明,我们假设“OtherBranch”是从“master”分支出来的,并且您当前的工作树也基于“master”。
您的工作流程通常从使用远程版本更新本地主分支开始......

# Fetch commits from remote "origin" and update the master branch:

# If your current branch is identical to master
git pull origin master

# If your current branch has extra commits on top of master
git pull --rebase origin master

# If you don't want to touch your current branch
git fetch origin master:master

然后您摆弄当前分支并进行一些耗时的编译。最终,您决定在 OtherBranch 上工作。此 OtherBranch 应该基于 master 重新构建(最好使用最少的文件系统操作)。以下部分将展示如何操作。

变基其他分支(参考示例 - 不要这样做)

以下解决方案是 git 方法:

git checkout OtherBranch
git rebase master    # or git rebase origin/master

这样做的缺点是第一个命令更改了当前工作树的日期,即使文件将通过以下方式恢复第二个命令。

以最少的更改重新确定其他分支的基础

为了最大限度地减少触及的文件数量,您需要签出到新的基础分支,然后使用 OtherBranch 中应用所有额外提交“https://stackoverflow.com/questions/1670970/how-to-cherry-pick-multiple-commits/3933416#3933416">gitcherry-pick

在执行任何操作之前,您需要识别 OtherBranch 中的提交。

  • git log OtherBranch 显示 OtherBranch 上的提交(如果您尚未更改 OtherBranch,则主要有用)
  • git reflog 显示对您的分支的更改本地存储库(如果您已经更新了分支并犯了错误,则很有用)。

在当前示例中,您将发现 OtherBranch 上的最后一次提交是 commitE。您可以使用 git log commitE 来查看之前的提交列表(或者如果您想要更短的列表,则使用 git log --oneline commitE )。如果您查看列表,您将看到基本提交是 commitC

现在您知道基本提交是 commitC 并且最后一次提交是 commitE,您可以按如下方式重新设置 OtherBranch 的基数(从以前的“master”到新的“master”) :

# Replace the old OtherBranch with "master" and switch to it.
git checkout -B OtherBranch master

# Cherry-pick commits starting from commitC and ending at commitE.
cherry-pick commitC^..commitE

或者(如果您想在替换 OtherBranch 之前成功完成“变基”):

# Create new branch NewOtherBranch based off "master" and switch to it.
git checkout -b NewOtherBranch master

# Cherry-pick commits starting from commitC and ending at commitE.
cherry-pick commitC^..commitE

# Replace the old branch with the current branch (-M = --move --force)
git branch -M OtherBranch

为什么这样做有效?

git 中的分支变基需要将当前分支切换到要更新的分支 (OtherBranch)。

使用 git rebase 工作流程,会发生以下情况:

  1. 切换到 OtherBranch (可能是从一个非常旧的基础分支分支出来的)。
  2. Rebase(内部步骤 1):保存不在上游分支中的提交。
  3. 变基(内部步骤 2):将当前分支重置为(新)基础分支。
  4. Rebase(内部步骤 3):恢复步骤 2 的提交。

步骤 1 和步骤 3 涉及许多文件,但最终许多涉及的文件实际上并未发生更改。

我的方法将步骤 1 和 3 合并为步骤 3,因此接触的文件数量最少。唯一受影响的文件是:

  • 在基本分支和当前工作树中的当前提交之间更改的文件。
  • OtherBranch 中的提交更改的文件。

As others have said, it is not possible to rebase a branch without touching the working directory (even the suggested alternatives such as creating a new clone or worktree cannot change this fact; these alternatives do indeed not touch your current working directory, but only by creating a new worktree).

For the special case where the branch that you want to update is to be rebased on the current working tree (or a parent thereof), it is possible to "rebase" the other branch without unnecessarily touching files.
This special case often happens if you are having a git workflow where you are working on many branches that are all branched from the main "master" branch (which is regularly updated to the remote master branch).

To illustrate, assume a Git repository with the following structure:

repo
- commitA
- commitB
- commitC <-- master <-- OtherBranch based on master
  - commitD <-- First commit in otherBranch
  - commitE <-- Second commit in OtherBranch
- commitD <-- Unrelated commit in current working tree

For the sake of the example, let's assume that "OtherBranch" is branched off "master", and that your current working tree is also based on "master".
Your workflow typically starts with updating your local master branch with the remote version...

# Fetch commits from remote "origin" and update the master branch:

# If your current branch is identical to master
git pull origin master

# If your current branch has extra commits on top of master
git pull --rebase origin master

# If you don't want to touch your current branch
git fetch origin master:master

... and then you fiddle with the current branch and do some time-consuming compilations. Eventually, you decide that you want to work on OtherBranch. This OtherBranch should be rebased on master (preferably with minimal filesystem operations). The following section will show how.

Rebasing other branch (reference example - do NOT do this)

The following solution is the git way to do it:

git checkout OtherBranch
git rebase master    # or git rebase origin/master

The disadvantage of that is that the first command changes the dates of the current worktree, even though the files are going to be restored by the second command.

Rebasing other branch with minimal changes

To minimize the number of touched files, you need to check out to the new base branch and then apply all extra commits in OtherBranch on top of the base branch using git cherry-pick.

Before doing anything, you need to identify the commits in OtherBranch.

  • git log OtherBranch shows the commits on OtherBranch (mainly useful if you haven't changed OtherBranch yet)
  • git reflog shows the changes to branches in your local repository (useful if you have already updated branches and made a mistake).

In the current example, you will discover that the last commit on OtherBranch is commitE. You can see a list of commits before that with git log commitE (or if you want a shorter list, git log --oneline commitE). If you look through the list, you will see that the base commit is commitC.

Now you know that the base commit is commitC and the last commit is commitE, you can rebase OtherBranch (from its previous "master" to the new "master") as follows:

# Replace the old OtherBranch with "master" and switch to it.
git checkout -B OtherBranch master

# Cherry-pick commits starting from commitC and ending at commitE.
cherry-pick commitC^..commitE

Alternatively (if you want to successfully complete the "rebase" before replacing OtherBranch):

# Create new branch NewOtherBranch based off "master" and switch to it.
git checkout -b NewOtherBranch master

# Cherry-pick commits starting from commitC and ending at commitE.
cherry-pick commitC^..commitE

# Replace the old branch with the current branch (-M = --move --force)
git branch -M OtherBranch

Why does this work?

Rebasing branches in git requires one to switch the current branch to the branch that you want to update (OtherBranch).

With the git rebase workflow, the following happens:

  1. Switch to OtherBranch (potentially branched off a very old base branch).
  2. Rebase (internal step 1): Save commits that are not in the upstream branch.
  3. Rebase (internal step 2): Reset current branch to the (new) base branch.
  4. Rebase (internal step 3): Restore commits from step 2.

Step 1 and step 3 touch many files, but ultimately many of the touched files have not actually changed.

My method combines step 1 and 3 into step 3, and as a result the number of touched files is minimal. The only files that are touched are:

  • Files that were changed between the base branch and the current commit in the current working tree.
  • Files that are changed by the commits in the OtherBranch.
猫腻 2024-10-23 03:02:50

我也很喜欢它,但这对我来说没有希望:

如果指定了,则 git rebase
将执行自动 git checkout
在做任何其他事情之前。
否则仍保持当前状态
分支。

http://git-scm.com/docs/git-rebase

I would also love it, but this leaves no hope to me:

If <branch> is specified, git rebase
will perform an automatic git checkout
before doing anything else.
Otherwise it remains on the current
branch.

http://git-scm.com/docs/git-rebase

忆伤 2024-10-23 03:02:50

Craig- P. Motlin答案建议使用工作树

现在,自从 Git 2.41(2023 年第 2 季度)确保使用一些子命令来阻止用户在链接到同一存储库的另一个工作树中使用的分支上工作时,这一点现在更加强大。

rebase:拒绝切换到分支已经在其他地方签出

签字人:Rubén Justo

b5cabb4中(“rebase:拒绝切换到已经在其他地方签出的分支”,2020-02-23,Git v2.26.0-rc0 -- 合并)我们添加一个条件来防止涉及切换到已在另一个工作树中签出的分支的变基操作。

最近修复了一个导致此功能无法按预期工作的错误。

让我们添加一个测试来观察将来是否会发生变化。

Craig- P. Motlin's answer suggests the use of worktree.

This is now more robust since Git 2.41 (Q2 2023) makes sure a few subcommands have been taught to stop users from working on a branch that is being used in another worktree linked to the same repository.

rebase: refuse to switch to a branch already checked out elsewhere

Signed-off-by: Rubén Justo

In b5cabb4 ("rebase: refuse to switch to branch already checked out elsewhere", 2020-02-23, Git v2.26.0-rc0 -- merge) we add a condition to prevent a rebase operation involving a switch to a branch that is already checked out in another worktree.

A bug has recently been fixed that caused this to not work as expected.

Let's add a test to notice if this changes in the future.

失眠症患者 2024-10-23 03:02:50

因此,您希望在签出该分支之前对该分支进行变基吗?我真的不明白其中的原因,因为如果您不签出该分支,您就无法对其进行操作。为什么要对一个你不工作的分支进行变基?进行结账,它会改变你的mtime,然后进行rebase。变基将触及已更改的文件,当然您需要重建它们。

然而,解决此问题的一个简单方法是使用其他工作树进行变基。只需将环境变量 GIT_WORK_TREE 设置为其他工作树即可。只是不要忘记让你的 HEAD 与你的工作树相匹配。

根据他所在的分支以及推送的内容,推送到非裸仓库可能是危险的。更好的解决方案是使用珍贵的工作树从存储库中获取。
示例:

`
orgbranch=$(git rev-parse HEAD)

mkdir /tmp/tmp_wd

cp -r !(.git) /tmp/tmp_wd

导出 GIT_WORK_TREE=/tmp/tmp_wd

git checkoutbranch1

git rebase master

git checkout $orgbranch

导出 GIT_WORK_TREE=

rm -rf /tmp/tmp_wd`

So you want a rebase done on for a branch before you checkout that branch? I really can't see the reason for that, since if you don't checkout that branch you can't work on it. Why do you want to rebase a branch that you don't work on? Do the checkout, it will change your mtime and then do the rebase. The rebase will touch files that are changed and of course you need to rebuild them.

However, a simple way to solve this is to use an other worktree for the rebase. Just set the enviroment variable GIT_WORK_TREE to an other worktree. Just don't forget to have your HEAD match your worktree.

Depending on which branch he is at and what's pushed, a push to a non-bare repo can be dangerous. A much better solution is to fetch from the repo with the precious worktree instead.
Example:

`
orgbranch=$(git rev-parse HEAD)

mkdir /tmp/tmp_wd

cp -r !(.git) /tmp/tmp_wd

export GIT_WORK_TREE=/tmp/tmp_wd

git checkout branch1

git rebase master

git checkout $orgbranch

export GIT_WORK_TREE=

rm -rf /tmp/tmp_wd`

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文