Add / commit a file to all branches

萝らか妹 提交于 2021-02-17 05:22:09

问题


Say I am on a branch and the index is dirty. I made changes to a file x, and I have some other changes too.

Is there a way to add file x to all existing branches? Something like this:

    #!/usr/bin/env bash
    current_branch="$(git rev-parse --abbrev-ref HEAD)"
    git add .
    git commit -am "added x"
    git fetch origin
    git for-each-ref --format='%(refname)' refs/heads  | while read b ; do
       git checkout "$b" 
       git checkout "$current_branch" -- x
    done
    git checkout "$current_branch";  # finally, check out the original branch again

so basically it checks out all branches and then checks out the file x..so I need to commit on each branch b or no? why not?


回答1:


so basically [my loop] checks out all branches and then checks out the file x..so I need to commit on each branch b or no? why not?

The answer is both no and yes. You almost certainly do need some commits.

Remember, a branch name is in effect a pointer, pointing to one specific commit. One of these names has the name HEAD attached to it,1 and all the other names point to some tip commit. Let's draw a picture of these commits and names. Let's say that there are four names, and three such branch-tip commits:

  T  <-U  <-X   <--name1 (HEAD)
 /
...  <-V  <-Y   <--name2, name3
  \
   W  <-Z   <-- name4

The uppercase letters here stand in for some actual commit hash IDs.


1More precisely, at most one name has HEAD attached to it. If HEAD is detached, it points directly to some commit. In this case git rev-parse --abbrev-ref HEAD will just print HEAD. It's often wise to check for this, though if you're doing this as you work and know for sure that you are not on a detached HEAD, there's no need.


Your first steps are:

current_branch="$(git rev-parse --abbrev-ref HEAD)"
git add .
git commit -am "added x"

Your current branch is name1, which points to commit X. The first line sets current_branch to name1. Your current commit is commit X: this commit's files exist in your index and in your work-tree, due to the fact that you ran git checkout name1 at some point in the recent past and that filled in both the index and the work-tree from commit X.

You use git add . to copy all files in the current directory or any sub-directory into the index,2 so that they can be committed. This includes this new file x that you just created in your work-tree. Your index is now ready for committing.


2More precisely, this copies into the index, from the work-tree, all such files that are (a) already in the index, or (b) not ignored. Part (a) here is implied by part (b)—a file that's in the index is by definition not ignored—but it is worth emphasizing.


The third line, then, does the git commit. (It's not clear why you use git commit -a along with git add ., but -a would add files in some other directories if needed. You might equally run git add --all, assuming Git 2.0 or later, and leave out the -a.) Assuming it succeeds,3 there's now a new additional commit after X, and name1 points to this new commit, so the picture should now look like:

  T--U--X--α   <-- name1 (HEAD)
 /
...--V--Y   <-- name2, name3
  \
   W--Z   <-- name4

(I ran out of Roman letters so this is commit alpha.)


3All throughout this, we're assuming everything works, or that if a command fails, that this failure is good.


Your next command in your script seems to have no function here:

git fetch origin

This will obtain new commits that the Git at origin has that you don't, and update your origin/* remote-tracking names, but you do not use the remote-tracking names after this point, so why update them at this point?

The problematic parts occur here, in the loop:

git for-each-ref --format='%(refname)' refs/heads  | while read b ; do
   git checkout "$b" 
   git checkout "$current_branch" -- x
   # proposed: git commit -m "some message"
done

First, the %(refname) output is going to read refs/heads/name1, refs/heads/name2, and so on. git checkout will check each of these out as a detached HEAD, which is not what you want. That's easily fixed by using %(refname:short) which omits the refs/heads/ part.

The names you will get, in our hypothetical example here, are name1, name2, name3, and name4. So you will start by asking Git to extract commit α again—which goes very fast since it's already there—and then use the name name1 to extract file x into the index and work-tree. Those, too, are already there.

The proposal is to add git commit. This particular git commit would fail with an error saying that there is nothing to commit, which in this particular case is probably what you want: there is already a file x with the correct content in the tip commit of branch name1, i.e., in commit α.

The loop would then go on to git checkout name2, i.e., commit Y. This would replace your index and work-tree contents with those extracted from commit Y, and attach HEAD to the name name2. The git checkout name1 -- x line would extract file x from commit α into the index and work-tree, and the proposed git commit would make a new commit and hence cause the name name2 to move forward to point to this new commit. Note that the name name3 continues to point to commit Y. Let's draw in the new commit, which we can call β (beta):

    U--X--α   <-- name1 (HEAD)
   /
  T       β    <-- name2
 /       /
...--V--Y   <-- name3
  \
   W--Z   <-- name4

Now your loop moves on to name3, which still points to commit Y, so Git will set the index and work-tree back to the way they were a moment ago, when you had commit Y checked out via the name name2. Git will now extract file x from commit α just as before, and make another new commit.

This is where things get very interesting! The new commit has the same tree as commit β. It also has the same author and committer. It may, depending on how you construct your -m message, have the same log message as commit β as well. If Git makes this commit in the same time stamp second that it used when making commit β, the new commit is actually the existing commit β and all is well.

On the other hand, if Git takes enough time that the new commit gets a different time stamp, the new commit is different from commit β. Let's assume that this does happen, and that we get commit γ (gamma):

    U--X--α   <-- name1 (HEAD)
   /
  T       β    <-- name2
 /       /
...--V--Y--γ   <-- name3
  \
   W--Z   <-- name4

Finally, the loop will do this same process yet again for name4, which currently points to commit Z but will end up pointing to a new commit δ (delta):

    U--X--α   <-- name1 (HEAD)
   /
  T       β    <-- name2
 /       /
...--V--Y--γ   <-- name3
  \
   W--Z--δ   <-- name4

The general problems

One issue here comes about when more than one name points to the same underlying commit. In this case you must decide whether you want to adjust all names in the same way—i.e., to have name2 and name3 both advance to point to commit β—or whether you don't want it:

  • If you do want this, you must make sure that either you use some branch-name-updating operation (git branch -f, git merge --ff-only, etc) to update all the names that point to the specific commit. Otherwise you are relying on getting all the commits done within one second, so that the time stamps will all match.

  • If you don't want this—if you need the names to individualize, as it were—you must make sure that your git commits take place at least one second apart, so that they get unique time stamps.

If you're sure that all your names point to different commits, this problem goes away.

The other things to think about are these:

  • Do any of the names points to some existing commit that does have a file named x? If so, you'll overwrite this x from the x we extract from commit α (the first commit we make, on the current branch, at the start of the entire process.)

  • If any names do have x—one certainly does, that being the one that is the branch we were on in the first place—then does that x match the one in commit α? If so, the proposed git commit will fail unless we add --allow-empty. But in our particular case here, that's probably a good thing, since it means we can avoid having a special case to test whether $b matches $current_branch.

  • Do you actually have branch names for all the ... well, it's not clear what to call these things. See What exactly do we mean by "branch"? for details. Let's call them lines of development. Do you have a (local) branch name for each such line? This might be why you have git fetch origin in here: so that you can accumulate an update on all your origin/* remote-tracking names.

    If you have origin/feature1 and origin/feature2, you might, at this point, want to create (local) branch names feature1 and feature2, so as to add file x to the tip commits of these two branches. You'll need branch names to remember the newly created commits. But just using git for-each-ref over refs/heads will not accomplish the desired result: you might want to use git for-each-ref over refs/remotes/origin, accumulate the names minus the origin/ part into a set along with all the refs/heads names (minus the refs/heads/ parts of course), and use those as the branch names.




回答2:


for remote in git branch -r; do git checkout  —track $remote ; <make your changes> ;  git add . ; git commit -a -m “change commit” ; git push origin $remote ; done


来源:https://stackoverflow.com/questions/52092844/add-commit-a-file-to-all-branches

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!