0

I have a local_branch which has local changes , I want my main branch to merge into it. But when I do “git merge origin/main” it shows me already upto date . But I am sure I am not as I can see some changes in main that I don’t have locally. It could also be that I accidentally resolved conflicts to mine and that’s why it is showing already up to date. How can I get out of this situation ? I want my local branch to merge the main branch

ct2048
  • 1

2 Answers2

1

I have a local_branch which has local changes, I want my main branch to merge into it. But when I do “git merge origin/main” it shows me already up to date ...

That means exactly what it says: you've already merged origin/main, so there's no work left to do.

It's worth noting a number of things here:

  • First, git merge does not mean make same. If it did that, we would lose work! Instead, it means combine work.

  • Second, the definition of work and combine are tricky. The "combine" part isn't that hard once you figure out what it means to "find work that has been done", but that part is kind of hard.

  • Last, the "direction" of the merge matters somewhat, although less than most people think at first. We'll see this below.

To understand why the definition of "work" is tricky, let's start with some very basic things you need to know about Git.

Git is about commits

Those new to Git often think that Git is about files, and it's not. It's about commits. Each commit holds files—if they didn't, Git would be useless—but what Git really cares about here is the commit. You get all of a commit, or none of it: you never get half a commit.1

Those new to Git often think that Git is about branches, and it's not. It's about commits. Git organizes commits into branches, and we (humans) use branch names to have Git help us find the commits, but again, what Git really cares about here is the commits.

Aside: I also find that branch is a bad word in Git—not bad as in the kind of "four-letter word" profanity, but rather, bad as in nobody knows at first what you mean when you say branch, in the same way that bad word brings up profanity, or how "bad" sometimes means "good". That is, the word branch has multiple meanings in Git, and people will use this word in several sentences—or several times in one sentence—with each occurrence meaning something different!

So, okay, Git is about commits. It's possible to use Git without having any branch names at all (but this is not a good idea, at least not if you're a human). This means we need to know exactly what a commit is and does for you. Let me start with this though:

  • A Git repository is, at its heart, a collection of two databases, one usually much bigger than the other.

  • The usually-bigger database holds Git's commits and other Git objects (supporting objects that let Git hold onto commits and/or make commits work better). Everything in this big database is read-only, and the database itself is mostly (though not 100%) append-only. That is, commits don't normally go away, and no commit, once made, can ever change. Instead, we just add new ones.

  • The objects in this big database are numbered, with big, ugly, random-looking hash IDs or (more formally) Git object IDs or OIDs. These were exclusively SHA-1 hashes at one time (that's no longer true) so old documentation will call these SHAs and the like, too. Git literally needs the hash ID (the OID) to find the object. So every commit has a unique hash ID; that hash ID, in a sense, is the commit.

  • The (usually smaller) second database holds names: branch names, tag names, remote-tracking names, and lots of other kinds of names. Git provides these for us humans, for whom hash IDs are unusable except via cut-and-paste with the mouse or whatever. Instead, we get to use things like branch names: a branch name like main or develop translates into a (one, single) hash ID, which—for a branch name—is defined as the latest commit that is "on" that branch.

Hence, by using these two databases, we can give Git a name (local_branch for instance, or origin/main). Git will use that name to look up a commit hash ID as needed. Git then uses the hash ID to look up the actual commit in the big all-Git-objects database.

When I say the commit's hash ID is unique, I really mean unique: we will "clone" a Git repository with git clone, for instance, and when we do, we copy all the Git objects from the other repository's database. These copies have the same OIDs as the originals. (That's why the objects are read-only: the OIDs are cryptographic checksums of the data. Change the data, and you've changed the checksum and have a different object. Since the objects database is largely append-only, all you have done is add a new object: the old one is still there, under the old ID.)

So that's what we need to know about the databases:

  • The big one holds objects indexed by their hash IDs, including the commits. That's how Git finds any specific commit: you give Git the hash ID and Git simply looks it up. It's either there—the whole thing is there, never just some part of it—or it's not, and because it can't change, if you have a commit with some known hash ID, and someone else has something in some other Git repository with the same hash ID, you both necessarily have the same commit.2

  • The little one means that most of the time, we don't have to use a raw hash ID: we can use a name and Git will look up the raw hash ID for us.

Now let's move on to the commit. A commit, in Git:

  • Is numbered, as we just saw.
  • Contains two things: a full snapshot of every file, and some metadata, or information about the commit itself.

On hearing that every commit has a full snapshot of every file, people often object: Won't that make the repository grossly huge? It would, except that:

  • the files in the snapshot are compressed, sometimes highly compressed; and
  • the files are de-duplicated, which in many ways is even more important.

(Git achieves the de-duplication by storing each file's content as an object in the big objects database. The high-degree-of-compression happens later in the game when objects become packed into a "pack file", which we won't cover at all here, as none of this matters for simply using Git.)

While the files are what we care about when we go to do work, right at the moment, it's actually the metadata we need to care about. The metadata of any one given commit contain, among other things:

  • a user name and email address for the person who made the commit;
  • a date-and-time stamp;
  • a log message, which git log can show or summarize; and
  • crucially for Git's operation, a list of previous commit hash IDs.

This list is usually exactly one entry long. The (single) commit hash ID thus stored in any given commit is called the parent of the commit.

This trick of storing a parent commit hash ID in each commit means that if we can find the last commit in some chain of commits, we can use that to work backwards. Suppose H stands for the hash ID of the last commit in some chain of commits. We say that the hash ID stored in H points to the previous commit—let's call it G for simplicity. We can draw this like so:

        <-G <-H

That is, commit H is literally pointing backwards to commit G. Of course, G is a commit too, so it too has a parent, which we can call F:

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

and since F has a parent, Git can start at H and work back to G and then F and so on. Eventually, all this working-backwards will come to the very first commit ever, which won't have a parent, because it can't. Its list of parents, in its metadata, will be empty, and that gives us the whole chain:

A--B--C--D--E--F--G--H

(our repository apparently has just eight commits in it).

How do we find H? We already know: we use a branch name like main to do that:

A--B--C--D--E--F--G--H   <-- main

Note that I have gotten a bit lazy about drawing the arrows between commits. That's partly because they literally can't change—they're metadata in the commits, and no part of any commit can ever change—and also because of the more complicated graph I'm about to draw.


1A still-being-developed feature called partial clone lets you get "half" (or some fragment of) a commit, with a promise that the rest will appear later if/when needed. This does a lot of damage to the way you should think about Git initially, so let's pretend it doesn't exist. It's not really ready for the average user yet anyway. Once it is, I believe it will be pretty seamless—that is, you won't have to be aware that it even exists—except for the fact that if your network is down and you have a partial commit when you need a full one, your Git will simply stop working at that point. If your network is up, your Git software will just get slower (and maybe print out progress messages about downloading stuff).

2This literally can't work forever, and won't. In theory, the size of the hash ID puts the day of failure far enough into the future that we don't care. There's a flaw in this theory, but it is being addressed.


Branches, of several kinds

Suppose we take our simple, relatively new Git repository with a mere eight commits and one branch name main, and add two more branch names, br1, and br2. A branch name in Git must store exactly one hash ID; that's the hash ID of the commit that is the last commit "on" or "in" the branch. If we make all three names point to H, we get:

...--G--H   <-- br1, br2, main

Right now, it doesn't matter which name we use, since all three point to H. But in a moment that won't be true. To remember which name we're using, let's attach the special name HEAD, written in all uppercase like this, to exactly one branch name:

...--G--H   <-- br1, br2, main (HEAD)

This says we're using the name main to find commit H.

If we now run:

git switch br1     # or git checkout br1

we're telling Git to switch from branch main, i.e., commit H, to branch br1, i.e., commit H. We aren't changing commits—which we'll see in a moment is pretty important—but we are changing branches. We now have:

...--G--H   <-- br1 (HEAD), br2, main

Let's make a new commit now. We'll come back in a moment to some important things about making new commits, but for now let's just assume that this makes a new snapshot-and-metadata (it does) and let's call this new commit I. Git will arrange for new commit I to point backwards to existing commit H:

          I
         /
...--G--H

and then, as the last step of git commit, Git will write I's hash ID, whatever that is, into the name br1 so that we have:

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

That is, the current branch name—which we find by seeing which name HEAD is attached to—gets updated to point to the new commit. The new commit is now the last commit on the branch. If we make another new commit, we get:

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

Commits I-J are only "on" branch br1 right now. Commits up through H are on all three branches right now.

I keep saying right now because, in part:

  • we can, at any time, add new names, using git branch for instance;
  • we can, at any time, delete branch names, also using git branch (with -dor-D`); and
  • we can, at any time, move any branch name, using either git branch or git reset with various options.

Whatever commit a branch name points to, that's the last commit that is in that branch. Other commits that we can find by moving backwards, through the parents, are also "in" the branch, and many commits are generally in many branches, with the very first commit usually being in every branch.

Anyway, let's git switch br2 now, or git checkout br2, and run git branch -d main, to get this:

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

The name main is completely gone (this makes it easier to draw the graph, there's no particular other reason to remove it here), but we don't need it: it just gives us a fast way to find commit H. We have a slow way: we can find commit J, using name br1, and then work backwards two hops to I and then H. And we have another fast way, right now: the name br2 finds H.

Note that at this point, the files we had for commit J vanish, and are replaced with the files for commit H. Changes we made seem to be gone! But they're not gone: there's a full snapshot in commit I, and another in commit J, and if we git switch br2 we get those files back (and the files from H vanish). It's time to talk, briefly, about your working tree and Git's index.

Git's index and your working tree

We'll skim this quickly for space reasons, but it's important to know: A standard ("non-bare") repository includes a working tree or work-tree. The repository proper is found inside the hidden .git directory (or folder, if you prefer that term) that you can find at the top of this working tree. Inside this .git there are various control and auxiliary files that implement the two big databases and all the other stuff that Git wants to have. You may look inside them, but in general you shouldn't change any of these files unless you really know what you're doing: Git is very fussy about its files in the .git repository proper. It's a bad idea to store this in a cloud-synced folder, too, because cloud-syncing software and Git have very different ideas about what's important. Cloud-syncing software will eventually damage a Git repository (and Murphy's Law means this usually happens right before some big demo). Don't do it; sync Git repositories using git fetch and git push.

The working tree, by contrast, holds the files that you see and work on / with. We mentioned that a commit stores a full snapshot of every file (that Git knows about, at the time you, or whoever, make the snapshot), and that what's in a commit is in a special format that only Git can read and literally nothing can overwrite (not successfully, that is: physically overwriting will just damage the data since the hash ID won't match up any more: this is one way cloud syncing software breaks a repository). But to get work done, we need regular (non-Git-ified) files that programs can read and write. Git puts those files into our working tree: they can be copied out of a commit, like extracting an archive, and then we have ordinary files.

Important: The files in your working tree are not in Git! They may have just come out of Git, but the files that are in Git (in commits) are the special Git-ified ones. These are ordinary files; Git has nothing to do with them while you work on them, and if you remove one, Git can't get that one back. Git can only get back the Git-ified ones in the commits.

If Git were like other version control systems, it would stop here: there would be committed files that are frozen for all time, and there would be your working tree files, which are yours to do with as you please. There would be two copies of each "active" file: the frozen one in the current commit, and the one you're fiddling with. Now and then you'd run git commit and Git would take those working tree files and freeze them. But Git isn't like those other version control systems. That's not how Git goes about making new commits.

Instead, Git has a third area—sort of between the commits and the working tree—where Git stores a third copy, in between the frozen copy in the commit and the usable copy in the working tree. We might call this a "copy" rather than a copy, though. This third "copy", the one in-between, is in something that Git calls the index, or the staging area, or—rarely these days—the cache. The "copy" in the index is in the Git-ified and de-duplicated format, so at least initially, this "copy" never takes any space, because it came out of the current commit.

Remember that we ran git switch br2 or whatever, and that's where we got the commit that Git used to fill in the working tree. Git will remove, from your working tree and from its index, all the files that go with the commit you're moving away from. Git will then place, into its index and your working tree, all the files that go with the commit you're moving to. So when we have:

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

(and everything else is all "clean", as in there's no uncommitted work anywhere), Git's index and your working tree match commit J's snapshot. Then we run:

git switch br2

and Git removes the commit-J files and installs, instead, the commit-H files:

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

and once again, your current commit, Git's index, and your working tree all match.

To make a new commit now, you:

  • modify working tree files;
  • run git add: this updates the proposed new commit snapshot by copying the updated file back into Git's index;3
  • eventually, run git commit.

When you do run git commit, Git packages up the index files—the ones that are in the frozen format, but are replaceable until this point—into a new frozen snapshot, and uses that for the new commit. Git gathers or generates the appropriate metadata as well, and writes all this out and now we have our new commit, which gets a new, unique hash ID, and then git commit stuffs the new hash ID into the current branch name:

          I--J   <-- br1
         /
...--G--H
         \
          K   <-- br2 (HEAD)

We've added another commit as usual, and note that now—since the index and working tree matched after we git add-ed the files, and new commit K matches the index since it was made from the index—the current commit, Git's index, and your working tree all match again.

We can now make another new commit:

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2 (HEAD)

and we have a nice symmetric setup all ready for merging. Note how commits up through and including H are on both branches, while commits I-J and K-L are only on br1 and br2 respectively.


3We'll skip all the details about how this works. It's simple in some ways and complicated in others. The end result is that the index is ready to commit, and holds an updated proposed new commit with the added file having been changed.


git show and git diff and git log -p

Before we get to git merge, let's note one more thing about these commit sequences: they all have snapshots, but if we look at some commit with git show or git log -p, we see a diff. If we run:

git diff <hash-of-I> <hash-of-J>

we see a diff. What are these diffs and why do we see them?

The answers are pretty simple: a diff is a recipe, of sorts. It shows what changed between two snapshots. If you take the changes shown, and apply those changes to the earlier or left-side snapshot, you get the later or right-side snapshot.

Git can in essence play a game of Spot the Difference. We pick one commit, like I, and put it on the left, and another like J and put it in the right, and say: Tell me what changed. Git doesn't store the changes—it stores the whole snapshots—but by comparing the two snapshots, Git can provide a change-recipe: add this here line after line 12 of README.txt for instance. Git shows the change as a series of diff hunks, or in some cases, as a completely new added file, or a completely removed file, or a rename (possibly a rename followed by some changes). We won't cover all these possible cases and will mostly just concern ourselves with the simple diff hunks.

By comparing the files in I and J, Git can show us what changed. That's often much more interesting than the two full snapshots, and that's why git show hash-of-J, or more simply, git show br1, will run this particular git diff. So will git log -p. The diff shows us what we changed, not what's in the snapshot, and that's what we want to see.

Both git show and git log -p use the parent information: to show some particular commit, Git finds the parent, extracts (to a temporary area in memory) each of the two snapshots, and compares them with git diff. (The de-duplication helps a lot here: Git can tell immediately, without even extracting the files themselves, whether two files are 100% identical, and thus not bother to diff them at all.)

But using the command line, we can manually pick out any two commits and run git diff on them. For instance, if we look up the hash ID of commit H—or have some other way to find it (maybe the branch name main, if we didn't delete it)—we can run:

git diff <hash-of-H> br1

and that will show us what changed between commits H and J. This is a summary of all the changes in I and J, and it conveniently eliminates any change made in I that we "undid" in J (e.g., if we inserted a typo, and then fixed it, this doesn't show up in the diff since Git skips right over commit I).

This is going to be our definition of "work". The work done, from some starting-point commit to the end of the branch, is simply a git diff from that starting-point commit to the tip commit of the branch as found by using the branch name.

We can now move on to git merge.

How git merge works, in a nutshell

Let's go back to this repository, but git switch br1:

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

We have commit J checked out, so we see those files. If we run git diff L we'll see what's different between this commit and commit L (backwards, sort of, because this puts L on the left and our working tree on the right). But that's not the work we did, not on br1. There's work that happened on br1, but that's not what this diff shows. And, there's work that happened on br2, too.

What we want from Git is to combine the work we did on br1 with the work we (or someone) did on br2. How do we do that? Well, we already saw that: we find some commit, like commit H, that's on both branches. Every commit up through and including commit H would work here, but there's one that's sort of obviously best, and that's commit H itself. That's where the two branches split apart.

So we'll have Git run:

git diff <hash-of-H> <hash-of-J>   # what we did on br1

and then run:

git diff <hash-of-H> <hash-of-L>   # what they did on br2

These get us "work done", and because we carefully picked the same starting commit, both sets of "work done" can be applied to the snapshot in that starting commit. That is, Git just has to combine the two sets of "work done".

(Note that if we start earlier, at commit G for instance, this still works, but now everything in H is "work done" in both commits. This makes the merge work harder, to no advantage. That's why it's so obvious that commit H is the best shared commit.)

Git's "combining work" happens by doing this:

  • for each file we changed, if they didn't touch it at all, use our version of the file;
  • for each file they changed, if we didn't touch it at all, use their version;
  • for files nobody changed, use any version—all three match;
  • for files both sides changed, work harder.

In the work harder section, Git goes through the diffs: for each hunk we changed, if they didn't touch the same lines, take our changes. For each hunk they changed, if we didn't touch the same lines, take their changes. If we both made the exact same change, take one copy of that change.4 If we made overlapping changes that disagree, Git complains: I don't know how to combine these! Git says that there is a merge conflict. And, as a special case, if we and they make different changes to different lines, but those two changes abut, Git also declares a merge conflict.

If there are merge conflicts, Git leaves behind a mess. For space reasons we won't go into any details here, except to mention that this "mess" involves leaving partial merge results in your working tree and multiple copies of files in Git's index. You, as the programmer operating the Git machine, are responsible for cleaning up the mess.

If there aren't any merge conflicts, though, Git will go on to make a merge commit of its own, using the snapshot it got by:

  • combining the diffs
  • applying the combined diffs to the snapshot in the merge base, in this case commit H

This new snapshot gets metadata as usual, so it's just like every other commit—a snapshot and metadata—with one difference: its list of parent hash IDs holds two hash IDs. The new commit goes on the current branch as usual, so it looks like this:

          I--J
         /    \₁
...--G--H      M   <-- br1 (HEAD)
         \    /²
          K--L   <-- br2

Commit M points back to two earlier commits instead of just one. The list of parents is ordered, so sometimes—though not very often—we care which one is first and which one isn't. That's where the little 1 and 2 come in: the first parent of M is J, because br1 pointed to J when we started. The second parent of M is L, because commit L is the commit we merged.

Note that we merge a commit, not a branch! We usually pick that commit using a branch name, i.e., git merge br2. But we can run this with a raw hash ID, and get the same result. Commits K-L are now "on br1", as well as being on br2. (Commits I-J are not on br2: if we run git switch br2 and git log, we won't see I and J; we'll see L, then K, then H, then G, then whatever comes earlier than that.)

Had we used git switch br2; git merge br1, we would have gotten this:

          I--J   <-- br1
         /    \₂
...--G--H      M   <-- br2 (HEAD)
         \    /¹
          K--L

That is, new merge commit M would be on br2, and all the commits would now be on br2, and br1 would still point to L and would not have had any commits added to it. The first parent of M would be L, and the second parent of M would be J. But the snapshot would be the same, because Git had the same sets-of-changes to combine (and we assumed there were no conflicts).


4Exercise: is this always the right thing to do? What if the changes are adding columns of numbers (like, dollars to be billed) that should be summed up? Suppose the change we made was to add $10 because we sold a $10 item, and the change they made was to add $10 because they worked for an amount of time that costs $10. Is it right to just keep one $10 charge?


Already up to date; fast-forward

When git merge says you are already up to date, you are. Here's an illustration:

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

Running git merge main or git merge origin/main directs Git to look at your current commit J, and also at commit H, and to work out which commit is the best shared commit. That means we start at J and work backwards to I and to H, and start at H and—hey, look, a shared commit! The best shared commit is commit H. But ... hang on, commit H is already in the feature branch. If we ran git diff H H to see "what they changed", we'd get absolutely nothing. If we ran git diff H J to see what we changed, we'd see what we changed, and adding that to H gets ... J. So there's literally nothing to do.

We can get a sort of opposite situation, and it actually occurs pretty often:

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

Note how we're now on branch main, as git status would say. We run git merge feature and Git goes to find the best shared commit. Well, that's commit H again. Diffing H against H will once again show nothing. But this time, we want to add their changes to our lack of changes. If we do that as a true merge, we'll get a new commit—let's call it M again—that will look like this:

          (origin/main still points to H)
         /
...--G--H------M   <-- main (HEAD)
         \    /
          I--J   <-- feature

The snapshot in commit M will exactly match the snapshot in J, because adding something (the changes from H to J, if there are any) to nothing produces the something. This kind of "addition", adding zero, is trivially easy. So if you don't prevent it, Git will take a short-cut and do this:

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

That is, Git will move the name main to point to J, and will check out commit J. It's as if you did a git switch or git checkout using J's hash ID or the name feature, except that HEAD stays attached to main and the name main "slides forward" in what Git calls a fast-forward operation. Git specifically calls this fast-forward operation a fast-forward merge, but there's no actual merge involved.

torek
  • 448,244
  • 59
  • 642
  • 775
0

You may need to update your local version of the main branch via a git fetch:

# from local_branch
git fetch origin
git merge origin/main

The above git merge would be using the most up-to-date remote tracking branch, which should reflect the latest changes on the main branch from the remote.

Tim Biegeleisen
  • 502,043
  • 27
  • 286
  • 360
  • I got same message , also my got log show me those remote commits which I can’t see locally it’s like I am rebased on them but can’t see – ct2048 Jun 16 '22 at 04:40