TL;DR
My guess is that you're referring to branch.autoSetupMerge (and perhaps also branch.autoSetupRebase). There is a lot to know about these, though.
Long
There is a great deal of history here, along with some historical mistakes that are necessarily preserved for all time in case people depend on them.1 For the most part, though, these are the central concepts:
A Git repository can have any number of remotes. A remote is just a short name. There's a standard—a social standard at this point, enforced only by defaults in Git—that your first remote is named origin.
This short name stores a URL, and acts as the prefix for remote-tracking names. If you only have one remote, it's wise to name it origin.2 That's simply because git fetch has a fallback: if all else fails, try the hard-coded string origin.
A branch name like main or master can have no upstream, or one upstream. The upstream setting is the one you have mentioned here, that you can set with git branch --set-upstream-to. (The --set-upstream option was one of the mistakes, which is why --set-upstream-to exists here; the --set-upstream option is being phased out. This is only the case for the git branch command: other commands that set an upstream still take --set-upstream or, usually, -u for short.)
Commands that create new branch names—there are multiple such commands—can be told whether and when to set an upstream for the new branch.
I suspect this last item is what you refer to, rather elliptically,3 here:
a mechanism for configuring git to have this behavior was discovered
More to the point, I really have to guess here. The tricky bits here are the behavior of git pull when given no additional arguments, and all of the various configuration knobs, all of which can interact. So, let's look at the configuration knobs themselves. These are items you can set with git config—which sets something for this repository only, and therefore must be run within a specific repository—or git config --global, which sets something in your own personal settings to affect all repositories, and therefore can be run outside of some repository.
(Note that if some setting is set both globally and locally-within-a-repository, the local setting usually overrides. There are exceptions to this rule: for instance, remote.remote.fetch is a cumulative setting, so that local settings just add on to any global or system settings, rather than overriding them.)
With this in mind, there are two settings that you would normally set with git config --global:
branch.autoSetupMerge, and
branch.autoSetupRebase
These two control the next several settings at branch creation time. That is, commands that create a new branch will set—or not set—the following items:
branch.name.remote (contains the remote name, such as origin)
branch.name.merge (contains a branch name: more on this later)
branch.name.rebase (see below)
When you run git pull with no additional arguments, it is these three settings that control what happens. Remember that git pull is short for first, run git fetch; then, if that works, run a second Git command. The second Git command is usually git merge or git rebase.
For concreteness, let's say that we are on branch xyzzy right now, and we run git pull with no arguments. This will run git fetch first. The remote setting, stored in branch.xyzzy.remote, controls which remote Git will use with this git fetch. If it's not set, the fallback is origin (modulo footnote 2).
Assuming this git fetch succeeds, git pull will go on to run its second command. If branch.xyzzy.rebase is set to true,4 the second command will be git rebase. If false, it will be git merge.5 If set to specific strings as described in the git config documentation, it generally means use rebase, but with extra options.
Both rebase and merge need more than git fetch needs, though. In particular, both need a commit. They do not need a branch name, though you can use one here and most people do. They need a commit hash ID. If you give them a branch name, they use git rev-parse—internally anyway—to turn it into a hash ID.6 The pull command, however, runs this second command with a raw hash ID. The interesting issue is this: where does it get this hash ID? We must now return to the git fetch command.
What git fetch does, in a nutshell, is have your Git call up some other Git. Your Git is operating on your repository. Their Git—whoever they are: this part comes from the remote—is operating on their repository. Each repository has its own branch names. Their branch names are theirs, to do with as they will. Your branch names are yours, to do with as you will. The names do not have to match, although for your own personal sanity, you probably will want them to match as often as possible.7
Anyway, for these reasons, when git fetch calls up the other Git, and the other Git lists out its branch names, your Git transforms these branch names. They become your remote-tracking names. Their main or master becomes your origin/main or origin/master, for instance. Your git fetch will obtain, from them, any commits they have that you lack, that you need to update your remote-tracking names. Then, at the end of the git fetch, your Git will update your remote-tracking names.8
The commit hash IDs, for these new commits—and all the old ones—in your repository and in their repository are the same. Commit hash IDs are universal. That's why Git uses them. The branch names are not, which is why your Git stores their branch names as your remote-tracking names. But the branch.name.merge setting here predates the invention of remote-tracking names, so what goes in it is their branch name.
When git fetch finishes successfully, it write a file—.git/FETCH_HEAD—in which it records their branch names and the hash IDs. And then git pull, having run git fetch successfully, fishes these out of the file—or, in modern Git, where git pull is now a C program, takes them from the same in-memory data structures that Git used to write the file—to get the right hash IDs to pass to the second Git command.
So, if it's not too late to say "long story short", the branch.name.merge setting contains the branch name as seen in their Git repository. When git fetch—the first part of git pull—runs, it sees that name; their Git says that that name means some particular commit hash ID. Your Git make sure that you have this hash ID, obtaining any required new-to-you commits, and then uses the second Git command with that hash ID.
1This is a slight exaggeration, but only slight.
2Git is growing a mechanism by which you can choose a different default first name; once you've set this, with git config --global, it would become wise to choose whatever default you have set. The details will depend on the ultimate implementation, but it's likely that git fetch will use this configured name as its fallback.
3I feel some irony here in accusing others of "using or involving ellipsis, especially so as to be difficult to understand". See definition 1 here, from the OED. On the other hand, I like to think of myself more as fluent, loquacious, or at worst somewhat prolix. But like beauty to the eye, verbosity is in the ear of the reader, so to speak.
4A boolean (true/false) setting in Git can also be made to "act true" by setting it to 1, or "act false" by setting it to zero 0. The rebase setting can be any of true, false, 1, or 0 to make it "act boolean", but it can also be set to particular alphabetic strings.
In general, Git configuration items that can be set true or false—even if they can also be set to strings—will have a default setting that is either true or false. However, Git's configuration mechanism is so general that other defaults are possible. You must therefore consult the documentation to be sure.
5In the special case that you are on an unborn branch, the second command from git pull is neither merge nor rebase, but rather a specialized variety of git checkout that creates the branch. There were, in the past, some very bad bugs here: I got burnt badly enough 15+ years ago that I still mostly don't use git pull.
Even with the bugs long fixed, having seen how git pull confused me, and continues to confuse others new to Git, I personally still recommend using git fetch directly, followed by whatever second command you prefer. This also gives you the opportunity to look at what git fetch did before you choose the second command. Sometimes it matters.
6Note, though, that git merge also saves the branch name you give it to make up the default merge log message, if it winds up doing a true merge. The git pull command gives git merge a raw hash ID, and therefore has to (and does) run git merge with the -m option to set a merge message; it includes the remote name and/or URL here. If you split up your git pull into separate git fetch and later git merge operations, you won't get this default. I don't believe that this default has much value, but do be aware of this.
7When dealing with more than one remote, this may be impossible. Suppose, for instance, we have remotes named fred and barney. Both of these Git repositories have a wife branch. Fred's wife is of course Wilma; Barney's is Betty. You can't have, in your own repository, one branch named wife to refer to both Betty and Wilma—and if you're married, using wife to refer to either of these two cartoon women is perhaps unwise. So, you might have wilma as your local branch to work with fred/wife, for instance.
(This phraseology may be antediluvian, but so are Fred and Barney.)
8In very old versions of Git—predating 1.8.4—some git fetch operations fail to update remote-tracking names even though they could. This is yet another reason to run a separate git fetch, although hardly anyone uses such an ancient Git version.
The branch.autoSetup* settings
Keeping all of the above in mind, let's look at how we create new branches.
Suppose that you have one remote, origin. Suppose further that this one remote Git repository, over at origin, has branches named main, develop, feature/short, feature/tall, and xyzzy.
When you clone this repository, you get:
- a new empty repository that, before you get hold of it, has all their commits but no branches at all, but then
- as the last part of
git clone, your Git creates one branch in this new repository of yours.
The branch your git clone creates is the one you tell it to, with your -b flag, at git clone time. If you don't provide a -b flag, your Git asks their Git for a recommendation: which branch should I create? They recommend whichever branch their HEAD says. (If you're running a GitHub or similar repository for others to clone, there's a web page and/or API to set this so that you can provide the right recommendation.) Let's say they recommend main.
Regardless of your configuration settings, this new main branch—created by git clone—has its branch.main.remote set to origin, its branch.name.merge set to main, and its branch.name.rebase not configured (so that it defaults to false).
Now that your repository exists, though, you can enter it:
cd newrepo
and create new branches. One of the commands that creates a new branch is git checkout (or git switch):
git checkout develop
This seems to be a request to check out the existing branch named develop. But there is no existing branch named develop. Here, what happens is a bit complicated—and hits a bug in Git versions predating 2.23—but usually what happens is that we invoke what Git calls the Do What I Mean code in git checkout (or git switch).
This code checks to see if we have a remote-tracking branch that looks enough like develop to act as a source for a new (local) branch name develop. And, since we have exactly one such name—origin/develop, which our git clone made when it renamed all their branch names—we do!
This invokes (internally, as it's all been coded in C for a long time now) git branch to create the name develop. This git branch command can be told set origin/develop as the upstream or do not set an upstream.
The upstream setting of the new branch develop is composed of two of those three settings we've been talking about: branch.develop.remote and branch.develop.merge. The remote part is to be set to origin, because that's the remote under which we find the remote-tracking names. The merge part is to be set to refs/heads/develop, because that's the full name of the branch as seen in the other Git over at origin.
The argument to git branch to tell it whether to set this upstream is spelled --track: this is a historical artifact, from before the days of remote-tracking names and formalized upstream settings. If branch.autoSetupMerge is true—which is the default—then this kind of DWIM branch-creation operation uses git branch --track.
There are other settings you can set here besides true and false. See the git config documentation for details.
If you like git pull and wish to use it, but have it run git rebase as its second command, you should consider setting the branch.autoSetupRebase setting as well.
Note that these affect both DWIMmed branch creation operations and git branch. However, in order to affect git branch, you must create the branch with a remote-tracking name as the starting point. The git branch command can be used to create a branch in two different ways:
git branch newbranch
means, in effect:
git branch newbranch HEAD
which resolves HEAD to a commit hash ID (see the git rev-parse documentation) and does not set an upstream. But:
git branch newbranch origin/xyzzy
resolves origin/xyzzy to a commit hash ID. Since this resolution goes through the remote-tracking name mechanisms, git branch will set an upstream for this branch: newbranch will have its upstream set to origin/xyzzy.
This all depends on branch.autoSetupMerge, but that setting defaults to true. If you have changed it to false, you'll need explicit --track arguments, or separate git branch --set-upstream-to invocations.