1

I am not sure if this question is asked as there are too many scenarios regarding github.

So my situation is as below:

  1. I cloned a master branch to my local working directory
  2. I add new files into the local working directory
  3. I commit the changes
  4. I then do a git checkout -b new_branch
  5. I then push the local working directory to new_branch
  6. The master branch has issues with latest release. Needs to roll back to older release.

Question: How can I do a git reset --hard so that all the files in my local directory are same as previous release version in master BUT preserving my work? such as new files created.

I tried git reset to last release SHA, but it preserves the files in my local working directory with the latest release.

I tried git reset --hard, it removes everything as well as my own work.

Hope this explanation is clear enough for your help.

Thanks.

UPDATE

A clearer understanding on my scenario: I clone a repo master with a v2 tag to my pc. I create a new test branch. I add new files and commit then push to test branch. Now my senior dev told me that master with v2 tag has a problem and ask me not to use. So my test branch needs to fallback to master with v1 tag.

How can I do that without deleting the files that I committed but removing files created by master v2 tag?

Derek Lee
  • 475
  • 1
  • 6
  • 20
  • `git reset --mixed` or simply `git reset`. – pishpish Feb 10 '19 at 20:20
  • Based on the question, I have to ask: what do you know about the differences between a *branch name*, a *commit* as identified by hash aka SHA1, Git's *index*, and the *work-tree* (all four of these are specific technical terms). It sounds like you want to make a new commit for `master` without affecting any of the commits you are making on `branch`, and that's just a natural automatic thing in Git, so there's some sort of issue with the question itself. – torek Feb 10 '19 at 20:22
  • @torek, I made a git push to branch which is clone from master. Now the master has issue. I want my branch to be sync with previous release of master. If i do git reset, it only make changes without removing new files created by master on the latest release. If i do git reset --hard, it removes the changes made by me such as creating new files . How can i sync my branch with master previous release but preserving changes made by me – Derek Lee Feb 10 '19 at 20:38
  • @destoryer, thanks will try out – Derek Lee Feb 10 '19 at 20:38
  • So you're saying that somehow, by pushing to, say, `derek_lee` on the other Git, `master` on the other Git (on the server) has changed? That doesn't happen in Git. Someone else may have pushed something else to the other Git's `master`, but if you didn't, you didn't change it. (It still needs *fixing* of course, but the source of the problem is elsewhere.) – torek Feb 10 '19 at 20:40
  • @torek, sorry maybe my words are not clear. Heres a simple scenario. I clone a **master** with a v2 tag to my pc. I create a new **test** branch. I add new files and commit then push to **test** branch. Now my senior dev told me that **master** with v2 tag has a problem and ask me not to use. So my **test** branch needs to fallback to **master** with v1 tag. How can I do that without deleting the files that I committed but removing files created by **master** v2 tag? – Derek Lee Feb 10 '19 at 20:49
  • Aha, that's very different. You're looking for `git rebase`. (You could update your question to mention what you've said in the above comment. Note, by the way, that you're cloning a *repository*, not a *branch*. It may help to include the actual `git clone` command you used as well.) – torek Feb 10 '19 at 20:57
  • @torek, updated my question. Thanks for your advice :). the git clone is just a simple clone command : `git clone – Derek Lee Feb 10 '19 at 21:09

1 Answers1

4

TL;DR

You want git rebase, which you must follow up with a force-push.

Long, using the updated question:

I clone a repo master with a v2 tag to my pc [using git clone <url>]. I create a new test branch.

Let's assume you did this with:

git checkout -b test v2

where v2 is the tag in question. There's a difference, in Git, between a branch name like master, and a tag name like v2. The difference is actually very small and comes in two parts that are strongly related.

  • A branch name identifies one particular commit. But which commit, changes over time. The name identifies the latest or last commit that we'd like to say is "on the branch". In fact, it changes automatically for you, after you run git checkout <branch-name> and start making new commits.

  • A tag name identifies one particular commit. It should never identify any other commit—it should stay a name for that commit, forever. (It is possible to move a tag—anyone can do this—but for purposes of keeping one's sanity, just don't do it.) And, after git checkout <tag-name>, you are in a slightly odd state that Git calls a detached HEAD.

The reason for all of this is that Git is really not about branches at all, but rather all about commits. A commit is sort of the fundamental unit in Git. Every commit has a big ugly hash ID, unique to that one particular commit. These hash IDs are, in effect, the "true names" of the commits. No commit can ever change—not a single bit—because the hash ID is actually a cryptographic checksum of the contents of the commit. If you could somehow change the contents, that would change the hash ID, which would mean you have a new, different commit.

This cryptographic-checksum thing is also why commit IDs seem so nonsensical, and are pretty much useless to humans. The fact that they are useless to humans is why we need names for them, and hence why we have branch and tag names.

Each commit stores a bunch of things, such as the name and email address of the person who made the commit (and the time-stamp), a log message for git log to show, and a full and complete snapshot of every source file. One of the things each commit stores is the raw hash ID of its parent commit. These form a backwards-looking chain:

... <-F <-G <-H

(where the letters stand in for the actual hash IDs).

Given the hash ID of commit H, Git can find the actual commit itself. That commit contains the hash ID of commit G, so Git can find G. Having found G, Git gets the hash ID of F, so that it can find F, and so on. So Git is constantly working backwards, starting with the latest and going back in time.

Since nothing inside a commit can ever change, we can just draw this as a linear sequence:

...--F--G--H

as long as we remember that it's easy to go backwards but not to go forwards.

Making a new commit is just a matter of:

  • Selecting a branch name, which also selects its last commit:

    ...--F--G--H   <-- branch-name (HEAD)
    

    Selecting the branch attaches the special name HEAD to the name, so now HEAD is simultaneously synonymous with both branch-name and commit H.

  • Doing some work and using git add to copy files back into Git's index (also called the staging area, or sometimes the cache, depending on who is doing this calling).

    Note that if you use git add on existing files, they've been copied into the index over top of the files that were already in the index, replacing some of the old versions. If you use git add on new files, they've been copied into the index, but they're just new files—they didn't overwrite any previous index copy.

  • Running git commit. This freezes the index copies into committed versions. It also gathers from you your log message, and puts you in as the author and committer of this new commit. The parent of this new commit is the current commit H:

    ...--F--G--H   <-- branch-name (HEAD)
                \
                 I
    

    And, now that the new commit exists (and therefore has acquired its own new unique hash ID), Git simply writes its hash ID into the branch-name, so that branch-name now points to commit I:

    ...--F--G--H
                \
                 I   <-- branch-name (HEAD)
    

So, if you did git checkout -b test v2, then you have something like this in your repository. Note that the name v1 identifies some other (different, probably earlier) commit, so let's draw them all:

...--F   <-- tag: v1
      \
       G--H   <-- tag: v2, test (HEAD)

Note that git checkout -b name commit-ID sets things up so that the new name points to the selected commit commit-ID, then makes it current by filling in your index and work-tree from that commit, and attaches the special name HEAD to the new name, all in one command.

I add new files and commit

OK, so you created a new file in the work-tree (where you do your work) and ran git add newfile. This copied newfile into your index—there's one index that just goes with this one work-tree—and then you ran git commit to make a new commit I:

...--F   <-- tag: v1
      \
       G--H   <-- tag: v2
           \
            I   <-- test (HEAD)

then push to test branch.

So at this point you ran:

git push -u origin test

This sends commit I itself to the other Git at the URL your Git is remembering under the name origin. Note that there's a whole separate Git repository there! It has its own branches and tags, though because tags never move—or should never move—your tags and their tags should all agree as to which commit hash ID v1 and v2 point-to.

Having sent commit I, your Git then asked their Git to set—or in this case create—their own branch-name test, pointing to commit I. Presumably this was all fine with their Git, over at origin, so it did that.

Now my senior dev told me that master with v2 tag has a problem and ask me not to use. So my test branch needs to fallback to master with v1 tag.

No part of any existing commit can ever change. This means commit I is stuck where it is. If you made a few more commits, they're all stuck:

...--F   <-- tag: v1
      \
       G--H   <-- tag: v2
           \
            I--J--K   <-- test (HEAD)

What you need is a new series of commits that are like I-J-K, but different in a few ways. In particular, you want your new files to be added to the snapshot that is in F, as pointed-to by tag v1. Then Git should commit that snapshot, re-using your commit message from commit I, to make a shiny new commits with a new and different hash ID that we'll call I' to indicate that it's a copy of I:

       I'   <-- [somehow remembered as in-progress]
      /
...--F   <-- tag: v1
      \
       G--H   <-- tag: v2
           \
            I--J--K   <-- test

Having successfully copied I to I', you now want your Git to copy J to J' in the same way, then copy K to K':

       I'-J'-K'  <-- [somehow remembered as in-progress]
      /
...--F   <-- tag: v1
      \
       G--H   <-- tag: v2
           \
            I--J--K   <-- test

Last, you'd like your Git to peel your test label off commit K and paste it onto commit K' instead, and get back on your branch test as usual, except now this means commit K':

       I'-J'-K'  <-- test (HEAD)
      /
...--F   <-- tag: v1
      \
       G--H   <-- tag: v2
           \
            I--J--K   [abandoned]

Once all that has happened, you need to send new commits I'-J'-K' to the other Git at origin and tell origin's Git to move their test to point to K', too.

git rebase does the first part for you

First, you should run:

git checkout test
git status         # and make sure everything is committed!

If all looks good, then you just need:

git rebase --onto v1 v2

This command tells Git: Copy some commits. The commits to copy are the ones that are "on"—technically, reachable from—my current branch, minus any commits that are also "on" the name v2. The place to put them is after the commit identified by the name v1.

  • The name v2 names commit H, which names commit G and so on backwards. So these commits won't be copied.
  • The current branch name, test, names commit K, which names J which names I which names H, so therefore I-J-K are the ones to copy.
  • The --onto v1 tells Git where to put the copies: after commit F, as named by v1.

At the end of the copying process, Git yanks the label test (your current branch) off commit K and makes it point to the copy K' instead.

git push now requires --force

Since you already sent commit I to origin, they have:

...--F   <-- tag: v1
      \
       G--H   <-- tag: v2
           \
            I   <-- test

as their set of commits and their branch and tag names. Though, of course, if you sent J and K since then, their test points to commit K. For now we can just assume theirs points to I and that they don't have J and K, because if they do have them and their test points to their copy of K

Note that the hash IDs—and the underlying commits themselves—are the same in every Git repository. This is why the hash IDs are cryptographic checksums of the contents! As long as you and they have the same contents, you have the same commit, and it therefore has the same hash ID. If you have different hash IDs, you have different contents, and different commits. All we need to know is: do you have this hash ID?

If you have not abused the tag names, those are also the same in all Git repos: the same names identify the same commits. But the branch names differ, on purpose, because Git is built to keep adding new commits.

So now that you have run git rebase and abandoned three old commits I-J-K, now you will run git push origin test again. This will send them I'-J'-K'—your new commits, that you have that they don't, which your Git and their Git can tell by hash ID alone—and then ask them to move their test from wherever it points now in their repository—to I or maybe to K. You're asking them to move it to point to K'.

They will say no. The reason they'll say no is that their Git will see that moving their test from I or K, whichever it is set to now, to K' is going to abandon commit I (and J and Kif they have those). So instead of politely asking the other Git, the one atorigin, to please if it's OK updatetestto point toK'`, you need to do:

git push --force origin test

to tell them: Move your test to point to K', even if that abandons some commits!

(They can still say "no", but generally, if it's your branch that you're telling them to force, they should allow it.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks @torek for the explanation. Does help me alot :) – Derek Lee Feb 10 '19 at 22:47
  • does the `git rebase --onto v1 v2` changes my local files as well? meaning overwrite my local files to v1 such as removing files created by v2, readding files which are created by v1. @torek – Derek Lee Feb 10 '19 at 23:47
  • You need a quick tutorial on the fact that Git isn't really based on *files* at all. Files are an OS concept, that Git just has to work around. Git is more interested in *content*. So files in your work-tree are barely used by Git: they are only for the `git checkout` and `git add` steps. They are only there to satisfy the OS's need to work with files, instead of directly with content. – torek Feb 11 '19 at 00:45
  • 1
    See, e.g., https://stackoverflow.com/q/3689838/1256452 or https://stackoverflow.com/q/20642980/1256452 for instance. – torek Feb 11 '19 at 00:50
  • Thanks @torek..gotta brush up on my git knowledge :) – Derek Lee Feb 11 '19 at 00:51