Home > Software engineering >  Modified history (rebase, reset, etc.) on the remote. How should other teammates behave?
Modified history (rebase, reset, etc.) on the remote. How should other teammates behave?

Time:08-13

I have read many books and blog posts about dangerous git commands like rebase or reset which are not to be used when the branch is already published because they can rewrite history and other people that have old history can have errors when syncing their commit, but I have found no docs about how to resolve this type conflicts when is made.

I understand that each commit has a pointer to the previous but when the history is rewritten and pushed other teammates has different commits hash, what append when they fetch/pull their commits with different history?

I found an article that says to do a git fetch and git reset --HARD to a point in common between remote and local (in this case I lose my commits), but I'm interested to know more about the behavior of git when it encounters this conflict situation.

CodePudding user response:

What do these commands do

git rebase inserts some commits from one branch to another branch (changing commit metadata).

Imagine a history like this:

C E
| |
B D
|/
A - common anchor

If the branch with E is then rebased onto the branch, with C, those commits will be taken and the branch is changed:

E' - branch with E is here now
|
D'
|
C - branch with C still here
|
B
|
A

The command git reset does something similar. It changes a branch to point to another commit (and its history). That commit can be completely unrelated.

The problem

If multiple people work on the same branch, changing the history of that branch is a problem because their history is then inconsistent with the remote history.

This makes them unable to push any changes.

The real enemy here is not the rebase/reset command but git push -f (or git push --force or git push --force-with-lease). This command completely overwrites the remote branch with the state of your local branch.

Mitigations

If this has been done, other people have different possibilities:

Force push

they could just force push their version of the branch which is basically removing everything you have done to the branch and replacing it with their version.

You and everyone else who checked out the remote branch in the meantime will have the same problem and your charges to the remote are reverted.

merge

If there is a common anchor, they could merge both versions. Pulling includes merging by default.

This would result in both histories being present afterwards and likely results in merge conflicts.

reset

They could reset their own changes to the state of the remote. This would loose all their commits taken that are not present in the remote.

reclone

They could clone their repository and apply all their changes again manually.

stash and reset

If they don't have any commits that are not present on the remote (in a modified form), they could stash (temporarily save uncommitted changes) their changes, reset their local state to the remote and then apply the stash again.

Soft reset

They could also soft reset their local branch the remote. This changes their branch to match the remote but keeps local changes.

Coordination

If you really need to do a force push (because of a reset or rebase), you need to communicate it with your teammates so that all of them know what to do and that they don't do anything conflicting with each other.

It is far less pain if you first get the repository to a state where the branch has all current changes and nobody else works on that branch (and other branches being impacted by the action). Then, you can do your changes and since nobody else currently has work on that branch, you can rebase/reset and force push to it. After that, everyone else can re-clone the repository (or (hard-)reset to that branch).

But ideally, you shouldn't force push to any branch if anyone else is working on the same branch.

How to behave if something has suddenly been force-pushed/rewritten

If a branch you work on is force-pushed to, you should not just try to fix it on your own. Instead, find out who is responsible for the change and ask them.

CodePudding user response:

I'm interested to know more about the behavior of git when it encounters this conflict situation.

When you push an altered history with git push --force, then pull on a second client which did a commit on the original version of the history, git will gracefully do a merge of these two histories. Both the original and the altered history will be present as the parents of the merge commit.

*   40f7179 (HEAD -> master) Merge branch 'master' of origin
|\  
| * 2ee7131 (origin/master) altered commit of legit A (team mate)
* | 4c0da00 legit commit B (you)
* | 4f5829c legit commit A (team mate)
|/  
* cb37efc initial commit

Your Team mate pushed A, you made commit B based on that. Then your team mate pushes an altered history. You pull and git will automatically merge that.

CodePudding user response:

tl;dr: Show everyone how to use rebase with the --onto option from the command line. (Or, explain how to create a new branch and cherry-pick their changes, for those that might have a panic attack when typing the word "rebase".)

Details:

First off, I want to provide a little context to the absolute statement you mentioned, which could be paraphrased as:

Never rewrite branches that have already been pushed.

One obvious problem with that is you should "never say never" since there may be cases where it makes sense, even if contrived. The other problem is that not all branches are created equal. Feature branches that are pushed but not used by other people could potentially be fair game to rewrite. Especially if you use PRs for your code review process. If a reviewer tells you to fix that one character typo, you could make a new commit and push it out for everyone to see forever and ever, but does that new commit have any value? I'd argue no; it just wastes brain cycles for anyone in the future who is unlucky enough to see that commit. The typically better option is to amend your good commit with the one character change and force push out your feature branch so the PR gets updated. Usually no one else is working on that branch without you knowing about it, and if someone is you can tell them you force pushed so they can read the rest of this answer to figure out what to do about it. If someone is using your branch, didn't tell you, and is basing new code off of it instead of using main (or similar), then let them figure out on their own that they need to read the rest of this answer to remove that one character typo and fix from their history. When we talk about the dangers of force pushing, what really matters is force pushing shared branches; it's not about pushing any branch in the repo. Perhaps the statement could better be written as:

Rewriting shared branches should generally be avoided.

Mistakes happen. We can hope this would be extremely rare, but sometimes even shared branches are rewritten, and that's what this question is about. When a shared branch is purposefully re-written the person doing it should shout from the hilltops announcing it, so that everyone knows, and ideally instructions for what to do would be included in the announcement.

How should other teammates behave?

First of all, if there wasn't an announcement with instructions, then they should either shout from the hilltops that they noticed it, or at least ask the repo admins/maintainers if it was intentional. If it was accidental, then it can be fixed ASAP and perhaps new measures can be taken to lock down that shared branch to prevent future force pushes.

If it was intentional, then they should follow the instructions that have already been given, or will be given shortly. (I realize this is a cheeky response. I feel strongly that anytime a shared branch is force pushed, instructions should always be provided. Even advanced Git users sometimes forget the exact commands to use in this situation.)

Background: Why do we need special instructions?

Let's suppose the branch that was rewritten is called main. The problem with the force push is that multiple people likely have branches that came off of main, and likely from different commits. After the force push of main, if the commit they branched off of is still on main, then they are fine, but if the commit they branched off of is no longer on main, then they will have to "rewrite" their own branch. If your team is already using a rebase strategy, where devs regularly rebase their branches onto main, perhaps like this:

git fetch
git rebase origin/main

then the instructions will probably not feel too scary for most users. But, if users are not regularly rebasing and instead typically merge in main like this:

git fetch
git merge origin/main

then your users are probably going to moan and groan a little louder, so prepare yourself for this.

Important: Devs should not merge or pull (which by default would do a merge), to bring in the latest main after a force push. Doing so will leave both the old and new version of main in the history, effectively making the force push of main pointless.

What should the instructions be after a shared branch is force pushed?

Let's suppose the branch that was rewritten is called main, your remote is called origin, and the user's feature branch is called feature-branch. As part of your instructions you should provide the previous commit ID that main was pointing to before it was forced pushed. Let's call that commit ID old-main-commit-ID. The idea is to perform a surgical rebase using the --onto option. Note that users need to do this for each of their branches that are still in progress.

# get the latest version of origin/main
git fetch

# rewrite your feature-branch
git rebase old-main-commit-ID feature-branch --onto origin/main

# if you already pushed your feature-branch, then next time you push:
git push --force-with-lease

In words, the rebase says:

Take all of the commits on feature-branch that were not on the previous version of main, and replay each of those commits one by one, in order, on top of origin/main.

Like anytime you merge or rebase, there may be conflicts, and with rebase there could be (in the worst case scenario) conflicts at every commit. Hopefully your teammates already know how to resolve conflicts and here it would no different than the usual conflict resolution, but sometimes it's helpful to offer up some of the expert conflict resolvers to assist, when many people will be rebasing like this on the same day.

Side Note: Oftentimes when a shared branch is rewritten, it's to remove something on the branch that isn't necessarily at the tip. For example, it may be 3 PR's ago that you need to strip out. In this case someone needs to manually redo those last 2 PRs again that you wish to keep, or at least do another fancy rebase to get those into another merge. In the latter case the rebase command would be similar to above, but this time you would need to set the first commit ID to the parent of the first commit you wish to keep, and the second commit ID to the last commit you want to keep, instead of using a branch name. This will leave you in a detached state after the rebase is complete, and then you can simply create a branch at that commit and merge it into main with it's own PR, if desired.

  •  Tags:  
  • git
  • Related