gitgitlabresetmerge-conflict-resolution

Revert to a specific commit on Gitlab


I'm fairely new to git.
While working on a project on Gitlab, I made some changes in the development branch(green colored branch) instead of checking out a new branch from the development, making changes in the new branch and then merging it back to the development.
Meanwhile, one of the other developers took a pull from the development branch and started working on it(ARE-1195). Now, I know this is going to create a lot of merge conflicts when the developer will try to merge his branch into the development branch. How can I avoid this? I tried looking for a possible solution and came across two terms, revert and reset but i'm confused between these.
I want to revert my repository back to the commit where the other developer created ARE-1195 but still keep the changes I made after that.
Posting the latest git graph snippet for reference.

enter image description here


Solution

  • I tried looking for a possible solution and came across two terms, revert and reset but I'm confused between these.

    Your confusion is appropriate. Git's author (Linus Torvalds) unfortunately chose the wrong verb for at least one of these two actions: the one called revert should probably have been called backout (as it is in Mercurial).

    To make sense of both, though, we should start with what a commit is and does for you. The image you included—which I will transform here into a new, different image—shows some of this:

           B--C--D--E   <-- ARE-1195
          /
    ...--A-----F----G   <-- development-ui-...
    

    Each uppercase letter here, A through G, stands in for a commit, just as each colored dot in the original image stands in for a commit. In my drawing, the newer commits are towards the right, while in the original image, the newer commits are towards the top. So the drawings are different but they show the same thing:

    Every commit, in Git, has a unique number, but this number is huge—something between 1 and 1461501637330902918203684832716283019655932542975, right now, with future versions of Git going even higher—and seems entirely random (though it's not). It's normally expressed in hexadecimal and often abbreviated, e.g., as a123456 for instance.

    This number—the unique number for this one particular commit—is the commit, in an important sense: no other commit, anywhere, in any Git repository anywhere in the universe, can ever use that same number, unless it's literally the same commit. So two Git repositories can, at any time, meet up and just compare their numbers (which, despite being huge, are much smaller than the commits themselves). If repository R has some number that repository S lacks, R can transfer the commit to S and now they both have it.

    What's in each commit is:

    So, in our drawing above, commit G lists the number of commit F in its metadata. This means that as long as Git can find G, it can find F. Commit F, being a commit, has snapshot and metadata, and its metadata list the hash ID of commit A as its parent, so Git can move backwards from F to A. A has metadata too, which lists some previous commit (not shown here); this repeats on and on, and the commits we find as we traverse this list backwards, one commit at a time, is the history in the repository. Eventually we get to the very first commit ever, which can't list the hash ID of any previous commit, so its list is empty, and that's where we stop going backwards.

    Meanwhile, as long as Git can find commit E, it can use this to find D, which finds C, which finds B, which finds A, which finds whatever comes before A, and so on. So that's the history as seen from ARE-1195.

    Note that there's no way to go forwards. If we're at commit A, we can work backwards to the start of history, but we can't go forwards to B or F, because commit A does not know their commit hash IDs. To make the magical numbering system work, Git must not ever change any part of any commit, so it's not possible to add forward links to future commits—and the hash ID of any future commit depends exquisitely on every bit of data that goes into that future commit, including the exact time at which someone makes it, so we have no idea what a future commit's hash ID will be.

    This is why history always works backwards, and it's also the key to git reset.

    Using git reset to discard commits

    The git reset command is big and complicated. It has a number of different jobs it can do. We'll ignore most of them and concentrate just on the one job that's interesting to you right here and now.

    We noted above that Git needs to find commit G in order to find commit F. Git needs to find commit E in order to find commit D, which it needs in order to find C, and so on. (Either F or B suffices to find A, though.) So how does Git find the random-looking number that designates the most recent ARE-1195 commit?

    The answer is that the name ARE-1195 itself holds the commit hash ID. This is what a branch name does for you and Git: it holds a commit hash ID. (In fact, all of Git's references work like this. References or refs include branch names, tag names, remote-tracking names, and many other names. Branch names are a bit special in that .git/config often holds more information on them, but we'll ignore that for this answer.)

    The git reset command can move a branch name. Technically, it will move the current branch name; if we want to move some branch name that's not-current, we must use git branch -f, or switch to the branch so that it is the current branch. It also does more than just move the branch name, but again we're ignoring all the extra stuff it does for now.

    Suppose we were to move the name ARE-1195 so that it found commit C instead of commit E. That is, what if we made the graph look like this:

                D--E   ???
               /
           B--C   <-- ARE-1195
          /
    ...--A-----F----G   <-- development-ui
    

    Git would now find commit C first, then work backwards to B, and then to A. It's as though commits D and E just stopped existing. They didn't—if you memorized their raw hash IDs, or wrote them down on paper, or something, you'd still be able to find these two commits (and in fact you need only save E's hash ID since E lets you find D)—but they seem to be gone.

    Now, git reset, when used in the mode where it moves a branch name, also does two more things—or rather, optionally does two more things:

    1. First, git reset moves the branch name.
    2. Then, unless you used --soft to make it stop after step 1, git reset resets Git's index.
    3. Then, if you used --hard—but not if you used --soft or --mixed, which stop at steps 1 or 2 respectively—it resets your working tree.

    We haven't explained Git's index and your working tree here (and won't for space reasons) but when you're removing commits like this, you generally want git reset --hard: you want commits D and E to be totally gone forever, or at least seem that way, so you want to get rid of their effect on Git's index and your working tree, which means using git reset --hard.

    That's what git reset --hard will do for you: make it look like those commits are gone. There's one big flaw here though. If you had your Git connect to some other Git software earlier, and had your Git send commits D and E to that other Git, that other Git repository now has those two commits. Git is built to add commits, not to remove them: this git reset --hard trick requires a fair bit of by-hand work, but normal everyday usage of Git will automatically add new commits without a lot of by-hand work. So if you've used git push to send your new commits to someone else, they can easily come back to you, as though the someone-else had made them.1

    If you have never sent these commits anywhere else, using git reset can make them seem to vanish from your repository. Nobody else has them yet and your Git won't hand them over to other Git repositories, because your Git doesn't see them any more. So this is a safe way to get rid of commits, if you have not used git push to send them off.


    1Although Git records author and committer names in the commit metadata, Git doesn't look at those while greedily adding new commits to your repository. It just says oooh shiny new commit, must add it and adds it.


    git revert backs out a commit

    Let's go back to this drawing yet again:

           B--C--D--E   <-- ARE-1195
          /
    ...--A-----F----G   <-- development-ui
    

    Let's suppose further that something in commit D is bad, but that commits D and E have escaped and are out in other Git repositories now. You could use git reset to discard D and E entirely, but they'll come back and re-infect your repository, like some kind of nasty virus. Besides, you like commit E: it's all good! What you'd like to do is back out the effect of commit D.

    Running git switch ARE-1195 first (if needed), then:

    git revert d789012
    

    (if that's D's hash ID)

    or:

    git revert HEAD~1
    

    (~1 because we want Git to count back 1 first-and-only-parent link) or similar tells Git: Figure out what changed in D, and undo that change. Make a new commit whose message is revert <hash-of-D>. The end result is:

           B--C--D--E--∇   <-- ARE-1195
          /
    ...--A-----F----G   <-- development-ui
    

    where new commit backs out the changes made in D.

    Because Git repositories everywhere are greedy for new commits, they will be happy to add this one on, undoing the effect of the one commit. This won't undo the effect of commit E; if you want that you must revert both commits:

           B--C--D--E--∃--∇   <-- ARE-1195
          /
    ...--A-----F----G   <-- development-ui
    

    It's generally wise to revert commits in reverse order: undoing E with gets you files that look exactly like they did after D happened, so that undoing D with has no merge conflicts and gets you files that look exactly like they did after C happened.

    Reverting all the commits in a series backs out all the changes in that series. The git revert command is smart enough to do this in reverse order by itself, so that you simply list all the commits to revert. For instance, to revert both D and E you might run:

    git revert c00795f..HEAD    # if that's the hash ID of commit `C`
    

    or:

    git revert HEAD~2..HEAD
    

    will back out both E and D. Note that when we use the two-dot <hash1>..<hash2> form like this, we give as hash the commit hash ID before the first commit we care about. This—along with the idea that we can use either a hash ID or a name like HEAD or ARE-1195, or even the suffix trick like HEAD~2 or ARE-1195~2, wherever Git needs a hash ID—is basic how-to-work-with-Git knowledge that you should carry around in your head. If you need to brush up on it, though, see the gitrevisions documentation, which is worth multiple readings as it is just packed with good stuff.