2

I tried it with some random text files, that I created on github, pulled them on my computer, modified them, then I created a separate branch on github, called 'another'. When I tried pushing the modified files from my computer to github using git I got the message:

error: src refspec another does not match any<br>
error: failed to push some refs to 'https://github.com/username/repo-name.git'

What I typed before: git push origin another

dejanualex
  • 3,872
  • 6
  • 22
  • 37
Mike
  • 29
  • 1
  • 8
  • 1
    "*created a separate branch on github, called 'another'*" You created the branch at Github; it's not yet in your local repo so you cannot push it. – phd Feb 01 '21 at 10:05

2 Answers2

2

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.

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

First move to the branch another

git checkout another

then copy the file from the other branch (master in this case)

git checkout master -- path/to/file

then push to the another branch

git add *
git commit -m "message"
git push origin another
Mheni
  • 228
  • 4
  • 15