Home > OS >  Not sure if I lost my local changes, how to get it back and push to a specified remote branch
Not sure if I lost my local changes, how to get it back and push to a specified remote branch

Time:03-14

I have a feeling that I overwritten my local changes with what's in remote...how can I recover it?

I have a remote branch develop. I also have a local branch which wasn't a git repo.

I made my local directory a git repo by doing git init. I want to add everything I have locally to the remote develop branch so I tried:

git remote add origin git@github.....
git fetch
git add .
git commit -m "some comment"
git status

but it tells me:

On branch master
nothing to commit, working tree clean

which I found weird.. so I tried

git push origin develop

But I got the error:

error: src refspec develop does not match any
error: failed to push some refs to '[email protected]:.....'

So I thought I needed to be on develop so I did:

git checkout develop
git push

But then it tells me everything is up to date

But when I go to github's develop branch I don't see the changes I've committed.

I tried to undo the git fetch from what is shown here: How to undo 'git fetch'

so I did git remote remove origin but that did nothing.

What did I do wrong and how can I get my original changes back so I can push it to develop in remote?

UPDATE:

From git reflog I see:

a1be24c (HEAD -> develop) HEAD@{0}: checkout: moving from master to develop
771b082 (master) HEAD@{1}: checkout: moving from develop to master
a1be24c (HEAD -> develop) HEAD@{2}: checkout: moving from master to develop
771b082 (master) HEAD@{3}: commit (initial): trying to keep develop up to date ..

I see 771b082 (master) HEAD@{3}: commit (initial): trying to keep develop up to date .. is my first commit. I think that is what I need to get back my local source code.

CodePudding user response:

git reflog

git reflog shows you a list of every commit you've visited recently. Even if you deleted the local branch, you can still go to the latest commit and then create a branch at that location.

So, do that first.

Once that's done, you can figure out whether you need to do any branch surgery to integrate your work with the rest of the repo.

CodePudding user response:

TL;DR

You don't have to search. Your master branch contains your one commit. But you *do *have to do something about the way you made it, which isn't useful.

Exactly what you want to do will depend on a lot of things, and really needs a much longer discussion. My guess is that you will want to start with git worktree add. See the "how to recover" section below (but not until you understand the long part).

Long

771b082 (master) HEAD@{3}: commit (initial): trying to keep develop up to date ..

The commit (initial) is the real clue here. What this means is that you created a local repository (git init), which put you on your own branch named master. This branch did not exist yet. This is a confusing state of affairs; see below.

You then ran:

git remote add origin git@github.....
git fetch

This added a remote: a short name, in this case origin, for a way for your Git software to reach out to another separate Git repository. The git fetch then did reach out to this other Git repository (successfully, apparently) and got from it, all of its commits, and none of its branches. When git fetch runs, your Git software gets to see all of their branch names, but instead of just copying their branch names to create or update your own branches—which would be bad: see more below—your Git software then renames each of their branch names. Their develop, for instance, becomes your origin/develop. This name with origin/ in front of it—the origin part comes from the name origin in git remote add origin ...—is a remote-tracking name, which your Git software uses to remember, in your repository, their repository's branch names.

So you now have all of their commits, and none of their branches: instead, for each of their branch names, you have a remote-tracking name. That's all git fetch does (get any of their commits that are new to you, and update your remote-tracking names) so git fetch is now finished.

Then you ran:

git add .
git commit -m "trying to keep develop up to date"

Since you're (still) on your branch master, which still doesn't exist, this added all your working-tree files—none of which are from any of their commits—and made one new commit in your repository, creating your branch named master.

Your:

git status

then showed you on your branch master, with nothing to commit, because you've just committed everything. So your working tree (and Git's index) match your one commit that you just made.

You then attempted git push origin develop, but as you had no branch named develop at this point, that failed. (You did have origin/develop, but that's a remote-tracking name and spelled origin/develop, not develop). Then you ran:

git checkout develop

Since you had no branch named develop at this point, you might expect an error, but both git checkout and git switch have a special feature here. If you don't tell Git not to do this with --no-guess, Git will now guess what you meant to do. Git's guess will search for a remote-tracking name that ends in develop. You have one! It's origin/develop. So the search succeeds and finds exactly one. Git's guessing code then guesses that you meant to run:

git checkout -b develop --track origin/develop

So your Git now creates a new branch name, develop, pointing to the same commit as your origin/develop. You now have a branch named develop that you can switch to, and git checkout now does that.

Your develop is now even with your origin/develop, of course. It's entirely separate from your own master, and contains no commits from your master. If, at this point, you were to run:

git log --all --decorate --graph

(without --oneline as it's deceptive in this case) you would see two disjoint graphs, one for develop / origin/develop (both names select the same commit), and one for master containing one commit.

What's going on here, and why this is so confusing

Git is, really, all about commits. Git is not about files, although commits hold files, and Git is not about branches either, although branch names help you (and Git) find commits. It's really all about the commits.

Every commit in a repository is numbered, with a universally unique hash ID. If two Git repositories have two commits that have the same number, they must necessarily have the same commit. So if your Git repository has a commit with number a1be24c (abbreviated: it's actually 40 hexadecimal digits long), and their Git repository has a commit with the same number, you and they have the same commit. You got it from them, or they got it from you, or both of you got it from a third Git repository perhaps, but one way or another, you both have the same commit.

This means your Git repository and their Git repository can just compare these hash IDs to see who has what. Any commits they have, that you don't, you'll get with git fetch. Any commits you have that they don't, you can give them with git push (though you won't necessarily give them everything you have that they don't, as git push is more complicated than git fetch here, the way we'll use it).

Aside from this rather magical numbering trick,1 each commit contains two things:

  • Any given commit contains a full snapshot of every file Git knew about at the time you, or whoever, made the commit. For instance, your one commit on your master knew about files from git add ., so those are the files in that commit. These files are in a special, read-only, Git-only, compressed and de-duplicated format, so if you keep recommitting the same version of some file—which is very normal—there's really only one actual copy in the repository. But each such commit "holds" the (shared) copy. (This is safe because all parts of every commit are read-only, which is necessary for the magical numbering trick.)

  • Any given commit also contains some metadata, or information about the commit itself. This includes the name and email address of the author of the commit. It includes some date-and-time stamps. It includes any log message you gave (git commit -m ...). And—crucially for Git's own operation—each commit stores, in this metadata, a list of previous commit hash IDs. This list is usually exactly one element long, but for an "initial" or "root" commit it's empty.

If we look at how this works in a less-confusing situation than an initial, totally-empty repository, we see something like this. Let's draw commits using single uppercase letters such as H to stand in for hash IDs:

            <-H

Here, H is the last commit in our repository, on the main or master branch for instance (we'll assume just one branch name at this point rather than the two or three we more often see). Being a commit, H holds both a snapshot and metadata. The metadata tells us who made H, but importantly, it holds the hash ID of the previous commit, which we draw as a backwards-pointing arrow. This pretend-arrow "points to" the previous or parent commit, which has some random-looking hash ID, but we'll call it G:

        <-G <-H

Of course, G is a commit, so it has a snapshot and metadata, and the metadata include a single backwards-pointing arrow (a single hash ID) that lets Git find still-earlier commit F, which is G's parent:

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

This just keeps going and going, except that eventually it has to stop, because some commit (commit A) was the very first commit. Commit A has no parent, because there's no previous commit:

A <-B <-C ... <-H

The way Git really works is that it starts from the end, at commit H, and works backwards. When it reaches the beginning—a root commit with no parent—it finally gets to stop.

For Git to find commit H, something—perhaps you, the human—has to provide to Git the raw hash ID of commit H. Memorizing hash IDs is a ridiculous task, of course, but it's also completely unnecessary. We have a computer: we can just have the computer memorize the last hash ID. This is where branch names come in.


1How does your Git software know, at the time you run git commit, that the hash ID it assigns to your new commit is different from every other existing hash ID everywhere? The answer is that it doesn't: it relies on probability. And yet, this actually works in practice.


Branch names point to last commits

Just as commits point backwards to their parent commits, by holding the parent commit's hash ID, a branch name points to a last commit by holding the hash ID. So we can extend our drawing a bit:

...--G--H   <-- master

Now instead of having to memorize some hash ID, we can just give Git the name master. That's a branch name and by definition, whatever hash ID is inside the name is the last commit "on" that branch.

Let's suppose now that we decide to create one new branch name br1, with git branch br1 or git checkout -b br1 or git switch -c br1. The last two do the same thing, while the first one—git branch br1—creates the name without switching to it. Either way, the new name also points to commit H right now, like this:

...--G--H   <-- br1, master

We now need a way to tell which name we're using to find commit H. The way Git does this is to attach the special name HEAD to one of the two branch names:

...--G--H   <-- br1, master (HEAD)

This means we're "on" master; if we now run git checkout br1 we get:

...--G--H   <-- br1 (HEAD), master

which means we're "on" br1. Either way we're using commit H, so for the moment it wouldn't matter which branch we're on. But now let's create a new commit (e.g., by changing a file, using git add, and running git commit—this will create a new snapshot, reusing the unchanged files, and adding a new version of the one changed file).2

Anyway, to make the new commit, Git saves out the snapshot, adds the metadata—including hash ID H, which is that of the current commit—and writes all of this out as the new commit, which assigns the new commit its new, unique hash ID; we'll just call it "commit I":

          I
         /
...--G--H

Since I is now the latest commit "on" whichever branch we're on, Git updates that branch name—the one to which HEAD is attached—to point to new commit I. The other branch names remain unchanged, so if HEAD is attached to br1, we now have:

          I   <-- br1 (HEAD)
         /
...--G--H   <-- master

Note that commits up through H are on both branches, not just master. Commit I is only on br1 at this point, though that could change in the future.

If we now git checkout master, Git will:

  • rip out all the files that go with commit I (they're safely saved, read-only, in commit I so this is safe to do);
  • replace them with all the files that go with commit H (extracted from commit H)

and now we have:

          I   <-- br1
         /
...--G--H   <-- master (HEAD)

If you look at the files in your working tree, you'll see the files from H, rather than those from I. Of course many of them will be exactly the same (you only changed one file), and Git also secretly works hard behind the scenes not to bother to rip out then re-create a file that isn't going to change and that makes the claim that it rips-and-replaces all the files a little too strong, but thinking about it this way works pretty well, to get you started.

If you switch back to br1 you get the commit-I files again:

          I   <-- br1 (HEAD)
         /
...--G--H   <-- master

and if you now make another commit you get:

          I--J   <-- br1 (HEAD)
         /
...--G--H   <-- master

New commit J is the last, and br1 points to J; J points backwards to I, which points backwards to H, and so on.

If you switch back to master and create br2 and switch to br2, you get:

          I--J   <-- br1
         /
...--G--H   <-- br2 (HEAD), master

and if you now create two more new commits you get:

          I--J   <-- br1
         /
...--G--H   <-- master
         \
          K--L   <-- br2

This is how branches work, in Git.


2Note that git add winds up:

  • compressing the file;
  • checking for duplicates;
  • if a duplicate, discarding the newly compressed file and using the original;
  • if not a duplicate, arranging for the newly compressed file to be truly added, and using that.

So it doesn't hurt to git add an unchanged file. It does take a bunch of extra CPU work though, so Git will try to guess at cases where it can pretend to have git add-ed a file without actually bothering. In a few very rare cases you may find that you must override this pretending, with, e.g., git add --renormalize.


Remote-tracking names find commits just like branch names

Let's suppose we have a repository with this in it:

          I--J   <-- dev (HEAD), origin/dev
         /
...--G--H   <-- master, origin/master

You might get this by cloning a repository that has both master and dev on it (creating your origin/master and your origin/dev) and then doing a git checkout dev, which creates your dev pointing to the same commit as their dev, i.e., as your origin/dev.

Now we wait a day (or an hour, or a second, or some time interval), long enough for them to add two commits to their dev. Then we run git fetch. Our Git reaches out to their Git software and their repository and finds two now commits K-L that they have that we don't, so our git fetch gets those commits. Meanwhile their dev now points to (shared) commit L. So our Git software now updates our origin/dev to remember where their dev points now:

               K--L   <-- origin/dev
              /
          I--J   <-- dev (HEAD)
         /
...--G--H   <-- master, origin/master

Our dev has fallen behind, not because of something we did—we did nothing at all—but because they have added commits to their dev. We might now want to force our own dev to move forward to L, to match their dev. (To keep this answer from getting ridiculously long, I'll skip over how to do that. Just note that it looks like there are separate dev branches, dev and origin/dev. And there sort of are: it all depends on what we mean by branch. Is a branch "a commit pointed to by a name", or is it a branch name in our repository, or is it some collection of commits, or what? See also What exactly do we mean by "branch"? Humans use the word ambiguously, so you have to ask what someone—sometimes even yourself—means when you hear "branch".)

The weirdness of a new, totally empty repository

In Git, a branch name must point to exactly one commit. But in a new, empty branch, we have no commits (count them: zero, zilch, nada, none). So we can't have any branch names either.

Nonetheless, Git can attach HEAD to a non-existent branch name, something like this:

master (HEAD)

(The word master might be greyed-out, if I could do that.)

Whenever a Git repository is in this state, with HEAD attached to a branch name that doesn't exist, the next commit you make will be a root commit (or an "initial" commit as your reflog shows). So if we now add some files and git add and git commit, we get the very first commit, which we can call A:

A   <-- master (HEAD)

The new commit has no parent, making it a root commit. The name master now springs into being as a real branch name, and HEAD remains attached to it. We're now out of that crazy setup.

We can now make new commits and they'll string along in chains, as we would expect:

A--B--C   <-- master (HEAD)

We can, however, put Git back into this weird situation, even though there are commits, by attaching HEAD to a new nonexistent branch name. We use git checkout --orphan to do this (since Git 1.7.2), or git switch --orphan (since Git 2.23). These two are subtly different, so be careful, but you would almost never want to use either one, so we won't cover the difference; we'll just illustrate the effect if we were to do one of those with the name disconnected and then create some more commits:

A--B--C   <-- master

D--E   <-- disconnected (HEAD)

That is, the latest commit on disconnected is now commit E, whose parent is D, but commit D is a root commit: the going-backwards action stops here. If we now git checkout master or git switch master, Git will remove all the commit-E files and extract all the commit-C files, and git log will start at commit C and work backwards to B and then A and then stop.

The history in a commit repository is the commits in the repository

History, in Git, is nothing but the commits. We find the commits from branch or other (e.g., remote-tracking) names, which find a last commit, and from there, we have Git work backwards. Using git log --graph, Git will draw the connections from each child commit to its parent(s).

By using trickery like --orphan, we can create multiple separate disconnected graphs. They're not very useful, but the theory behind Git allows them, and Git allows them. It kind of has to, because there's another way to create them, and that's what you stumbled into.

What you did

If we:

  • create a new, empty repository with git init, so that we're on master (or even main);
  • use git remote add and git fetch to populate the repository with commits and remote-tracking names;
  • create and add some files and commit; and
  • use the --guess mode to create a develop branch pointing to their develop which our origin/develop ...

then we will get a single-commit master branch with one root commit on it, plus our own develop matching their origin/develop. We'll see, in our work area, their develop files; our master-branch-single-commit files will not be visible anywhere.

We will have something like this:

          I--J   <-- develop (HEAD), origin/develop
         /
...--G--H   <-- origin/main

K   <-- master

depending on whether they have a main or a master (here I've called it main so that we have origin/main).

Commit K is kind of useless, but it does have a bunch of files we might like to be able to see.

How to recover

We could try to run:

git cherry-pick master

right now, to add those files to the files we have in our working tree, but this is likely to produce a lot of add/add conflicts. The reason is that git cherry-pick tries to take the changes from "their" commit (our K in the drawing above), and those "changes" are "add all the files that exist in K from scratch".3 Git tries to combine them with our "changes", which at this point will be "add all the files in J" from scratch. The result tends to be one giant, massive add/add conflict: not useful.

More useful, probably, is to extract all the files from commit K into ordinary everyday files, which you can then open in your editor. To do this, you have a lot of options; the two easiest are:

  • git archive master will produce a tar or zip archive of all the files in commit K, which you can extract in any way you like; or
  • git worktree add ../master will add a new working tree in which the files from branch master are checked out in a new directory ../master (you should be at the top of your working tree when you run this).

The git worktree add method creates this working tree and fills it in, so the files are all there for you to see, and you can even do work in there and commit (to add on to your master) if that's easier than working here in the develop branch.

Once the files are really ready in ../master/*, you can copy them here and run git diff and/or git add as desired, and then git commit to make a new commit that adds on to commit J:

               L   <-- develop (HEAD)
              /
          I--J   <-- origin/develop
         /
...--G--H   <-- origin/main

K   <-- master

When you're totally satisfied with commit L and have successfully used git push origin develop to send it to the Git repository over at origin, you can delete the added working tree (see git worktree remove) and then delete your master branch, which you don't need any more now that you don't need commit K.

Note: commit K will remain in your own repository for some time (at least up to a month by default), in case you change your mind and want it back. To get it back you must find its hash ID somehow (e.g., via git reflog), and then create a branch or tag name to remember that commit hash ID. But if you don't ever want it back, after enough time expires, Git will notice that you not only can't see it normally, you've never gone back to find it, and will throw it out for real. (This assumes you never git push it somewhere else: if you do that, it's much harder to get rid of forever. Commits are like viruses: once they've infected a couple of repositories, you can't stamp them out easily.)


3The trick here is that Git is comparing commit K to its non-existent parent. To do that, Git pretends there's a completely-empty commit, using Git's empty tree to fake it; that makes all files "newly added".

  • Related