Why does git tell me "my branch is up to date" when I clearly made changes in local repo?

Viewed 498

I am working on an upstream-forked, cloned repository.

I literally made changes in the file "fundamentals/src/main.js", and git shows the file is modified as seen in the terminal with the "M" sign.

I am not sure why git isn't recognizing the changes when I try to commit. I was able to do the same process yesterday night on this same file.

Today, I worked on another file within the same directory, and kept making commits and pushes, although I don't suspect this to be the cause.

enter image description here

2 Answers

You are only adding the current directory to the index by invoking git add .. The only modified file is in the parent directory, so nothing gets staged, there is nothing to commit. You can add it explicitly, or use git add -A to add everything (tracked or not), git add -u to add only tracked or git commit -a which is the same as the previous add and then commit.

To expand on Tejes' answer, let's start with this:

  • Git is, at its heart, really all about commits. Branch names are something humans use (so they matter) but Git isn't really very concerned about branch names. It cares about the commits.

  • Every commit is numbered. The numbers aren't simple sequential numbers, though. They don't count up: commit 1, commit 2, commit 3. Instead, the "number" of each commit is a unique but random-looking hash ID.

  • Internally, the hash ID is a checksum of the full contents of the commit. This means that you can't change a commit. If you take a commit out of Git's database of commits, alter it in any way, and put it back, what you get is a new and different commit with a new unique hash ID. The existing commit continues to exist. So nothing in any commit can ever change.

  • The stuff inside each commit is in two parts:

    • There is a full snapshot of every file. Each file is stored in a compressed, frozen (read-only), Git-only, de-duplicated form. That way, since most commits mostly re-use a previous commit's files, the files don't take any extra space.

    • Besides the frozen snapshot, each commit also contains some metadata, such as the name and email address of whoever made the commit, some date-and-time-stamps for when they made the commit, their log message about why they made the commit, and so on. There's one part of this metadata that Git uses for its own purposes: every commit lists the hash ID of its immediate parent commit(s).

The fact that commits store their parent commit number means that Git can, given the last commit's hash ID, work backwards. From the last commit, Git can step back, once, to the second-to-last commit. From that commit, Git can step back once again to the third-to-last commit. From that commit, Git can step back again, and so on. This in turn means that all Git needs, to find every commit, is the commit hash ID of the last commit in each branch.

So that's what a branch name, like master or develop stores. Each branch name just holds the hash ID of the last commit on the branch. From there, Git works backwards.

The fact that nothing in any commit can ever be changed, though, is a big issue here. The files inside each commit—inside the snapshot—are in a format that non-Git programs on your computer can't even use. So to use a commit, Git has to give you a way to extract all the files from that commit's snapshot. This is what git checkout (or git switch, since Git 2.23) is for.

Checking out a commit extracts all of its files

You might use git checkout master or git checkout develop, for instance. In Git 2.23 or later, you can use git switch instead of git checkout—these do the same thing, except that the old git checkout command has way too many options and can be tricky for newcomers to Git. The git switch command simplifies away some of these options and makes Git easier and more pleasant to use. (At least, that's the intent.)

Checking out (or switching to) some particular name like master or develop instructs Git to extract all the files from the last commit on that branch. The commit's number is stored in the branch name. These files come out of the Git repository, where they're stored in the special frozen-and-de-duplicated Git format, into an area where you can see and work with them.

The place where you see and work on your files is your working tree or work-tree. These are not Git's files. These are your files. Git just extracted them. You can also have Git overwrite your work-tree, in case you've messed up a file and just want it back out of a commit-snapshot. (Commands to do this include git checkout—which means you can accidentally overwrite your own work, if you've forgotten to save it; git switch won't do that, which makes it safer—or since Git version 2.23, git restore.)

Git doesn't make new commits from your work-tree

Note that there are, in effect, two copies of each file you might be working on: the one in the current commit, which is frozen for all time into the commit snapshot, and one in your work-tree, which isn't frozen, and isn't in some format that only Git can use. The actual stored bits are different—because Git's internal file is in the compressed Git-only format—but they represent the same file.

Given the above, you might think that git commit would make a new commit from the files you have in your work-tree. This makes sense and is how some other version control systems work. But that's not how Git does it.

Instead, Git keeps a third copy1 of each file. This third copy is in the frozen format, like the copy in the current commit, but unlike the committed copy, isn't actually frozen. In particular, you can replace it wholesale, using the git add command.

This is why you have to git add files over and over again. You're not just telling Git use this file. You're explicitly telling Git: ... and overwrite the existing frozen-format copy, by copying the work-tree copy into the frozen format, ready to be committed.

This third copy of each file exists in what Git calls, variously, the index, the staging area, or (rarely these days) the cache. When you tell Git to make a new commit, Git just packages up these files, along with the required commit metadata, to build the new commit. This means that, at all times, the index / staging-area represents the next commit you intend to make. To make a new commit that has updated files, you must update the index / staging-area.

The git add command is the main work-horse for updating files in the index / staging-area.


1Technically, the index doesn't hold an actual copy of the file, but instead just the information needed to commit the file. What's in the index is a mode, file-name, staging number (normally zero), and blob hash ID, plus a bunch of cache information to help Git go fast. But you don't need to know any of this unless you start getting into some of the very low level index-manipulation commands. You can just think of the index as holding a frozen-format, de-duplicated copy of each to-be-committed file, as that's how it works in the end.


Viewing what's staged and not staged

There's no good way to see what's in the index / staging-area directly.2 But you don't really need to see what's in it. What you need to know is what's different in it. The git status command does this.

Running git status prints:

  • the current branch's name, and some additional information if it's available and useful;
  • a list of files that are staged for commit; and
  • a list of files that are not staged for commit.

Your git commit command found nothing to commit—every file in Git's index matched every file in the current (or HEAD) commit—so it did not actually commit, and instead, ran git status for you. This git status said:

On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
        modified:   ../fundamentals/src/main.js

Note the leading .. here: this indicates that you are not in the top level of your work-tree, but rather at least one level down.

In order to list files that are staged for commit—which in your case, was no files at all—Git now compares the snapshot in the current commit, which in your case is the last commit on your branch master, to the files stored in the index / staging-area. Whenever one of these files is different, that file will be listed out here.

Since no files were different, every file in Git's index matches the copy of the file in the current commit. So there is nothing to commit.

In order to list the files that are not staged for commit, Git compares the files stored in the index to the files in your work-tree. For each file that is different, Git prints that file's name. In this case, the file ../fundamentals/src/main.js is different: Git's index copy does not match your work-tree copy.

If you use git add ../fundamentals/src/main.js, this will replace the index copy with a frozen-format version of your work-tree copy. A new git status command will now show this file as staged for commit. This time, git commit will actually build a new commit.


2You can see what's in it: run git ls-files or git ls-files --stage. But this usually isn't very helpful: for one thing, in a big repository it prints way too much.


The git push command sends commits, not files

When you use git push (or git fetch, including the git fetch that git pull runs), your Git calls up some other Git repository. Your Git and their Git have a conversation about which commits—by hash IDs—each repository has. One Git will send commits to the other Git, if and as needed.

Until you have a new commit to send, git push won't do anything. Once you have a new commit, that some other Git repository lacks, using git push can send that commit—including all of its files3—to the other Git. The receiving Git can then add that commit to its collection-of-all-commits, adding that commit to one of its branches. We won't cover all the details here, though. The point to remember is that Gits deal in commits, with branch names like master remembering the last one.


3Note that, while the commit automatically includes all of the files—it's a full snapshot of every file—the two Gits' conversation usually enables the sending Git to realize that the receiving Git already has most of those files, through the usual de-duplication scheme. So the sending Git can send a slimmed-down set of internal Git objects. The exact details of what Git calls a thin pack here are beyond the scope of this answer.

Related