From our develop
branch, I created a new branch with git checkout -b jsm/logging
. Made my changes, commited, and pushed to origin with git push -u origin HEAD
. Did a PR and merged and delted the remote branch. Made another tweak and amended my last commit with git commit --amend --no-edit -a
. Then I checked my status and force-pushed with git push -f
. To my surprise, the wrong branch had been (force) pushed! Have a look at my console log (note that I've aliased g
to git
, st
is an alias for status
, and co
is an alias for checkout
).
Side note: I've also noticed that when I attempt to push develop
, for instance, Git often complains that master
is out of sync (need to pull first) -- but why is it doing anything with master when I'm not on that branch? Seems to be related, not sure what the issue is.
console log (branch name before "$"):
josh:~/Projects/my-project jsm/logging $ git commit --amend --no-edit -a
[jsm/logging 4cdb3dc] add logging
Date: Mon Aug 27 15:18:41 2018 -0400
1 file changed, 12 insertions(+), 6 deletions(-)
josh:~/Projects/my-project jsm/logging $ git st
## jsm/logging...origin/jsm/logging [ahead 1, behind 1]
josh:~/Projects/my-project jsm/logging $ git push -f
Counting objects: 1, done.
Writing objects: 100% (1/1), 685 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:my-org/my-project
+ 5a649bc...8d320d2 develop -> develop (forced update)
^^^^^^^ why is it pushing a different branch than I'm on?!
josh:~/Projects/my-project jsm/logging $ g co develop
Switched to branch 'develop'
Your branch is up-to-date with 'origin/develop'.
josh:~/Projects/my-project develop $ g co jsm/logging
Switched to branch 'jsm/logging'
Your branch and 'origin/jsm/logging' have diverged,
and have 1 and 1 different commit each, respectively.
(use "git pull" to merge the remote branch into yours)
josh:~/Projects/my-project jsm/logging $ git st
## jsm/logging...origin/jsm/logging [ahead 1, behind 1]
josh:~/Projects/my-project jsm/logging $ git push -fu origin jsm/logging
Counting objects: 11, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (11/11), 1.04 KiB | 0 bytes/s, done.
Total 11 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 5 local objects.
To git@github.com:my-org/my-project
* [new branch] jsm/logging -> jsm/logging
Branch jsm/logging set up to track remote branch jsm/logging from origin.
git config
alias.st=status -sb
alias.co=checkout
alias.cob=checkout -b
pull.rebase=true
push.default=matching
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.url=git@github.com:my-org/my-project
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.develop.remote=origin
branch.develop.merge=refs/heads/develop
branch.jsm/logging.remote=origin
branch.jsm/logging.merge=refs/heads/jsm/logging
knugie's comment contains the correct reason: you set push.default
to matching
. You may have been expecting to use current
, or even upstream
. That's the TL;DR right there.
The default push.default
in modern Git is simple
, which most people consider safer—matching
is what was the default before 2.0, and it caused a lot of people a lot of headaches. But to understand why this is the case, let's look at what git push
does, at a high level. This is also what git fetch
does, sort of, so it's worth covering both to some extent. I'll leave out the special fetch-specific rules, though.
First, your Git picks out a (single) other Git to contact. (If you fetch from or push to multiple remotes, Git does it one at a time.) This other Git is found at some URL. Typically you use a name like origin
to supply the URL, but you can spell one out directly, if you like. (This is rarely a good idea—it's mostly a holdover from prehistoric Git.) Or you can let Git figure it out. If there's only one remote, named origin
, Git will get it right every time. :-)
Then, your Git calls up another Git at that URL (remote.origin.url
). That other Git has its own branches, tags, and other references. Your Git has their Git list all their branches, tags, and other references. You can see what your Git sees by running git ls-remote origin
, which does these two steps, prints out the result, and stops.
For git push
, though, the next step depends on several things:
Did you list refspecs on the command line? (If so, Git uses the refspecs you listed.) The refspecs go after the remote name: git push origin <refspec1> <refspec2> ...
, for instance. If you ran git push
with no extra arguments, you did not list any refspecs.
If you did not list refspecs, is there a special default for this remote? (If so, that's the default. Note that this is a refspec, not a string literal like push.default
!)
Otherwise, push.default
is the default. It has five settings, which we'll get into below.
For git fetch
, there is a similar pattern, but Git almost always winds up using the remote.remote.fetch
setting, e.g., remote.origin.fetch
, because there's always such a setting, and you as a user will tend to just run git fetch origin
, or even just git fetch
.
This leaves one other obvious question: what the heck is a refspec anyway?
The second-simplest form of a refspec looks like master:master
or jsm/logging:jsm/logging
—or, for git fetch
, like master:origin/master
. That is, there's a left-hand side name, a colon :
character, and a right-hand side name.
The name on the left is the source and the name on the right is the destination. Each name is a reference or ref name, which means you can spell out a full name like refs/heads/master
. If you don't spell out the name, Git will typically guess correctly that master
is a branch-name and v1.2
is a tag-name (by looking at the branch and tag names you have), but if it guesses wrong, or you want to be really sure, you can spell out the full name.
But I said this is the second-simplest form. The simplest is to omit the colon and destination entirely: master
or v1.2
or jsm/logging
. Here, fetch and push differ in how they treat these: they're both still the source for the operation, but for git push
, the destination is a copy of the source. For git fetch
, the destination is to not save—to discard—the name. Since we are looking at git push
, we can skip the fetch special-ness, and concentrate on how git push
likes to use the same name on both sides.
Worth noting: you can add a leading +
to a refspec. This sets the force flag for that refspec only. Let's look at a simple example below.
There are two things that fetch and push have to achieve. The first, and most important by far, is to transfer commits. Without the commits, Git has nothing: the commits are the reason Git exists. They hold (indirectly) the files.
So if you run git push
, you have your Git give to their Git any commits that you have, that they don't, that they are going to need. If you run git fetch
, you have your Git obtain from their Git any commits that they have that you don't that you will need.
The set of commits that you, or they, will need is determined by reachability, which is a fairly big topic. For a really nice introduction to this, see Think Like (a) Git. The one line overly-summarized summary, though, is that they will need the commits that are on your branch, when you push your branch; you will need the commits that are on their branch, when you fetch their branch.
Having transferred the right set of commits to the right Git, your two Gits now must cooperate in one last step: setting some name(s). If you are git push
ing, you have your Git ask their Git to set their names: you ask them to update their master
, or update or create their jsm/logging
, for instance. If you are git fetch
ing, you have your Git set your origin/master
based on their master
, for instance—and that particular trick, of renaming their master
to your origin/master
, happens through the refspecs set up in remote.origin.fetch
.
So, if you do name some set of refspecs on the command line, your Git will fetch or push commits based on the source names you listed. The receiving Git will remember the commits fetched or pushed by setting some names—in your Git if fetch
ing, in theirs if push
ing—based on the destination names you listed.
Note that git push
can send its final name-setting operations as either polite requests—*please set your master
to a123456....
—or as rather forceful commands: set your master
to a123456...
or it's straight to bed without supper! Their Git can still refuse commands, but the usual default is to check polite requests to see if they merely add new commits, and obey forceful commands.
git push
falls back, perhaps all the way to push.default
If you just run git push origin
or git push
—with or without the force flag—your Git uses some default setting. If you don't have a specific refspec for the remote, your Git uses push.default
. This is where its five settings come in:
nothing
: this makes git push
fail, forcing you to list some refspec(s). (I tried this myself for a while but found it too painful.)
current
: this tells your Git to use your current branch. It may have been what you were expecting. It's equivalent to doing git push remote refs/heads/branch:refs/heads/branch
.
upstream
(aka tracking
): this tells your Git to use your current branch as the source, but use its upstream name as the destination. That is, if your current branch is B
, but B
's upstream is origin/not-B
, this is equivalent to git push origin B:not-B
.1
simple
: similar to upstream
but requires that the upstream name match the current branch name. That is, if master
has origin/master
as its upstream, and you are on master
, git push
pushes to origin/master
as you would expect—but if B
pushes to origin/not-B
and you are on B
, git push
simply fails.
matching
: your Git goes through their Git's list of branch names (all the git ls-remote
names that start with refs/heads/
). For every branch name that they have, your Git pushes your branch of the same name.
Note that if you use the --force
flag, this applies to all pushed branches. If your mode is matching
, it applies to the matching branches. That's why your output read:
+ 5a649bc...8d320d2 develop -> develop (forced update)
Your Git found that both you and they had refs/heads/develop
. Theirs was 5a649bc
, yours was 8d320d2
, and 5a649bc
is not an ancestor of 8d320d2
. A polite request—*please set your develop
to 8d320d2
—would have been rejected, but with the force flag in effect, your Git sent a command, and their Git obeyed. That lost some commit(s) from their develop
, so they said "forced update" and your Git printed that and the three dots (a normal non-forced push
shows only two dots).
If you still have commit 5a649bc
in your own repository, you can easily recover from this. If not, it's trickier. To recover, if you do have it, consider running:
git push origin +5a649bc:refs/heads/develop
This uses a refspec in which the +
(force flag) is set, the source is the raw commit hash 5a649bc
, and the destination is the branch name develop
. Note that it's wise (and maybe even necessary) to spell out refs/heads/develop
here since the source "name" is a raw hash ID, so your Git doesn't know this is supposed to be a branch.
1This may or may not summon Shakespeare's ghost. (Or is that King Hamlet?)