Git isn't really about files, and git pull
doesn't pull files. You can, however, probably achieve what you probably want. The real problem here lies in figuring out what you want, in the context of Git, which stores commits, not files.
To start with, though, let's address this message:
error: src refspec another does not match any
This means you do not have a branch named another
. That makes some sense, given what you just said here:
I [created] some random text files ... on github,
It's not entirely clear how you did that (see Minimal Reproducible Example), but let's assume that this created one or more new commits on GitHub, each on some branch—master
or main
perhaps.
... pulled them on my computer,
Let's assume here you ran:
git pull
in a repository you first created with:
git clone <url>
where the specified url
is one that has your Git, on your computer, talk with GitHub's computers to get commits from, and put commits to, your GitHub repository. This git pull
will have obtained commits from your GitHub repository. Those commits will contain the files you created. These commits are on whichever branch(es) they are on (any given commit can be on one or more branches here, or even, in some cases—though not this one—on no branch). Let's assume you're using the new main
branch name here, so that these new commits are on main
(and not on any other branches).
Following the "obtain new commits" step (git fetch
), git pull
runs a second Git command of your choice. If you don't tell it otherwise, this second command will be git merge
. In conditions that probably apply in this case, this git merge
will perform what Git calls a fast-forward merge, which is technically not a merge at all. The result is that your main
branch—or whatever its name is—in your Git repository, on your computer, is now in sync with the main
branch—or whatever its name is—in the repository over on GitHub.
(We need to be fairly precise here, otherwise none of this stuff works, as you've just seen.)
[I] modified [these files locally], then I created a separate branch on github, called 'another'.
Note that after the fast-forward operation, your Git repository will have the same commits as the GitHub repository. Your own Git will have run git checkout
on the updated branch name locally—again, I'm still assuming this is named main
here, although it might be master
or some other name.
This git checkout
step created or updated the visible and editable files that you saw and modified. These files are not in the repository. To put them into the repository, you must run git add
and git commit
locally. This creates a new commit. The commits are what are in the repository.
You don't mention having done that, so I assume you haven't. Until you have, those files are just files sitting around on your computer. They're not stored in Git at all. They're just there for you to work with. You'll need the git add
and git commit
steps to make a new commit.
Meanwhile, let's get back to this:
then I created a separate branch on github, called 'another'.
This creates a branch on GitHub, in the other repository over there. It has no effect on your repository here. So you have no branch named another
.
When I tried pushing
We must guess what command you used, since you didn't tell us, but presumably that was:
git push origin another
which is sensible, but would immediately have given you the error you just saw, because—as we already noted—you don't have such a branch.
Dealing with Git is a bit messy and complicated
This is because distributed source control is fundamentally complicated.
Git's approach here is to work in terms of commits. Each commit you make has some interesting properties:
Each commit is numbered. These numbers are not simple sequential things—Git doesn't make commit #1, then commit #2, and so on—and are instead random-looking, but not random at all, hash IDs. The hash IDs are cryptographic checksums of the contents of each commit, and are guaranteed to be unique to that one particular commit. (To help make this happen, Git puts date-and-time-stamps on each commit, for instance.)
Each commit has two parts, as it were:
One part is a full snapshot of every file. These files are stored in a compressed, read-only, and de-duplicated format, so if you have many files, but only change one, the new commit you make just re-uses all the others. This saves a lot of space. It does, however, mean that nobody can ever change any committed file at all. (This is also enforced by the numbering scheme.)
The other part of each commit is metadata, or information about the commit itself. This includes your name and email address, for instance. It includes the date-and-time-stamps I mentioned. It includes a log message, in which you should explain why you made the commit. And, crucially for Git's internal purposes, the metadata of a commit includes the raw hash ID of some set of earlier commits.
Most commits record a single earlier-commit hash ID. Git calls this the parent of the commit. The parent of the commit is the commit you used—via git checkout
—when you made this commit. The result is that after a series of git commit
operations starting from some git checkout
, you have a string of commits, pointing backwards, one step at a time, to each previous commit:
... <-F <-G <-H
Here, H
stands in for the hash ID of the last commit in this chain of commits. H
contains a snapshot and metadata, and the metadata for H
includes the hash ID of earlier commit G
. We say that H
points to earlier commit G
. G
in turn contains a snapshot and metadata, so that G
points to F
. F
, likewise, has a snapshot, and points backwards.
Given this structure, Git needs only one thing to find the entire chain: it needs the hash ID of the last commit in the chain. Git typically stores this hash ID in a branch name:
...--F--G--H <-- main
The name main
contains hash ID H
, which lets Git extract commit H
—including all of its files—for you to work on/with, in that work area that's technically outside the repository proper. This is your working tree or work-tree.
You can create any new files you like here. You can update any existing files. You can do whatever you want, because this work area, outside the repository, is literally yours. All Git does with it is:
- extract files to it, when you ask; and
- prepare files from it for committing, when you ask.
These two "ask"s can use various commands, but the two obvious ones are git checkout
—which fills in your entire work-tree from some commit—and git add
. The checkout step leaves alone any files that Git doesn't know about. These are, after all, your files, not Git's. If you create a new file xyzzy
and never tell Git about it, Git just leaves it there.
The git add
step works by copying some file into an already-set-up, hidden work area, which Git sometimes calls its staging area, and sometimes calls its index. This thing—the index or staging area—initially contains all the files from the current commit, that you checked out with git checkout
, ready to go into the next commit. Using git add
tells Git to copy some work-tree file from your work-tree into the staging area, so that it's ready to go into the next commit.
In this way, Git's index / staging-area contains, at all times, your proposed next commit. When you run git commit
, what Git does is:
- gather any necessary metadata (your name and email address, "now" for the date-and-time, and so on);
- turn what's in the staging area—a replaceable, but ready-to-commit, copy of every file that Git knows about—into a new snapshot;
- write all of this stuff out as a new commit, using the current commit's hash ID as the parent of the new commit; and
- write the new commit's new unique hash ID into the current branch.
So if you add some new file, and/or modify some existing files, and run git commit
, Git turns:
...--F--G--H <-- main
into:
...--F--G--H--I <-- main
This is how you make new commits.
More about branch names
As we've just seen above, a branch name simply points to some commit: the last commit in some chain.
Let's start with the repository as presented earlier:
...--G--H <-- main
Now, let's make a new branch name. To do so, we must pick some existing commit. There's no particular reason to pick anything other than the latest commit here, so let's use that one: commit H
. We'll make the new name develop
. Now we have:
...--G--H <-- develop, main
We now have a problem: do we pick the main
branch, or the develop
branch? To make it clear which one we're using, let's attach a special name, HEAD
, to one of the two branch names:
...--G--H <-- develop, main (HEAD)
This means we're using the name main
to find commit H
. If we now run git checkout develop
, we get instead:
...--G--H <-- develop (HEAD), main
Now we're using the name develop
to find commit H
. Either way, we have commit H
checked out: the usable files are copied from H
, and the proposed next commit matches commit H
.
Now we'll create a new file in our working tree, modify one existing file, and then run git add
on both files. The add step will copy the new file into the staging area, and copy the updated file into the staging area, and leave all the other files alone in the staging area: now all files in the staging area match all files in our working tree.
Now we run git commit
. Git packages everything, including all the files it knows about in its staging area, into a new commit, which we'll call I
. The parent of new commit I
will be existing commit H
, which we have out until new commit I
becomes the current commit. And, last, Git will write I
's hash ID into the current name, develop
, to which our HEAD
is attached:
...--G--H <-- main
\
I <-- develop (HEAD)
If we make another new commit while still on develop
, we get:
...--G--H <-- main
\
I--J <-- develop (HEAD)
This is how branches grow.
Should we decide to make another new branch, we now have two "last commit" candidates: commit H
, which is the most recent on main
, or commit J
, which is the most recent on develop
. Which should you use? That's up to you. You can pick either of these or any other existing commit. Let's pick commit H
though, so that our new branch feature
extends from commit H
:
I--J <-- develop
/
...--G--H <-- feature (HEAD), main
and now we can make two more commits, as usual: they'll start with the files from commit H
since that's the commit we have out now, but as we go, some files in the new snapshots will differ. Let's draw the resulting commits:
I--J <-- develop
/
...--G--H <-- main
\
K--L <-- feature (HEAD)
Note how commits up through H
are on all three branches. Commits I-J
are reachable only by starting at develop
and working backwards, and commits K-L
are reachable only by starting at feature
and working backwards, but we can get to commit H
using the name main
, or by starting from either of the other two branch-ends (Git calls these the tip commit of each branch) and working backwards.
Push, fetch, etc., part 1: git fetch
As we have just seen, branch names help you and Git find commits. What matters is not, in fact, the names, but rather the commits themselves. Still, the names are how we find the commits, so the names aren't completely irrelevant.
But now, we note that we have more than one Git repository. We need a way to transfer things between them. This is where git push
and git fetch
come in.
Note that git pull
runs git fetch
first, then runs a second Git command. Many people like this because the fetch step is not very useful by itself. Some (including me) dislike it because the choice of second command is problematic. Git very recently started recommending that you configure this second command explicitly, or choose it explicitly, rather than just letting Git pick one for you. This is an improvement, but I think it's best to learn to use git fetch
—which is as close as Git gets to an opposite of git push
—first, then learn to use the second commands, and only then decide if you like allowing Git to run them as a single unit. But that's another discussion entirely.
What git fetch
does is have your Git call up another Git. That other Git has commits, and has branch names that help that Git find the commits. Your Git needs the commits, because those are what matter.
Suppose that your own Git has these commits:
J <-- feature
/
...--G--H--I <-- main (HEAD)
Suppose that their Git has these commits:
...--G--H--K <-- main (HEAD)
Your Git calls up their Git. They have a commit K
that you don't have at all. You would probably like to obtain this new commit K
. The git fetch
command will do this, depositing commit K
—the only one they have, that you don't—into your repository:
J <-- feature
/
...--G--H--I <-- main (HEAD)
\
K
Note that you have I-J
and they don't, and you have branch name feature
and they don't. But they have commit K
and you didn't, until your git fetch
got it. But how will you find commit K
? Remember, it has a random-looking hash ID.
Well, their Git finds that commit with their branch named master
. Your Git could update your master
, so that you have:
...--G--H--I--J <-- feature
\
K <-- main (HEAD)
But if your Git did that, you'd no longer have commit I
on your master
. You'd still have access to it through feature
, but what if you didn't have commit J
and the name feature
? (Think about that for a moment, and draw what your commit graph might look like.)
What your Git does, instead, is to not update any of your branch names. Instead, your Git creates or updates some names that are not branch names. I call these remote-tracking names.1 Your Git forms these names by sticking origin/
or some other name2 in front of the branch names:
J <-- feature
/
...--G--H--I <-- main (HEAD)
\
K <-- origin/main
It's now easy to find commit K
: they call it main
, so your Git calls it origin/main
.
1Git calls these remote-tracking branch names, but the word "branch" here just clutters up the term. They aren't branch names. They were created from branch names, but you cannot attach your HEAD
to one of these.
2The thing that gets stuck in front is the remote, plus the slash. This is just a name you use to have your Git talk to the other Git. The name origin
is the standard first remote name—git clone
uses origin
by default—so origin/
is extremely common, but you can pick some other name if you like.
Push, fetch, etc., part 2: using git merge
It's pretty common, in many setups, for you to have, in your own Git:
...--G--H <-- main (HEAD), origin/main
when you then hear (over Slack or whatever) that some co-worker has created a new commit I
, which you obtain with git fetch
:
...--G--H <-- main (HEAD)
\
I <-- origin/main
At this point, you almost certainly just want to have your Git advance your main
so that it selects commit I
. You also want to have your Git check out commit I
, so that you have in your work-tree (and ready for the next commit, in your staging area), all the files from commit I
. To get there, you can let Git do what Git calls a fast-forward merge, as we mentioned before.
We can draw the result like this:
...--G--H--I <-- main (HEAD), origin/main
You're now "on" commit I
, via main
; your working tree and index are updated to match commit I
.
This fetch-and-then-merge operation is very common—so common, that git pull
exists to do just that. Running git pull
or git pull origin
has your Git call up the other Git at origin
, fetch any new commits, and then use git merge
to merge your current branch—which was and still is main
—with their origin/
counterpart. You can set git pull
up to operate differently, but we won't go into details here as this is already quite long.
Push, fetch, etc., part 3: using git push
Let's say we just did the above so that we now have commit I
and are in sync with the Git over on origin
. Now we modify some file in our work-tree, or add a new one, and then make a new commit J
:
J <-- main (HEAD)
/
...--G--H--I <-- origin/main
What we'd like to do now is send, to the other Git, our commit J
. The git push
command achieves this. We run:
git push origin main
The origin
part tells Git who to call up: the other Git over at origin
. The main
part tells Git which commit(s) we want to send, plus one more thing, which we'll come back to in a moment.
Our Git now talks with their Git. Our Git finds out which commit is the tip of their main
—that's still commit I
—and our Git therefore discovers that the only new commit that we have, that they don't, is commit J
. Our Git therefore sends them just commit J
. If they were on, say, commit G
, our Git would send them commits H-I-J
at this point.3 But we just send them J
.
Having sent them our commits, our Git then sends a polite request to their Git: Please, if it's OK, set your main
to point to commit J
. The name we send here, main
, is from our git push
. The hash ID we send here—for commit J
—is from our git push
. Note that we don't ask them to set fred/main
or george/main
or anything: we ask them to set their main
. If they accept this request, their branch gets updated.
Because this is a polite request, not a forceful command, they can and will reject our attempt under various circumstances. (Even with a forceful command, you can have GitHub reject some commands.) We won't worry about any of those here though. Just note the significant difference between git fetch
, which updates our remote-tracking names, and git push
, which has them update their branch names. So while fetch and push are as close as we get to opposites, in Git, they're not strictly opposites.
In this case, though, they'll just take our request. We say *update your main*
and our commit adds on to their commits, so they say "ok", and when they are done, we have, in our repository, this:
...--G--H--I--J <-- main (HEAD), origin/main
Our Git updates our own origin/main
because we see that they said OK to our request to set their main
. So our origin/main
, which remembers what their main
was the last time we talked with them about it, should update.
3Note that it's possible for someone to have removed commits H
and I
from the other Git. In this case, we'll put them back. This is one of the main reasons it can be very hard to get rid of a commit: you can drop it from your Git repository, but any other Git repository that got it can give it back to you. It's like having a virus where you're never immune from getting it a second time.
Creating new branches on the other Git
Suppose that we have:
...--G--H <-- main (HEAD), origin/main
We create a new branch—feature
, another
, or whatever—and use git checkout
or git switch
to get onto it and add some commits to it in the usual way:
...--G--H <-- main, origin/main
\
I--J <-- another (HEAD)
We can now run:
git push origin another
This will call up their Git, figure out which of our commits I-J
are new (both of them, of course), send them, and then ask them, politely, to create or update their name another
to point to commit J
.
Since they don't yet have the name, they'll obey this request by creating the name another
, pointing to commit J
. Our Git will update our own repository to add the new remote-tracking name:
...--G--H <-- main, origin/main
\
I--J <-- another (HEAD), origin/another
That's all there is to it. Well, almost all.
Upstreams
Each of our own branch names can—is not required to, but can—have one of what Git calls an upstream. An upstream is simply another name. For our purposes here, we'd like this name to be one of our remote-tracking names.
Our main
probably already has origin/main
set as its upstream. We just created another
and when we did, we didn't set any upstream, so it has no upstream at all. Our git push
didn't set an upstream on it. So if we want origin/another
to be set up as the upstream of our branch named another
, we have to do that now:
git branch --set-upstream-to=origin/another another
This simply sets the upstream of our branch named another
to origin/another
.4 We had to do this after our git push
because we didn't have origin/another
until then. Our origin/another
exists now because the branch named another
exists now on origin
.
To make this a little easier, we can run git push -u origin another
. This -u
is short for run the git branch --set-upstream-to
for me after the git push
succeeds.
Once we have this upstream set, we can just run any of these commands:
git fetch
git merge
git pull
git push
to have the "right thing" happen when we're on our branch named feature
, where "right thing" for git fetch
is to call up origin
, "right thing" for git merge
is to merge with origin/another
, and so on. Having the upstream set also makes git status
output have more information.
4In quite old versions of Git, there's no --set-upstream-to
option and you must use --set-upstream
, which has the arguments kind of backwards, which caused confusion, which is why Git now has --set-upstream-to
. If you have such an old Git, upgrade it. If you can't upgrade it, remember this footnote.
What if you've already created another
on origin
?
In your case, you used GitHub's web interface to create a branch named another
over on origin
. You can now run:
git fetch origin
which will have your Git call up their Git, list out their branch names, figure out if they have any new commits (maybe they do, maybe not), and use the branch names they list out to create or update your own remote-tracking names.
Let's say that you started, way back at the top, with:
...--G--H <-- main (HEAD), origin/main
Then, you used GitHub's interfaces to create a new commit or two on main
over there, and ran git pull
, which first ran git fetch
on your machine, to get:
...--G--H <-- main (HEAD)
\
I--J <-- origin/main
Your git pull
now ran git merge
, which used the upstream setting of your main
to merge with your just-updated origin/main
/ commit J
, which did a fast-forward instead of a merge:
...--G--H--I--J <-- main (HEAD), origin/main
and then you used GitHub to create the name another
on that repository, over on GitHub, pointing to commit J
.
If you run git fetch
one more time, your Git will see that they have the name another
and will create your own origin/another
, like this:
...--G--H--I--J <-- main (HEAD), origin/another, origin/main
You can now run:
git checkout another
in your own Git repository. This looks around for a branch named another
and does not find one. Rather than immediately giving you an error, though, your Git will keep looking, this time at all the remote-tracking names, and find origin/another
. This name is the result of sticking origin/
in front of the name you just requested. So your Git now uses what Git calls Do What I Mean or DWIM mode to create a new (local) branch name, another
, pointing to the same commit as your origin/another
, which is still commit J
, and then do a git checkout
of that name:
...--G--H--I--J <-- another (HEAD), main, origin/another, origin/main
This kind of branch creation sets up the upstream for the new branch: the upstream of the newly-created name another
is origin/another
.5
You can now make new commits, on this branch named another
, and then run:
git push
Since its upstream is origin/another
, this git push
will know to push to origin
and ask them to use their name another
based on your named another
.
5This is all very configurable, but in my observations, I've never seen anyone turn this particular option off.