Any way to have git add a file from master into 10 other branches?

Viewed 78

Is there any way to tell git to add a new file into every other branch?

Rather than git checkout <branch>; git merge master for every branch, is there a shortcut for adding newfile into each branch below?

     master
       +-----newfile
       |
      --- 
    /  |  \
   /   |   \
  /    |    \
foo   bar   baz

EDIT: This is the bash script (needs some cleanup if implemented) I came up with but hoping there's a better way.

MSG='added newfile which contains a method to workaround issue IS-797'
for branch in $(git branch | grep -v master | sed 's#^[ *\t]##g'); do
   echo merging into $branch
   git checkout $branch && git merge master -m "$MSG"
done
3 Answers

There are a couple ways to go about this. Most of the advice in other comments/answers (as of this writing) are suspect in my opinion. Here's a rundown of different options, with pros and cons.

First of all, there are (at least) three different options for what the final history can look like. Each has pros and cons. Sort of. More like each has cons, and the pros are that it doesn't have the other ways' cons.

  • It can look like the file was "always there", since before the affected branches were created. The big down-side is that this is a history rewrite, and that has consequences.

  • It can look like the file was independently added by each branch. The file will likely end up with a hard-to-read history, and there's a lot of potential for avoidable merge conflicts. But, it doesn't rewrite history.

  • It can look like the file was added once, and then merged into each branch. Some people really hate merge commits, and even as someone who doesn't particularly hate merge commits I think this approach leads to a messy branch topology. But, it avoids the history rewrite and will work well going forward; i.e. it's functionally the cleanest option, but aesthetically rough.

Looking at each in more detail:


Rewriting History :

If you alone use your repo, and you've never done anything that makes your existing commit ID's ("hashes") important, then this is a fairly attractive option. Even if you share the repo, you can make this work as long as you coordinate with the other users of the repo.

For big rewrites, my general advice is to coordinate with everyone to have a set date/time when all work will be pushed to the repo. Then everyone discards their clones, you do the rewrite, and everyone re-clones. If that's not practical, then a big rewrite probably isn't a good idea; but if you want to consider it anyway, read up on "recovering from upstream rebase" in the git rebase docs for more information about what will have to happen.

While rebase is, I suppose, the prototypical way to do a history rewrite, in this case it's almost certainly the wrong way. If you have many branches, you have to do it many times. If you have a complex history, it becomes very hard to do correctly. In particular it doesn't deal with merges well (even with recently-added improvements).

Instead you could use git filter-branch. The easy way is with a --tree-filter. You would put a copy of the file somewhere outside the work tree, then

git filter-branch --tree-filter 'cp /path/to/stored/copy/of/file path/to/file/in/worktree' -- --all

Then filter-branch will proceed to check out each commit in your history, run the cp command to add the file to the worktree for that commit, and write a new commit with the resulting content. Then it will move branches from the old commits to the new ones. If you have tags and want them to move, too, add a --tag-name-filter argument like

git filter-branch --tree-filter 'cp /path/to/stored/copy/of/file path/to/file/in/worktree' --tag-name-filter cat -- --all

The problem with this is, if your history is large, the process is slow. You can speed it up by using a ramdisk for the work tree (see the -d option), or by using an --index-filter instead of a --tree-filter (but this requires you to use different commands to directly update the index, and it's not quite as simple).


Each Branch Adds the File

This is what cherry-pick would get you. Since the question is about how to automate the process, it's unclear why someone commented with cherry-pick as though it were a solution. The process is just as manual (and just as scriptable) as merging.

The only difference is, when you do it this way git creates independent commits that individually add the file on each branch.

You could use the same basic script for this as you use for merging, just replacing the merge command with the cherry-pick command.


Merges

For this, you already have drawn up a reasonable solution - you need a script. The only think I would note is, git for-each-ref is often a more suitable command for feeding into a script, rather than git branch. https://git-scm.com/docs/git-for-each-ref

You can use git rebase to achieve it, but do read why you should not use git rebase from this article

If you already have the commit on master, I would simply cherry-pick it on each branch. This seems by far the simplest and most robust one to script; a failure in one branch should be inconsequential for the other branches.

commit='deadbeef'
for branch in $(git branch | sed -n '/master/!s#^[ *\t]##p'); do
   echo "cherry-picking onto $branch"
   git checkout "$branch" && git cherry-pick "$commit"
done

As an aside, note the preference for lowercase shell variables, the quoting, not using the g flag in sed when you only expect a single match on each line, and avoiding the useless use of grep.

The use of \t in the sed script isn't entirely portable; maybe try [*[:space:]] instead of [ *\t].

Cherry-picking an individual commit is of course also much more specific than merging all of master into each branch, which could well be a complete no-no for others who struggle with this.

Related