Suppose I want to create a brand-new branch that's an exact copy of an existing one. (If it matters, later I'll explain my particular motivation.)
The first time I needed to do this, I did it "by hand":
git checkout oldbranch
git checkout -b newbranch
Later I discovered the "official" way:
git branch -c oldbranch newbranch
But I just discovered a pretty serious problem: using branch -c, and if oldbranch was a remote-tracking branch, newbranch is not really a brand-new branch, in that it inherits oldbranch's upstream. This almost caused me some serious problems just now, and I spent a fair amount of time trying to untangle it.
So what's the right way to do this? Should I be using my original, "by hand" method? Or should I remember, any time I use branch -c, to then use git branch --unset-upstream (I think that's right) to remove the upstream tracking?
The context this comes up in is that I had to rebase a branch, but I didn't really want to rebase the branch; instead I wanted to rebase a copy of the branch. I wanted to keep the old, un-rebased branch around, partly on general (i.e. packratty) principles, partly because the un-rebased branch already had an upstream, which I obviously didn't want to upset.
When I went to push the rebased copy to my upstream, I expected git to complain that there was no upstream branch, and to remind me to do the --set-upstream thing. But instead it complained that things were out of sync, and that's when I discovered that the new copy retained the original branch's upstream.
TL;DR
I suspect you may want to set
branch.autoSetupMergetofalse, i.e.,git config --global branch.autoSetupMerge false. (The connection here is obvious, no? ) At least, though, you want to stop using--copyaka-c.Long
The motivation does matter somewhat, but perhaps not as much as you think:
This isn't quite right. Instead, all the branch creation methods can "set an upstream" on the new branch. Whether and when they do set an upstream depends on numerous options, which makes describing this tricky. In this particular case, when
oldbranchis a remote-tracking name (my term for it: see below), the default is thatoldbranchbecomes the upstream. That is, we don't findoldbranch's upstream—it does not have one; only branches have upstreams—but instead it is the upstream.To properly explain this, let me start with why I hate the official Git name for names like
origin/main. Git calls these remote-tracking branch names. This poor word branch now has about 6 different meanings (before breakfast?), one of them being "remote-tracking name". But we don't need it to mean that at all. If we just use the phrase remote-tracking name, omitting the word branch entirely, we have a noun phrase for names likeorigin/mainthat's not ambiguous and doesn't call up false associations."What false associations?" you might wonder, and here's where we get into the essential difference between a branch name and a remote-tracking name. Both locate one specific commit. Both are useful for finding multiple commits "as seen on some branch" (somewhere, in some Git repository, perhaps using the word branch in yet another way), although a raw commit hash is just as good for that in most senses. (The one sense in which it isn't "just as good" is ... well, try typing in raw commit hashes all day. They're just hard for humans to get right. I cut and paste.) But:
You can "get on a branch". With
git switchorgit checkout, giving them a branch name will—if the operation is successful—put you "on the branch". In this attached-HEAD mode, making a new commit will stuff the new commit's hash ID into the branch name.A branch can have an upstream set. The upstream is actually a two-part entity, consisting of a remote name like
origin, and a branch name as seen on that remote, such asrefs/heads/main. Fortunatelygit branch --set-upstream-to=origin/branch branchandgit rev-parse branch@{upstream}let us ignore this two-part business, which largely dates back to the time when "remotes" were first being invented.A branch can have "rebase mode" set for
git pull. That is, when on this particular branch,git pullmeansgit pull --rebase. (This is separate from the global setting.)A branch name lives in
refs/heads/: that is, the full name ofmainisrefs/heads/main. A remote-tracking name lives in therefs/remotes/namespace.All of these do show up at various times, with varying frequency. In particular
git switchrequires--detachwhen used with a remote-tracking name;git checkoutimplies--detachwhen used with a remote-tracking name; in both cases this puts us in "detached HEAD" mode, so that we're on no branch at all.Branch creation options
Creating a new branch, in Git, really consists of two steps (assuming we've already determined that the name is valid and not in use), but there are third and fourth optional steps:
First, we must locate some commit. We need its raw hash ID. Any valid, existing hash ID will do if it's a commit hash ID: tree, tag, and blob hash IDs are forbidden.
Then we just need to create a new ref whose spelling is
refs/heads/name.Optional: We may request that Git set the upstream of this new branch to some name. That name can be a branch name or a remote-tracking name.
Optional: We can even copy more items.
The
--trackor-toption, given togit branch,git checkout -b, orgit switch -c, tells Git that it should definitely do step 3. This requires that we also supply a starting point (though it's not an argument passed to the-toption); the starting point provides the hash ID for step 1 and the name for step 3.(Alas, this gets more complicated starting with Git version 2.35. Since I'm working through history, let's start with the much older history before we add the new thing.)
The
--no-trackoption, given to any of these commands, tells Git that it should definitely not do step 3. We can now provide a starting-point, safe in the knowledge that step 3 won't happen.If we use neither
--tracknor--no-track, the default is that Git will do step three if and only if (a) we provide a starting point and (b) the starting point we provide is a remote-tracking name.Using
git config, however, we can alter two Git settings:branch.autoSetupMergeand/orbranch.autoSetupRebase. Withbranch.autoSetupMergeset toalways, step 3 will happen even if we use a local branch name. That is, step 3 is avoided only if we use a raw hash ID or something else unsuitable (or, of course, use an explicit--no-track). Or, we can set it tofalse: then step 3 never happens. The default (which we can also set) istrue, which selects the "if it's a remote-tracking name" mode.Once we've set
branch.autoSetupMergeas desired, we can setbranch.autoSetupRebase. This sets whethergit pullshould meangit pull --rebase, and as before, it has multiple modes:never,local,remote, andalways; see thegit configdocumentation for further details. (The more interesting thing for me is how this interacts with the newpull.ffsetting, if it's set to something other than the defaultnever.)Once you've digested all of this, it's worth mentioning that
git switch -thas another function. Suppose you have a remote, such asorigin, and it has produced a slew of remote-tracking names in your repository. Thegit switchandgit checkoutcommands have a--guessoption (default = on, including whenever your Git is old enough to lack this as a separate option). With this option enabled,git checkout nameorgit switch namewill, by default, first check to see whethernameexists, and if so attempt to switch to it. But if not, before complaining that there is no such branch name, the command will search through your remote-tracking names. If there's exactly one "obvious match"—for instance, if you asked to switch to the nonexistent branchdevand there's oneorigin/dev—then--guessmeans createdevfromorigin/dev. The usual "tracking" (set or don't set an upstream) rules apply, perbranch.autoSetupMerge.But if you have two remotes—say,
gh1andgh2for two different but related GitHub repositories—you might have agh1/devand agh2/devboth. Thengit switch --guess devdoesn't know which one to use. Usinggit switch -t gh1/devwill create yourdevfrom yourgh1/dev(your Git's memory ofgh1'sdev). Of course, the upstream-setup is forced on here;git switch --no-track gh1/devwill pull the same trick but force upstream-setting off.Before we go on, let's make a few last observations:
The extra argument to
git branchorgit checkout -borgit switch -c, e.g.,git branch newbr startpoint, provides the initial hash ID to put in the new branch name. That is,startpointis parsed, as if bygit rev-parse, for its hash ID. But it's also parsed to see if it's a branch or remote-tracking name for thebranch.autoSetupMergepurposes.If we give Git the string
startpoint^{}orstartpoint^{commit}, the resulting hash ID is that of the same commit we'd get by default, but the string no longer matches a branch or remote-tracking name, because of the suffix. So this automatically defeats theautoSetupMergesetting. It can be used as a one-off.Besides the upstream setting, a branch name can have the rebase setting, so there are actually four steps to creating a new branch, with two of them optional (optionally set an upstream, and optionally set the rebase flag).
Besides the upstream setting, a branch name has a reflog. The reflog contains a history of hash IDs that were stored in the branch name. (Use
git reflog mainorgit reflog masterto dump the reflog for yourmainormasterbranch, to see these.) The "zeroth" entry is the current value.Reflogs can be disabled (though you still have an automatic
@{0}), but are on by default in non-bare repositories. So you probably have reflogs for all your branch names. Reflogs also exist forHEADitself, and you can have a reflog for every reference. Thecore.logAllRefUpdatessetting is what controls whether new reflogs are created as needed; see thegit configdocumentation.Besides the upstream and reflog, every branch can have arbitrary additional settings. There aren't any in Git now but there could be in the future. For instance, you can run
git config branch.main.abc defto setbranch.main.abc = def: it doesn't mean anything, but you can set it.The
-coption togit branchis the copy flag. It also tellsgit branchthat you're creating a new branch, of course, as it makes no sense to copy things. But "create new branch" is the default action forgit branch, if some other action isn't set. Adding-cor--copymeans copy the reflog and all other settings (even the ones Git doesn't know about!). This will copy the upstream setting when "copying" from a local branch, since it's, well, a setting.Now we can also describe the new
--trackflags in Git 2.35:--track=directand--track=inherit. The-toption means--track=direct. Whenbranch.autoSetupMergehas its default value, we only get an upstream set by default when we create a new branch using a remote-tracking name. The remote-tracking name itself is the new branch's upstream. But if we setbranch.autoSetupMergetoalways, we'll get an upstream set withgit branch newbr fooas well as withgit branch newbr origin/foo. Some people disliked the fact that the upstream fornewbris now the (local) branchfoo. They wantedgit branchto readfoo's upstream, and setnewbr's upstream to foo's upstream.This is what
git branch --track=inheritdoes. You must spell out--track=inheritexactly this way. Note that this is also whatgit branch --copy(akagit branch -c) does; it's just that-cdoes a bunch more stuff along the way (copying reflogs plus all settings).Rebase-and-keep
I do this a lot myself. In general, though, I keep only the current version upstream (or no version upstreamed), with all the old versions just in my own repository:
Since I always use the local name (and
git checkout -borgit switch -c) I never wind up with an upstream set, even with the default settings. The next time I rebase, I renamesomebranchtosomebranch.1, and so on.As a nice side effect, when I rename branches like this, any existing upstream setting sticks to the old (but now renamed to .0, .1, etc) branch, which for me means I can't
git pushit because I havepush.defaultset tosimple: the name no longer matches on both sides. Since I create the new branch from the existing branch, it has no upstream set and I can'tgit pushit either.I could just rely on the reflogs: if I did not rename anything at all, the reflog for
somebranchwould have in it the values that wind up insomebranch.0,somebranch.1, and so on. But reflog entries reflect something automatic, rather than some deliberate decision I made. If I'm making substantive changes, I may choose a new name for the branch in the first place.