Thread (6 messages) flat view 6 messages, 3 authors, 2020-12-04

Re: Unexpected behavior with branch.*.{remote,pushremote,merge}

From: Jeff King <hidden>
Date: 2020-12-04 10:14:42

On Thu, Dec 03, 2020 at 06:26:15PM -0700, Ben Denhartog wrote:
I have a few repositories on my system that exist primarily as local copies of remote repositories, in that I normally just want to track and follow the upstream project (however, I periodically contribute back upstream so they are technically forks -- origin is my remote, upstream is theirs).

In these repositories, I set the following configuration:
[remote "origin"]
  url = https://git.foo.com/me/bar.git
  fetch = +refs/heads/*:refs/remotes/origin/*
[remote "upstream"]
  url = https://git.foo.com/them/bar.git
  fetch = +refs/heads/main:refs/remotes/upstream/main
  tagopt = --no-tags
[branch "main"]
  remote = upstream
  pushRemote = origin
  merge = refs/heads/master
  rebase = true
I use a similar setup myself, and it works well for this kind of
triangular flow. A few notes:

  - I assume you mean "merge = refs/heads/main" from your examples

  - instead of branch.*.pushremote, I usually set remote.pushdefault, so
    that it covers all branches (i.e., I'd never want to push to the
    upstream remote, which I do not even have access to).

  - in a workflow like this, I generally have push.default set to
    "current"

  - I make frequent use of @{push} to refer to the matching branch on my
    remote (e.g., after doing some local work, I might use "git
    range-diff upstream @{push} HEAD" to examine the changes before
    pushing them up).
Based on my understanding of the branch configuration options, this
should effectively force my local `main` branch to track against
`upstream/main`, but push to `origin/main`. I notice what I believe to
be odd behavior when fetching: that FETCH_HEAD doesn't resolve to
`upstream/main` as I would expect:

➜ git fetch --all
Fetching origin
Fetching upstream
remote: Enumerating objects: 23, done.
remote: Counting objects: 100% (23/23), done.
remote: Total 32 (delta 23), reused 23 (delta 23), pack-reused 9
Unpacking objects: 100% (32/32), 12.97 KiB | 949.00 KiB/s, done.
From https://git.foo.com/them/bar
   63f7159..e65b80e  main     -> upstream/main
The culprit here is using "git fetch --all". It triggers the sub-fetches
with --append, so they'll each add to FETCH_HEAD instead of overwriting
it.  We do truncate it before the first one, so after this completes it
should have the complete set of refs fetched from both remotes (even if
it was a noop to fetch one of them, anything mentioned in the refspecs
shows up in FETCH_HEAD).

Which is what you're seeing here:
➜ cat .git/FETCH_HEAD
23e6881719f661c37336d9fcf7a9005a7dfce0cf        not-for-merge   branch 'main' of https://git.foo.com/me/foo
e65b80edd2a2162f67120a98e84bb489f15fcf97                branch 'main' of https://git.foo.com/them/foo
FETCH_HEAD is not just a ref, but contains some magic instructions that
get interpreted by git-pull. But when programs besides git-pull try to
resolve it to a single object, they just pick whichever value is first.

So I do think there's a bug there, or at least something not
well-thought-out in the way that "fetch --all" appends. Normally fetch
tries to put the merge branch (or branches) first in the file, exactly
so this naive "take the first one" lookup will do the most sensible
thing. But when running multiple fetches that all append, we get their
individual outputs in the order of fetch (which in turn is the same as
the order in the config file).

Perhaps the parent "git fetch --all" triggering the sub-fetches should
reorder FETCH_HEAD after they've all finished (to pull any merge heads
up to the top, regardless of which remote they came from).

But an obvious workaround, if you know you'll always be merging from
upstream, is to just reorder your config stanzas so that it's fetched
first (likewise you can run "git fetch --multiple upstream origin" to
specify the order manually, or define a group with remotes.<group>).

One reason I suspect nobody has come across this before is that there's
not much reason to use FETCH_HEAD in this setting. If you know you want
to rebase again upstream/main, then there are a number of ways you can
refer to that:

  - upstream/main

  - upstream, if your refs/remotes/upstream/HEAD is set up (if you
    didn't clone, you may want to run "git remote set-head upstream -a")

  - @{upstream}, if it's configured as your upstream (which it is here)

  - @{u}, a shorter synonym

  - nothing at all, since rebasing against the upstream is the default
    for git-rebase these days

  - you can say that (or even just "upstream", if your
    refs/remotes/origin/HEAD is set up)
Curiously, `git rebase FETCH_HEAD` seems to think the local branch is
up to date (erroneously),
That's what I'd expect, since it is doing the naive "what is the first
one in the file" lookup.
however `git-pull --rebase=true` [works]
That is running a new git-fetch under the hood, which will overwrite
FETCH_HEAD (and will only fetch from upstream, since that's what's in
branch.main.remote).
`git-merge FETCH_HEAD` both work as expected and merge/rebase with
`upstream/main`.
I think git-merge, like git-pull, understands the magic FETCH_HEAD
format.
Am I going about this incorrectly? The main purpose behind configuring
my "mostly just a fork" repository is that it simplifies tracking
against an upstream remote for projects which I do not work on
actively. Of course, you might argue that I don't need to keep my
remote around for this purpose and can just use a straightforward
`git-clone` here -- but I'd rather not, and would prefer responses
addressing the perceived bug rather than suggesting this particular
alternative workflow.
I think your workflow is perfectly reasonable.

-Peff
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help