Reset a git branch globally (for all users)

In our current workflow, we have 2 main git branches:

master – stable release branch

testing – were everyone tests their code

Now every developer creates new branches for each feature they develop. When they are done, they merge it to testing, and when our QA says it’s good to go, they merge their branch into master which gets deployed into production.

As time passes, our testing branch get polluted with commits that never make it to production. Abandoned features, stuff that got rewritten instead of fixed and other things.

To keep master and testing in a somewhat consistent state, we’d like to “reset” testing from time to time. Right now, we do so by entirely removing testing and re-branching it from master.

The big problem here is that we need to make sure that every single developer also remove his local testing branch and checks out a fresh copy of it.
If one developer forgets to do that and pushes to testing again, all the dirty commits that we are trying to get rid of are back.

Is there any way to reset a branch on the server in a way that it distributes to all users?

An acceptable solution would also be putting the testing branch in a state where nobody can push to it anymore without doing a reset locally. But I can’t think of a way how to do it.

Creating a diff between master and testing and reverting commits is not an option as this prevents each of these commits to ever go into testing again.

Ideally, I’d have a script that performs this reset periodically and no interaction (other than git pull) is needed on each users local environment.

  • How to automatically remove remote-tracking branch when the corresponding local branch is removed using git
  • Removing remote Git branch using JGit
  • Git: what exactly causes remote branches to update?
  • Git fetch a branch once with a normal name, and once with capital letter
  • Git: How to check whether local branch is ahead of remote branch when working directory is modified?
  • Why does Git tell me “Not currently on any branch” after I run “git checkout origin/<branch>”?
  • git : how to specify a default remote push-to branch?
  • How to remove remote branch with JGit
  • 4 Solutions collect form web for “Reset a git branch globally (for all users)”

    The short answer is “no, you can’t do that”.

    Remember that each clone is a complete stand-alone entity1 that is little different from the source repository it was cloned-from, except for its origin and (depending on clone options) some of the initial branch states.2 Once someone has picked up a branch named testing and called it origin/testing:

    • they have the commits that you let them have; and
    • they have a reference (“remote-tracking branch”) named origin/testing that their git will update automatically, and even prune (delete) if directed, when they connect to remote origin.

    So far so good, and this “automatic prune” action sounds great. If you can convince them to set remote.origin.prune to true:

    $ git config remote.origin.prune true
    

    then once you delete your branch named testing, their origin/testing will go away automatically on their next git fetch origin.

    The problem comes in when they create a branch named testing. Their git won’t delete this branch unless and until they ask it to. As far as their git is concerned, their private branches are their private branches. You can’t convince their git to delete their private testing any more than you can convince their git to delete their private experiment-22. They created it; it’s their repository; they keep control of it.

    (Note that they also keep control of automatic pruning, since they can git config the remote.origin.prune setting away, or to false, at any time as well. That setting is meant for their convenience, not yours—it goes with their remote.origin.fetch settings, which they change so that their git fetch changes what it does; its initial default setting is something they created when they ran git clone.)

    You can continue with this model, provided you get all your developers to do their own controlled deletion or cleaning of this branch label. But it’s not the way to go. Instead, you should use another model: create a new, different branch label for your developers for the new (and different) line of development you’re doing.

    For instance, you might have dev-feature-X as a temporary branch that your developers can all share for working on Feature X. When you’re all done with it, you keep or delete it at leisure, and your developers pick up the deletion automatically (with the prune setting) or not at their leisure. Meanwhile you’ve created dev-feature-Y as a temporary branch that your developers can all share for working on Feature Y, and so on.


    1Ignoring special cases like “shallow” clones that don’t apply here, at least.

    2If you clone without --mirror, the source’s branches become your remote branches, and you have no local branches at all until you check one out (usually master, usually as the last step of the clone command). Also, clone can’t see the source’s hooks, so those are not cloned. Neither is any other special state in the .git directory, such as items in .git/info. None of these affect the principles of ordinary branch usage, though.

    As time passes, our testing branch get polluted with commits that never make it to production. Abandoned features, stuff that got rewritten instead of fixed and other things.

    How is this possible? Clearly if a feature gets abandoned, then you should remove it from your testing branch as well, because it seems to be your gate keeper. Basically, if you say that your testing branch gets polluted with time, then it defeats the whole purpose of a testing branch, because now you are testing something which doesn’t represent the code which you want to push to production.

    If something doesn’t make it, then the developer should revert his changes and push a commit to the testing branch where the change gets reverted as well.

    In your scenario you should merge from testing to production either all or nothing.

    One option is to reset the state of the development branch by merging in the master branch in a special way.

    git checkout master
    git checkout -b new_testing
    git merge -s ours testing # this creates a merge commit, but
                              # its tree is that of the current work-tree
                              # which in our case is the same as master
    git checkout testing
    git merge ours_testing
    git branch -d new_testing
    

    We need to create the temporary new_testing branch since the merge strategy ours keeps the current tree rather than the other tree, and there is no equivalent theirs strategy.

    After this you will end up with a branch structure like

    *         (testing) merge
    |\
    | *       (master) last commit on master
    * |       last commit on testing
    | |
    

    But the content of testing will match the content of master.

    The advantage of this is that anyone that has local commits on testing that
    have occurred after last commit on testing will be able to rebase their changes up onto origin/testing as normal.

    Since this shouldn’t interrupt the usual development flow, there’s no reason why it can’t be done frequently (nightly?).

    If one developer forgets to [rebase] and pushes to testing again, all the dirty commits [from an abandoned testing tip] that we are trying to get rid of are back.

    You can’t control what goes on in other people’s repos, but you can control what they push to yours.

    An acceptable solution would also be putting the testing branch in a state where nobody can push to it anymore without doing a reset locally. But I can’t think of a way how to do it.

    This pre-receive hook will refuse pushes introducing unwanted history via merge:

    #!/bin/sh
    #  Do not permit merges from unwanted history
    #set -x
    err=0
    while read old new ref; do              # for each pushed ref
    
            [[ ${old%[^0]*} = $old ]] && continue # new branches aren't checked.
    
            nomerge=$(git for-each-ref refs/do-not-merge --format='%(objectname)^!')
    
            if [[ $( git rev-list --count --ancestry-path --boundary $old..$new $nomerge
             ) != $( git rev-list --count --ancestry-path --boundary $old..$new ) ]]; then
                    echo "$ref doesn't allow merges from outdated history"
                    err=1
            fi
    done
    exit $err
    
    # why it works:
    
    # if adding nomerge commits' parents as ancestors has any effect, then the
    # nomerge commits are reachable without going through $old, i.e. they're 
    # in some merged history. So check whether adding the abandoned commits as
    # explicit ancestors to the push makes them show up, and refuse it if so.
    

    To mark unwanted commits, refer to them under refs/do-not-merge, for instance

    git config alias.no-further-merges-from \
      '!f() { git update-ref "refs/do-not-merge/$1-@`date +%Y-%m-%dT%H%M%S`" "$1"; }; f'
    

    So the ritual for abandoning testing is

    git no-further-merges-from testing
    git checkout -B testing master
    

    and if you want to mark previously abandoned tips you can refer to them by sha or by any other expression, say

    git no-further-merges-from 'testing@{last october 31}'
    
    Git Baby is a git and github fan, let's start git clone.