You're starting by putting yourself in a very bad position, like trying to win the Kentucky Derby by getting yourself an emu in Australia rather than a horse in the USA. The problem is simple enough: a branch in Git does not have files. In fact, in an important sense, branches don't even exist in Git!
Of course that's nonsense—branches do exist—but the problem is that the word branch does not have one particular single meaning. So until you specify which kind of "branch" you mean, the word branch doesn't mean anything at all. We might as well make up a word like "cryosome" or "trajectomy" or something. The options for defining "branch" here might be branch name (these don't have files), remote-tracking (branch) name (these also don't have files), DAGlet (see What exactly do we mean by "branch"?), or branch tip.
This last one actually gets us somewhere! A branch tip has files! But another word for branch tip would be commit. It's the commit, in Git, that has files. Every commit has a full snapshot of every file, frozen in time for all eternity.1 These files literally can't be changed though: instead of changing the files in some existing commit, what we do is:
- extract some existing commit into an area in which we can change files;
- change some file(s) in this changeable area; and
- make a new commit from the resulting (changed) files, making a new permanent, unchangeable snapshot.
Every commit we make has a unique2 hash ID: that big ugly string of letters-and-digits that you can't remember. No human can deal with these, so we don't. We have Git remember the latest one from some particular thing-we-call-a-branch using a thing-we-call-a-branch. We branch the branch and branch branch branchity branch branch, branch-branch, branch? Er, sorry, I got Git-ified and all words became "branch". (This is the problem with the word branch and Git's enormous overuse of it.)
Anyway, let's take a look at commits—the things that actually matter in Git—and see if there is a way for you to solve your actual problem. We'll see how Git uses branch names to find commits, too.
1Or at least, for as long as the commit continues to exist. It's possible, sometimes, to remove a commit from a repository. (GitHub generally won't, and it's tricky to do in regular Git.)
2The hash ID here is required to be truly unique, as in, no other Git repository anywhere in the universe is allowed to use the hash ID you just got for your new commit, unless it's being used to store that commit (that they get from you, or from some other repository that got it from you). That's why the hash IDs are so big and ugly, so that they can be unique. This is mathematically impossible (see the pigeonhole principle), so Git will someday stop working. The size of the hash puts that day far enough in the future—trillions of years or more, we hope—that, we hope, we're all so long dead and gone that nobody cares.
How commits and names work
Every commit:
- is numbered, with a big ugly hash ID;
- stores a full snapshot of all of your files; and
- also stores some metadata: information about the commit itself.
I mentioned the first two of these already, but let's look at the third one a little more closely. When you make a new commit, Git uses your user.name
and user.email
settings to record you as the author (and committer) of the new commit. It uses your computer's clock to get the current date-and-time, and puts those in the new commit. Git makes you enter a commit message (with -m
or via an editor or something) and stores that message along with the commit.
All of this stuff is part of the metadata for the commit: the information about the commit that's not the snapshot itself. Git adds, in this metadata, a list of previous commit hash IDs. For most commits—the ones we'll look at here—this list has exactly one entry in it; Git calls this one hash ID the parent of this new commit. But it's a list, so it can be any number, from none at all to lots; it's just that "one" is the most common number of list-entries per commit.
The effect of storing one hash ID in the commit's metadata is to make the new commit point backwards to its parent. That is, the parent has a unique hash ID, and the new commit has a unique hash ID, and the new commit connects to the parent:
a123456 <--b789abc
for instance. The hash IDs are too big and ugly for human brains to handle, though, so we'll use uppercase letters, like this:
G <-H
to stand in for whatever hash IDs Git might really use. Commit H
here is thus your newest (hence latest) commit, and inside H
, in its metadata, Git has stored the hash ID of earlier commit G
. So Git can use H
to find G
.
But G
is a commit, so it too has a parent hash ID. That means G
points backwards to some earlier commit F
:
F <-G <-H
and of course F
points backwards too, and so on, forever, or at least until we get to the very first commit:
A <-... <-F <-G <-H
which can't point backwards, so it has an empty list.
The metadata, like everything in every commit, is frozen for all time, so if some commit points backwards to some earlier commit, it does so forever. Commits also can't point forwards to the next commit because the hash ID that you'll get depends on things like the exact time at which you will make the future commit. Git has no idea when that will be, or what will be in it, so commits can only point backwards. As a result, we can be lazy when we draw them, and I will.
Now let's add a branch name, like this:
...--G--H <-- somebranch
The branch name somebranch
simply holds the hash ID of the last commit in the branch. So the name somebranch
points to H
at the moment. If and when we make a new commit, though, the new commit will point backwards to H
:
...--G--H
\
I
and Git will immediately update the branch name—unlike commits, Git can change the hash ID stored in a name—so that it now points to new commit I
, which means we don't need to draw I
on a separate line after all:
...--G--H--I <-- somebranch
You might now say: Aha, so I just want to check every branch name's commit! Yes: this could be what you want! Alas, but no: it's probably not what you want, after all. You see, branch names are not the only names that select commits. Besides branch names, we also have tag names and remote-tracking names.
A tag name is a name for a commit, like a branch name. But unlike a branch name, a tag name normally never moves. We have Git create a tag name when we have some particularly important commit—such as a release version—that we want people to be able to name, now and forever, without having to memorize some random-looking hash ID. Do you want to look at tag names? Maybe, maybe not: this will depend on how you use tag names.
The bigger thorn here is the remote-tracking name. A remote-tracking name is your own Git repository's way of remembering some other Git repository's branch name. We just saw how, if you're on your branch somebranch
and make a new commit, your branch name somebranch
changes its stored value, so that it points to the new commit. If someone else, in another clone, has a branch named somebranch
and they make a new commit, their repository will update their somebranch
name to point to this other new commit.
Let's suppose this sort of thing does happen. That is, you make new commits in your repository on branch develop
, and Alice or Bob or Chandra or Darius makes new commits in their repository on develop
too. At some point you'll pick up their commit(s), while you have your own commit(s) that you have added. The result looks like this:
I <-- develop
/
...--G--H
\
J <-- origin/develop
Which of these is the latest commit? Remember that they made their new commit J
not knowing of your commit I
, and vice versa, and perhaps you and they made these same commits at the exact same second, just on different computers. Even if you were seconds or minutes apart, you're both "latest", so you can't go by time.
Ultimately, there's no single answer here. This is why merges exist in Git: sometimes we have no choice but to combine work done by two different people or groups, but done in parallel "at the same time". If this applies to your situation, you're using the wrong approach. But if not, you might well be able to use all the branch and/or remote-tracking names to find the latest commits on those "branches" (whatever we mean by branch here), and use those to inspect the file(s) in question.
Your other question
Or better: Is there a way to share one file among all branches? So that all branches share always the same version of a specific file?
We can already see that the answer to that is a definite "no". There are, really, only commits in Git. The branch names, and other names, are just ways to find particular commits, and each name finds one commit and then those commits hold frozen-for-all-time snapshots. If they do share one particular version of a file—which happens all the time in Git—then Git makes sure that they literally share that file's content, so that it's stored just once in the repository. (Git does this by automatically de-duplicating all content.) But they either identify the same commit, in which case they have the same content for that file, or they identify different commits, in which case they might or might not have the same content for that file and you'll have to check. There's no one right way to determine which one is the latest though, and there may be no way at all.
A possible alternative, but think hard before you use it
One thing you can do is that instead of storing this file as a regular tracked file at all, where it's committed inside every branch, you can declare that this particular file is untracked in all branches (and you'll list it in .gitignore
so that it stays that way). That way it's not stored in any of these commits. Meanwhile, you have one dedicated branch name—which still requires dealing with remote-tracking names, and merging if other people have made commits—in which you store, not a file named xyzzy.untracked
, but rather one named xyzzy.master-copy
. To update this file, you check out the xyzzy
branch, which stores only this one file:
git worktree add ../xyzzy # if this is the first time using it
cd ../xyzzy
... edit the xyzzy.master-copy file ...
git add xyzzy.master-copy
git commit
cd ../normal-work-area
cp ../xyzzy/xyzzy.master-copy xyzzy.untracked
Now, since xyzzy.untracked
literally is untracked, it never goes into any new commits. xyzzy.master-copy
doesn't go in these commits either: you keep it only in the xyzzy
commits, on the xyzzy
branch. You never edit that file in the place where you normally work, since it doesn't even exist there. It only exists in the added working tree with the xyzzy
branch in it.
To make the xyzzy
branch initially, you can check out the very first commit you ever made in your repository (find it with git log --max-parents=0
for instance) as a detached-HEAD commit, then create a new branch name:
git switch -c xyzzy
Remove any files you don't want to leave around here and create the "master copy" file with some initial content. Add and commit it. Then switch back to the branch you will normally work on/with, and notice that xyzzy.master-copy
no longer exists in your working area. Add xyzzy.untracked
to .gitignore
to make sure you don't accidentally extract it, then use git show xyzzy:xyzzy.master-copy > xyzzy.untracked
, or use the git worktree add
trick to add a working tree, or whatever you like.
(You can, instead of finding your root commit, use git switch --orphan xyzzy
. This will set things up so that your next commit makes the "orphan branch" by making a second root commit in your repository. This method is a little confusing to those not steeped in graph theory and/or Git, though.)
Note that now that you have just the one-file-in-one-branch, you have a problem: should you ever discover that you actually need two different versions of xyzzy.untracked
, you cannot get that. There's a reason Git makes this hard: it's almost certainly the wrong thing to do.