问题
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 commit
s 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 thisx
from thex
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 thatx
match the one in commitα
? If so, the proposedgit 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 yourorigin/*
remote-tracking names.If you have
origin/feature1
andorigin/feature2
, you might, at this point, want to create (local) branch namesfeature1
andfeature2
, so as to add filex
to the tip commits of these two branches. You'll need branch names to remember the newly created commits. But just usinggit for-each-ref
overrefs/heads
will not accomplish the desired result: you might want to usegit for-each-ref
overrefs/remotes/origin
, accumulate the names minus theorigin/
part into a set along with all therefs/heads
names (minus therefs/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