63
git filter-branch --env-filter '
export GIT_AUTHOR_EMAIL="foo@example.com"
export GIT_AUTHOR_NAME="foo"' -- commita..commitb

Results in Which ref do you want to rewrite?

So it seems that filter-branch doesn't allow you to use range notation use a range between two arbitrary refs.

What is the most straight forward way of running a filter over a range of consecutive commits (somewhere within the history of a branch) if this approach isn't possible.

Acorn
  • 49,061
  • 27
  • 133
  • 172
  • 17
    Seriously, who invented an error message as useless as that? The only use for that message seems to be to enter it into Google... Something like "the end of the range needs to be a reference, not the ID of a commit" (thx to @qqx) would appear to be more helpful. – oliver Apr 01 '15 at 09:41

8 Answers8

50

You cannot apply the filter-branch in the middle of the history, as said by @kan. You must apply from your known commit to the end of the history

git filter-branch --env-filter '...' SHA1..HEAD

Filter-branch can check for the commit author or other information, to chose to change or not the commit, so there are ways to accomplish what you want, see https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History, look for "Changing Email Addresses Globally"

Remember: if you have pushed the commits to a public repository you should not user filter-branch

Pedro Sanção
  • 1,328
  • 1
  • 11
  • 16
  • 2
    *PSA:* It cannot rewrite the middle of history, because it would invalidate the parent field of the tail end attached to the middle. – cdosborn Jun 09 '17 at 18:57
  • To do it only on the most recent commit (the head commit), use `HEAD^..HEAD`. – ADTC Jul 21 '22 at 18:53
25

The cleanest solution I found was to do the following:

  1. Create a temporary branch at refb.
  2. Apply the branch filter on refa..temp.
  3. Rebase onto the temporary branch and delete it.

ie.

git branch temp refb

git filter-branch --env-filter '
export GIT_AUTHOR_EMAIL="foo@example.com"' refa..temp

git rebase temp
git branch --delete temp
Acorn
  • 49,061
  • 27
  • 133
  • 172
  • 1
    +1 – nice trick, but I think the filter-branch needs to be run on `refa..temp` and your rebase command should be `git rebase --onto temp refa refb` – Chronial Mar 07 '13 at 07:34
12

git filter-branch does accept range notation, but the end of the range needs to be a reference, not the ID of a commit.

git checkout -b tofilter commitb
git filter-branch .... commita..tofilter

If given just commits, it would not know what ref to update with the filtered branch.

qqx
  • 18,947
  • 4
  • 64
  • 68
  • 5
    This being the case, how would I stop my filter script from being applied to commits before commita and commits after commitb? – Acorn Mar 06 '13 at 15:13
7

Enclose you filter commands in an if-statement that checks for that range. You can check whether a commit is within a given range with this command:

git rev-list start..end | grep **fullsha**

The current commit will be stored in $GIT_COMMIT in your filter. So your filter becomes:

git filter-branch --env-filter '
  if git rev-list commita..commitb | grep $GIT_COMMIT; then
    export GIT_AUTHOR_EMAIL="foo@example.com"
    export GIT_AUTHOR_NAME="foo"
  fi' -- ^commita --all

If you want to only rewrite your current branch, replace --all with HEAD

Chronial
  • 66,706
  • 14
  • 93
  • 99
5

You cannot just override commits in a middle of a history, because sha1 of a commit depends on a parent's. So, the git doesn't know where do you want point your HEAD reference after the filtration. So, you should rewrite all up to the HEAD.

Example:

A---B---C---D---E---F   master
            \
             \--G---H   branch

if you want filter commits B and C you should also filter all commits after: D, E, F, G, H. So, that's why git tells you to use a ref at the end of the range, so that it just not finishes up with a detached head.

After you modify B and C commits and stop will look like this:

A---B---C---D---E---F   master
\           \
 \           \--G---H   branch
  \-B'--C'      (HEAD or a temporary TAG?..)      

So, the master and branch will be untouched. I don' think this is that you want. It means you must override all commits. The history will be then:

A---B---C---D---E---F   (loose end, will be garbage collected one day)
\           \
 \           \--G---H   (loose end, will be garbage collected one day)
  \-B'--C'--D'--E'--F'  master
            \
             \--G'--H'  branch
kan
  • 28,279
  • 7
  • 71
  • 101
  • 1
    So what do you think the best approach is if I don't want to apply my filter to commits after `C`? Could I create a temporary TAG maybe? – Acorn Mar 06 '13 at 15:00
  • @Acorn If you want to keep commits after filtered C (I call it `C'`) you must filter all of them too at least to change their ancestor from `C` to `C'`, if not, just drop them moving master branch to the `C'`. There is no option. A commit has all history prior to it encrypted in its SHA1 id, so each commit is cryptographically protected, you cannot alter history without being noticed. – kan Mar 06 '13 at 15:23
  • 1
    Right, so I get the impression that I'd have to make the decision whether to apply the author/email change in my bash scriptlet. How can I tell whether I am within the range from there? – Acorn Mar 06 '13 at 15:30
  • @Acorn This is a new question. Maybe easier thing to do is to take a list of commits which you are interested in by `git rev-list commita..commitb` and when check in your scriptlet if the current commit is in the list. Also, could be useful: http://stackoverflow.com/q/3005392/438742 – kan Mar 06 '13 at 15:43
5

I do it this way.

Let's say you want to filter the content of a branch called branch-you-are-filtering.

Assume that there's an ancestor commit to that branch, with a ref called ref-for-commit-to-stop-at.

git filter-branch --commit-filter 'YOUR_FILTER_COMMANDS' branch-you-are-filtering...ref-for-commit-to-stop-at

After executing, the commit at ref-for-commit-to-stop-at will not be altered. All the filtered\changed commits in branch branch-you-are-filtering will be based on ref-for-commit-to-stop-at.

Whether or not you're using --commit-filter or something else is up to you.

Mark F Guerra
  • 892
  • 10
  • 13
0

The solution from @Acron seems wrong to me. I would suggest following to change between refa and refb including both hashes:

  1. git tag master.bak
  2. git reset --hard refa
  3. git filter-branch --env-filter ' export GIT_AUTHOR_EMAIL="foo@example.com"' refa^..master
  4. git cherry-pick refb..master.bak
  5. git tag -d master.bak
niels
  • 7,321
  • 2
  • 38
  • 58
0

Use filter-branch's --setup parm and some shell power:

git filter-branch --setup '
for id in `git rev-list commitA..commitB`; do
         eval filterfor_$id=rewrite
done
rewrite() {
        GIT_AUTHOR_NAME="Frederick. O. Oosball"
        GIT_AUTHOR_EMAIL=foobar@example.org
}
' --env-filter 'eval \$filterfor_$GIT_COMMIT'
jthill
  • 55,082
  • 5
  • 77
  • 137