How to add a file to a specific commit with git filter-branch?
Asked Answered
A

2

3

We need to add some file in a specific past commit on a repo and we are ready to rewrite history so it affects to all branches diverging from that commit's children. We are trying to use git filter-branch for that, using add, since once we add the file to a commit it won't be added to the children, but we can't find the right parameters for stop it affecting to some concurrent diverging commits. See image for understanding.

We are using this command targeting the red commit, but the file is appearing on the purple commit - why? - and it's also appearing on the green commit, and we don't want it to affect that code path, just appear on the red commit and then be inherited through all the child commits and the merge.

git filter-branch --index-filter "cp C:/Users/asdf/Documents/IdeaProj ects/git-crypt-tests/.gitattributes . && git add .gitattributes" 72c7e292ddd134c04285f3530afc829fd38de4 28..HEAD

What am I understanding bad?

Thank you.

enter image description here

Aleta answered 15/1, 2019 at 13:14 Comment(1)
Things get potentially tricky here, because the parent of the "New data to return" commit is actually a merge commit, meaning that your branch has two visible ancestors. This might be a deal breaker for doing a rebase, and filter branch could also have problems with this.Jackal
H
2

It looks like you thought that when you write a commit range as A..B that it would include the bounds. But it does not. This notation is short for B ^A, i.e., everything leading up to B, but excluding everything up to A. This removes the "lower" bound A from the range. The solution is that you write A~, which means "the ancestor of A": A~..B.

Furthermore, since you know exactly which commits you want to add the file to and which you do not want to add them, you can restrict the revision walker to list only the wanted commits:

git filter-branch --index-filter "cp C:/Users/asdf/Documents/IdeaProjects/git-crypt-tests/.gitattributes . && git add .gitattributes" -- HEAD --not 72c7e29~ ":/Fix Prestapp"

That is, you say that you want all commits leading up to HEAD, but nothing before 72c7e29~ nor before the commit whose message begins with Fix Prestapp.

Horace answered 15/1, 2019 at 13:39 Comment(5)
I am getting a very hard time trying to understand those parameters. It seems to be well documented nowhere. Is there any reference so I can stop posting basic questions on SO? Thank youParcae
@ÁxelCostasPena Look for gitrevisions; text search, ancestry. I used the text search variant only because you did not show an SHA1 for the commit and it was obvious from the image which commit you mean.Horace
thank you, that was incredibly useful. But why don't just focus on a single commit? I read the docs and tried, and why does this command also adds the commit to both branches? The man says this is like selecting a single commit. git filter-branch --index-filter "cp C:/Users/asdf/Documents/IdeaProjects/git-crypt-tests/.gitattributes . && git add .gitattributes" -- HEAD 72c7e292ddd134c04285f3530afc829fd38de428^^!Parcae
@ÁxelCostasPena A^! is short for A ^A^1 ^A^2 ^A^3..., i.e. A is included and all its parents are excluded; the result is a single commit. But when you say B A^!, you also select all commits leading to B in addition to A (but exclude all parents of A and before). Therefore, if B is a descendent of A (like in your example), you get all commits from A to B inclusive.Horace
thank you! It's still giving me some headaches but I've just discovered the rev-list so I can now debug the entire commit list.Parcae
H
0

Just another universal way to rewrite a single commit in any commits tree without build a tricky rev-list.

  1. Create tag on a commit if not exist - my-tag.

  2. Say

    git filter-branch --index-filter "cp \"<local-path-to-file>\" \"<sourcetree-path-to-dir>\" && git update-index --add \"<sourcetree-path-to-file>\"" -- my-tag --not my-tag^@
    

    Or use a shorthand instead of my-tag --not my-tag^@:
    my-tag^!

  3. Remove tag if added in step (1).

It is much faster, because no need to know the commits tree structure.

If you want to rewrite a commit together with it's children, then you can use git replace (git replace --graft <commit> [<parent>…​]) plus git filter-repo script to propagate changes into children commits:

Update all children branches

git replace --graft <commit-child-1> <commit-child-1-parents>
git replace --graft <commit-child-2> <commit-child-2-parents>
...
git replace --graft <commit-child-N> <commit-child-N-parents>

Commit changes and cleanup

git filter-repo --force
git for-each-ref --format="delete %(refname)" refs/replace | git update-ref --stdin

, where:

  • <commit-child-*> - children of the rewritten commit.
  • <commit-child-*-parents> - list of commits contained your rewritten commit.

So, in a simple case of single child it would be only a single call with a single commit:

git replace --graft <purple-commit> <rewritten-red-commit>
git filter-repo --force
git for-each-ref --format="delete %(refname)" refs/replace | git update-ref --stdin

Note:

If you are trying to replace a file and it has changes in next child commit(s), for example, changelog.txt file, then you must rewrite it in each next child, otherwise the next commits will be left with old file. In that case actual to use git filter-repo with file text search and replace instead of a file add/replace or manually rewrite each next child commit before call to git replace --graft ....

Hanleigh answered 19/5, 2023 at 10:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.