1

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.

Phil
  • 818
  • 5
  • 14

4 Answers4

2

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.

dustinmoris
  • 3,195
  • 3
  • 25
  • 33
  • 1
    While this is how it *should* work, in practice it doesn't. It's impossible to prevent 20 developers from 'forgetting' about their features. Even worse when business decides to put something on hold and the dev doesn't know whether this may still be needed 3 months later or if it is dead already. – Phil Feb 25 '15 at 00:53
  • Did you try rebase? Perhaps this does the trick then: http://git-scm.com/book/en/v2/Git-Branching-Rebasing – dustinmoris Feb 25 '15 at 01:00
  • Did you just post any random link you found? Rebasing has nothing to do with my problem. – Phil Feb 25 '15 at 02:56
  • @MrTweek, how do issues get resolved in your system? Perhaps they should remain open (or in some other non-closed state) until the relevant code gets merged *or removed from testing*. – ChrisGPT was on strike Feb 25 '15 at 14:45
  • Chris, it's a very agile environment and commits are not necessarily connected to a ticket. – Phil Feb 26 '15 at 04:12
2

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • The first line seems true. This is simply not possible. It's no problem telling everyone to set `remote.origin.prune`, but since I will delete the branch on the server and re-create it immediately, it won't have any effect. Next push will push all dirty commits back up. We already use feature branches, but we need the testing branch for continuous integration, to have one central point that gets build and deployed automatically and that QA can test. – Phil Feb 25 '15 at 02:51
  • I'm not sure how you are implementing your CI, but if you simply have multiple CI branches that you cycle through ("testing_1", "testing_2", etc) and leave most of them deleted most of the time, you'll only encounter problems if a developer has managed to not run "git fetch" (and hence prune) the branch for long enough for it to have come back into rotation. Same basic idea as above, just slightly different details... – torek Feb 25 '15 at 04:01
1

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?).

Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
  • I just tried this out. While it does what it does what I need, it only does not distribute this information to users. As soon as any user runs `git push`, all the dirty commits are back in the branch. – Phil Feb 25 '15 at 01:20
  • A simple push will not put the bad commits back, only a `push --force` will. But if your developers are using `push --force` everything is going to go royally wrong, they'll be overwriting each others changes etc. If they rebase they'll be able to push, but in that case the bad commits will be gone. And as @jthill mentions you can prevent accepting `push --force` by setting `denynonfastforward` in the remote repository. – Michael Anderson Feb 25 '15 at 03:19
1

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}'
jthill
  • 55,082
  • 5
  • 77
  • 137
  • `git config receive.denynonfastforward true` does not seem to have any effect on this behaviour. It still pushes all dirty commits from the local branch to the new and clean remote branch. – Phil Feb 25 '15 at 02:39