user7252501
user7252501

Reputation: 51

How to restore previous version from git?

enter image description here

I want to get back my this commit version project. How can I get it back. I tried many commands from StackOverflow but it's not working.

I can retrieve specific commit files but I can't push it. When I try to push its showing error , like nothing to commit, or everything up to date

Upvotes: 2

Views: 6136

Answers (2)

Huub
Huub

Reputation: 87

Complex reversion after different commits is not that easy so what I usually do to 'revert' (parts of) the develop branch to some state in the past is: check out that point in time in a new branch, select (only) the lost code and paste it back in the current develop branch.

Upvotes: 0

torek
torek

Reputation: 489828

With certain exceptions (shallow and single-branch clones), every clone of a Git repository has or will have every commit. If you want to access an old commit—all of it—you just use git checkout with the raw hash ID. If you have some other clone of the same repository, and in that repository you want to access that same commit, you just use git checkout with the same raw hash ID.

The git fetch and git push operations are used to add commits to a repository. You connect your repository to some other clone of the same repository, and then you have your Git get commits from them—git fetch—or give commits to them: git push. Once you have done this appropriately, your repository and their repository are back in sync so that once again, everyone has every commit.

This is literally half the point of commits, in Git: that everyone has them. (The other half is that git checkout of some other commit removes all traces of the current commit and replaces all of those files with the files from the other commit youv'e just switched to, as an all-or-nothing deal. There are, as always, various exceptions to these rules, but those are the basic points of commits. Each one stores a full snapshot of every file, and when you use one, you get all the files. You either have the commit or you don't, and if you don't, you connect your Git to another Git that does have the commit and get it.)

What I think you're trying to say here is something else entirely. Let me rephrase it this way:

  • I have, in my repository, a bunch of commits.
  • The one at the tip of some branch (named main or master or develop or whatever) is not good.
  • A historic one (9a15d6e...) is good. If I check this one out and run my tests, everything is fine.
  • Some other Git user, over whom I have little control, insists on using their Git repository to check out a commit by their name main or master or develop or whatever. I'd like to convince them, in some manner or other, to do one of two things:
    1. check out 9a15d6e... when they use this name; or
    2. check out a new commit—whose hash ID does not yet exist—that has those files in it, when they use this name.

If this is the case then there are two ways to make this happen:

  1. Make them treat their name as meaning commit 9a15d6e.... Or:
  2. Make a new commit that you can add on to their name that contains the right files.

Remember that they, whoever they are, are going to blindly use their name to pick a commit hash ID and check out that commit and thereby use those files. So either way, your real goal here is to update their name. The key to understanding this is that while every Git repository has every commit, each Git repository has its own branch names to use when finding commits. Your branch names are yours; their branch names are theirs. Git finds a commit by its hash ID—always!—but Git often finds the hash ID by using a branch name. That branch name is local to that repository.

This is what I mean by the phrase Git is about commits. Git isn't about branch names. Branch names are useful because they help us (and Git) find hash IDs. Git isn't about files either, though commits contain files. It's really about the commits. Each one is a full archive of every file, and we give Git a hash ID and say "check this commit out" and it extracts all the files from that commit.

Make them use a particular hash ID for a particular name

I mentioned above that git push is how you give commits to some other Git. That's true, but incomplete. It's also how you ask or command that other Git to set some of its names—usually branch names, but tag and other names work here as well—so that they find some particular commit by hash ID.

The git push operation has to end with this, because any Git repository is always going to be using branch names (or sometimes tag or other names) to find commits. Giving them some new-to-them commit(s) doesn't do any good unless we also give them some way to find those commits.

Now, we also know—from some things I haven't mentioned here—that no existing commit can ever be changed (this is why they have such big ugly hash IDs: the hash IDs have to be both unique, and good forever) and that Git is built to add new commits. It's possible to yank commits off the end of a branch, but:

  • some Git repositories are set up to forbid this with git push; and
  • even when it's not forbidden, it's not always a good idea.

That aside, we can tell some other Git repository, using git push --force or git push --force-with-lease1 that it should, regardless of whether this is throwing commits away, force some branch name to remember some particular commit by hash ID:

git push --force origin 9a15d6e:develop

for instance. This tries to force their Git to set their develop to 9a15d6e (after giving them commit 9a15d6e if needed of course). They may refuse! If so, you can't use this option. If not, you can, but maybe you shouldn't. This is the easiest option though. It may or may not be a good one, but it's definitely the easiest.


1The force-with-lease option is just a slightly safer variant of --force. We won't cover this in any detail here: the drawbacks are mostly the same as with --force, including that the recipient may just say hell no anyway, or—if they accept the force-push—it doesn't work, or creates future problems, or whatever.


When this isn't a good idea, we need to make new commits

Force-push is easy, but is usually a bad idea. So why did I even tell you about it? Well, for two reasons: One, it's easy. Two, it illustrates how Git works. A branch name finds a commit. With git push --force, we command some other Git to set one of its branch names to find one particular commit, regardless of what commit it found in the past. It's good to know that this is how that works!

What Git likes, though—the kind of git push that doesn't cause problems, and does not need the --force flag—is when we add new commits. Adding a new commit means that we add a new full snapshot. The existing commits continue to exist: the old snapshots are all still good. But now there's a newer commit with a new snapshot. Why is this a good plan? For the same reason you've posted your question in the first place: if the new commit doesn't work out, we still have the old ones.

So, let's say we have two Git repositories that were in sync, and maybe even are in sync right now. If they're not in sync right now, let's get them in sync: we'll use git fetch on our side to get any commits that they have that we are missing. Now we have all their commits.

Now let's use one of our branch names to get in sync with their branch names. That is, we'll make our develop or master or whatever name the same commit as their develop or master or whatever. We don't have to use the same branch name—our Git's names are ours and their Git's names are theirs—but it helps with our own sanity.

So now we have, as the last commit in our develop, some commit that's not the good 9a15d6e... commit. Perhaps, for instance, we have the eacae49... commit checked out.

Now we can make new commits. We can make one new commit, or two, or five, or a million—however many it takes. What we want, in the end, is to have the snapshot for our final new commit to match that in the good commit 9a15d6e....

There are multiple ways to get the result we want, and no one particular way is the best way. Given the image you posted, I'm going to show two ways to get there. You might find one of these easier than the other, but different people will find different ones easier. Some people will claim that one of the two is better, and other people will claim that the other of the two is better. The "best" one for you and them is, in the end, whatever makes both you and them happiest. Git won't care one way or the other.

  • Method 1: git revert.

    The git revert commit has one relatively simple job:2 it takes your existing committed state, plus the specification of some existing commit that you select, and tries to back out the commit you selected. In other words, we ask Git to figure out what changed in some particular commit, and then to undo that one particular change.

    In your case, the good commit 9a15d6e... is followed by two commits that, apparently, made things worse: first there was 3dab7c7... updated, then eacae49... update made things even worse. So you'd like your Git to back these out. For various reasons, it's good to back them out in reverse: first, we want to "undo" eacae49... update, then we want to "undo" 3dab7c7....

    You can do this with two git revert commands:3

    git revert eacae49
    git revert 3dab7c7
    

    The first one has Git back out the specified commit's changes, then make a new commit that has, as its message, "revert commit eacae49...". The second git revert has Git back out the specified commit's changes as well, making another new commit.

    In the end, this adds on two more commits whose effect is to undo the two "bad" commits. So now you have things back to the way they were when all was OK. And, since this merely adds new commits, you can now git push these two new commits to the other Git. They will add them on, and update their branch name to remember the last of the two new commits, and then they will see that as their latest.

  • Method 2: make a new commit that matches some old commit

    The revert method works really well for one commit. It's OK for two commits, or three or four, but when you get to dozens of commits, it can be a pain to use. There are some tricks to make it less painful (e.g., git revert -n and/or en-masse multiple reverts) but sometimes we'd just like to say: make a new commit that matches some existing commit.

    There are multiple ways to do this. The shortest and fastest one is to use git commit-tree, but that's especially tricky and requires a git merge --ff-only as well. The dumb-but-straightforward way is the simplest to show, and that's what I will show here.

    To get what we want, we start by telling Git to remove everything. We are not going to commit this removal—and in a lot of cases this step isn't needed anyway—but the effect here, and the reason we're doing it at all, is to make sure we don't leave any new files around from the bad commits, that aren't in the old good commit:

    rm -rf .
    

    Note that this command has to be run from the top of your working tree.

    Now that all of the bad commit's files are gone, we want to fill in our working tree—and Git's index / staging-area, which I haven't mentioned until now—from the good commit, like this:

    git restore --source=9a15d6e --staged --worktree .
    

    or, if you don't have Git 2.23 or later, or just prefer a shorter command-line command:

    git checkout 9a15d6e -- .
    

    As before, this has to be run from the top of your working tree. This checkout (or restore) tells Git that it should take all of the files from the specified (good) commit, and copy them into your working tree area—so that you can see them—and the staging area, so that they will be in the next commit.

    You can now run git commit. The new commit, which will get a new unique hash ID, will have, as its snapshot, the same files as were in 9a15d6e.

Whether you use Method 1 or Method 2, the result is to add a new commit whose files match those of the "good" commit you want to go back to. Since this new commit is new, it's easy to add it to their Git repository, with a regular everyday git push. When you do that, it becomes the newest commit on your branch and the newest commit on their branch too—and since it's the same files as the commit you wanted, it has the same function as the commit you wanted. But it's still a new commit: you've just gone back to the working version.

The ability to do this sort of thing—to go back to working versions, to revert one bad commit among a series of good ones, to cherry-pick a fix to some old version of your software, and to see how your code has evolved over time—is why we use version control systems in the first place. Because Git is a distributed version control system and sets things up so that everyone has a full copy of every version of every file, it gets a bit complicated, but the why still applies.


2As with Most Things Git, git revert can take on more jobs, and is less simple than it first appears, but let's just not worry about that here.

3You can actually do it with one git revert. Git will undo both in the right order. But it's better to learn a bit at a time here.

Upvotes: 2

Related Questions