Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 50 additions & 21 deletions scripts/bump-leg.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@
the remote has deleted no longer counts, and moves no local branch of
the leg (#898). It carries no refspec of the leg's own that can bring a
tag under `origin/`, where it reads branches, so a tag never answers for
one there (#1037). Where a TAG of the remote reaches a commit the leg
holds and no branch does, or the leg has no remote named `origin`, the
refusal says so instead of "never pushed" or "fix the network".
one there (#1037), nor one that puts a branch on another branch's name
there, which failed the fetch where the leg's own works (#1066). Where
a TAG of the remote reaches a commit the leg holds and no branch does,
or the leg has no remote named `origin`, the refusal says so instead of
"never pushed" or "fix the network".
* a dirty root. The commit is made with explicit pathspecs, so unrelated
edits would not be swept in — but a lockstep commit that cannot be read as
exactly one change is the thing the rule exists to keep readable.
Expand Down Expand Up @@ -1052,14 +1054,15 @@ def on_a_remote_branch(submodule: Path, commit: str) -> bool:


def _leg_fetch_refspecs(submodule: Path) -> list[str]:
"""The refspecs the leg's fetch carries (#898, #1037): every branch of
the remote, then what the leg's own `remote.origin.fetch` keeps under
`refs/remotes/origin/`, and none of what it excludes, nor one whose
source can name a tag, nor what gives another namespace the names the
branches take there. Which of the leg's are carried is
`shape_materialize`'s `origin_fetch_refspecs`, the rule `family.py`'s
`_origin_fetch_refspecs` asks of a member's mount (#686, #905, #1009);
its docstring says which clause leaves out what, and why.
"""The refspecs the leg's fetch carries (#898, #1037, #1066): every
branch of the remote, then what the leg's own `remote.origin.fetch`
keeps under `refs/remotes/origin/`, and none of what it excludes, nor
one whose source can name a tag, nor one onto the names the branches
take there, nor one that puts a branch under another name. Which of the
leg's are carried is `shape_materialize`'s `origin_fetch_refspecs`, the
rule `family.py`'s `_origin_fetch_refspecs` asks of a member's mount
(#686, #905, #1009); its docstring says which clause leaves out what,
and why.

WHY THE LEG'S OWN ARE ADDED. The fetch prunes, and `--prune` deletes
every ref under `refs/remotes/origin/` that no refspec on the command
Expand Down Expand Up @@ -1093,18 +1096,42 @@ def _leg_fetch_refspecs(submodule: Path) -> list[str]:
under `origin/` is pruned, and the leg's own next `git fetch origin`
writes it back.

A REFSPEC THAT PUTS A BRANCH UNDER ANOTHER NAME IS LEFT OUT (#1066),
and so is every one onto the branches' names, whatever its source: a
folder of branches onto them
(`+refs/heads/release/*:refs/remotes/origin/*`), one branch onto
another's (`+refs/heads/release/x:refs/remotes/origin/x`), and every
branch into a folder (`+refs/heads/*:refs/remotes/origin/all/*`, `x`
onto `all/x`). #946's rule carried each, and this fetch with it since
#1037: where the remote had both branches, `x` and `release/x`, the
fetch exited 128 ("Cannot fetch both refs/heads/x and
refs/heads/release/x to refs/remotes/origin/x"), where the leg's own
`git fetch origin` exits 0, so both runs gave the failed-fetch note and
answered from stale refs, through `origin` and through
`--local-remote-dir` (measured, git 2.43). Left out, the first refspec
still brings each such branch under its own name, so the answer loses
nothing; what the leg kept under the other name holds the branch of
that name, or is pruned, until the leg's own next `git fetch origin`.

A NEGATIVE REFSPEC (`^refs/heads/wip`) IS SKIPPED. A narrowing refspec
never decides reachability: a positive one that leaves a branch out is
overridden by the first refspec, and git applies a configured negative
one only to a fetch with no refspec of its own (measured, git 2.43), so
the excluded branch is fetched and counts like any other.

WHERE GIT CANNOT SATISFY A REFSPEC OF THE LEG'S OWN, THE FETCH FAILS,
where the fetch of every branch alone worked: one naming a ref the
remote no longer has (`couldn't find remote ref`, as the person's own
`git fetch origin` fails). That exits 128, so the note follows, the
stale-refs sentence after it where the refusal has one, and nothing is
pruned (measured, git 2.43).
WHERE GIT CANNOT SATISFY A REFSPEC OF THE LEG'S OWN THAT IS CARRIED, THE
FETCH FAILS, where the fetch of every branch alone worked. Two ways
remain: one naming a ref the remote no longer has (`couldn't find
remote ref`, as the person's own `git fetch origin` fails), and one
carried into a folder of its own, such as pull requests in
`origin/pr/`, where a branch's name falls in that folder (`pr/7` beside
pull request 7: "Cannot fetch both refs/heads/pr/7 and
refs/pull/7/head"), which the leg's own fetch fails too wherever it has
the branches' glob, and which a leg with no such refspec of its own
fails here alone. That exits 128, so the note follows, the stale-refs
sentence after it where the refusal has one, and nothing is pruned
(measured, git 2.43). A branch put under another name was a third way
until #1066, and is left out now.

READ BY A LISTING, `git config --list`, which exits 0 whether or not the
leg has a refspec (an `origin` with a URL and nothing else has none, and
Expand Down Expand Up @@ -1314,9 +1341,10 @@ def fetch_leg(submodule: Path, repository: str, commit: str,
(`_leg_fetch_refspecs`), so what its configuration keeps there stays,
but for what a refspec it leaves out writes, a tag under `origin/`
among it (#1037), which is pruned until the leg's own next `git fetch
origin`. `--no-prune-tags` is belt and braces: git 2.43 applies no
`pruneTags` setting to a fetch with refspecs on its command line
(measured). `origin/HEAD` stays. Offline nothing is pruned. Under
origin`, as is a branch the leg keeps under another name (#1066) where
no branch has that name. `--no-prune-tags` is belt and braces: git 2.43
applies no `pruneTags` setting to a fetch with refspecs on its command
line (measured). `origin/HEAD` stays. Offline nothing is pruned. Under
`--local-remote-dir` the prune deletes the `origin/*` tracking refs the
stand-in directory lacks, a branch the leg's real `origin` still has
included; that is deliberate, as the fetch has always written that
Expand All @@ -1332,7 +1360,8 @@ def fetch_leg(submodule: Path, repository: str, commit: str,
such a branch failed the fetch with "refusing to fetch into branch". The
configured refspecs that write under `origin/` are on the command line
already, but for those `_leg_fetch_refspecs` leaves out, whose refs the
prune drops in any case, so nothing else is lost, and the fetch writes
prune drops in any case, or the first refspec writes with the branch of
that name (#1066), so nothing else is lost, and the fetch writes
nothing outside `refs/remotes/origin/` and the tags git follows.

WHERE IT CANNOT ANSWER, THE REFUSAL SAYS WHY (#898), in the code it had.
Expand Down
18 changes: 12 additions & 6 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -8733,11 +8733,12 @@ def _origin_fetch_refspecs(member: Path) -> list[str]:
"""The refspecs `bump`'s fetch of `origin` carries (#686): every branch of
`origin`, then what the mount's own `remote.origin.fetch` keeps under
`refs/remotes/origin/`, and none of what it excludes, nor one whose
source can name a tag, nor what gives another namespace the names the
branches take there (#905, #1009). Which of the mount's are carried is
`shape_materialize`'s `origin_fetch_refspecs`, the one rule
`bump-leg.py` asks of a leg's fetch too (#1037); its docstring says
which clause leaves out what, and why.
source can name a tag, nor one onto the names the branches take there
(#905, #1009), nor one that puts a branch under another name, which
failed this fetch where `git fetch origin` works (#1066). Which of the
mount's are carried is `shape_materialize`'s `origin_fetch_refspecs`,
the one rule `bump-leg.py` asks of a leg's fetch too (#1037); its
docstring says which clause leaves out what, and why.

WHY THE MOUNT'S OWN ARE ADDED. `--prune` deletes every ref under
`refs/remotes/origin/` that no refspec on the command line maps a ref of
Expand Down Expand Up @@ -8769,7 +8770,12 @@ def _origin_fetch_refspecs(member: Path) -> list[str]:
A mount whose refspec git cannot satisfy, one that names a ref `origin`
lacks or two that map to one destination, fails this fetch as it fails
the person's own `git fetch origin`, and `bump` says so as it says any
failed fetch.
failed fetch. So does one carried into a folder of its own where a
branch's name falls in that folder (pull requests in `origin/pr/`
beside a branch `pr/7`), which the person's own fetch fails too
wherever the mount has the branches' glob, as `add` leaves it, and
which this fetch alone fails where it has not. A branch put under
another name failed this fetch alone until #1066, and is left out now.

READ BY A LISTING, `git config --list -z`, not `--get-all`: the dry
run's test holds every `git config` it runs to a listing, the one form
Expand Down
128 changes: 104 additions & 24 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -1555,7 +1555,7 @@ def readable_branch(what: str, value) -> str:


# ---------------------------------------------------------------------------
# What a fetch for a branch's answer carries (#686, #898, #1037)
# What a fetch for a branch's answer carries (#686, #898, #1037, #1066)
# ---------------------------------------------------------------------------
#
# `family.py bump` fetches a member's `origin` (#686), and `bump-leg.py` a
Expand All @@ -1580,15 +1580,22 @@ def readable_branch(what: str, value) -> str:
ORIGIN_BRANCHES = "+refs/heads/*:refs/remotes/origin/*"

#: The names `ORIGIN_BRANCHES` gives the branches (#905): a refspec of the
#: repository's own that gives another namespace these very names is not
#: carried (`origin_fetch_refspecs`).
#: repository's own onto these very names is not carried, whether it gives
#: them another namespace's refs (#905) or a folder of branches (#1066)
#: (`origin_fetch_refspecs`).
ORIGIN_BRANCH_NAMES = ORIGIN_BRANCHES.partition(":")[2]

#: Where a repository keeps its tags: a refspec of the repository's own
#: whose source can name a ref here is not carried (`can_name_a_tag`,
#: #1009).
TAGS_PREFIX = "refs/tags/"

#: Where a repository keeps its branches, the source of `ORIGIN_BRANCHES`:
#: a refspec of the repository's own whose source is here is carried only
#: where it gives each branch the name `ORIGIN_BRANCHES` gives it
#: (`gives_a_branch_another_name`, #1066).
HEADS_PREFIX = "refs/heads/"


def can_name_a_tag(source: str) -> bool:
"""Whether a fetch refspec's `source` can name a tag of the remote, so
Expand Down Expand Up @@ -1629,6 +1636,44 @@ def can_name_a_tag(source: str) -> bool:
return not source.startswith("refs/")


def gives_a_branch_another_name(source: str, destination: str) -> bool:
"""Whether a fetch refspec from `source` to `destination` puts a branch
of the remote under a name `ORIGIN_BRANCHES` does not give it, so that
the fetch does not carry the repository's refspec that has it
(`origin_fetch_refspecs`; #1066).

A SOURCE UNDER `refs/heads/` NAMES BRANCHES, and the first refspec
brings every branch already, under `refs/remotes/origin/` and its own
name. Such a refspec is carried only where its destination is that very
name, `refs/remotes/origin/` and the source's part after `refs/heads/`,
a pattern's `*` in the same place: one branch
(`+refs/heads/main:refs/remotes/origin/main`) or a folder of them
(`+refs/heads/release/*:refs/remotes/origin/release/*`). It maps each
branch it takes where the first refspec maps it, so it clashes with
nothing, and a branch it names that the remote lacks fails the fetch as
it fails the repository's own (`couldn't find remote ref`).

ANY OTHER PUTS A BRANCH ON THE NAME OF ANOTHER, the name the first
refspec gives the branch of that name: a folder onto the branches'
names (`+refs/heads/release/*:refs/remotes/origin/*`, `release/x` onto
`x`), one branch onto another's
(`+refs/heads/release/x:refs/remotes/origin/x`), every branch into a
folder (`+refs/heads/*:refs/remotes/origin/all/*`, `x` onto `all/x`),
or a folder into another
(`+refs/heads/release/*:refs/remotes/origin/rel/*`). Carried, each
clashed with the first refspec wherever the remote has both branches:
the fetch exited 128 ("Cannot fetch both refs/heads/x and
refs/heads/release/x to refs/remotes/origin/x") where the repository's
own `git fetch origin`, which has no such first refspec, exits 0
(measured, git 2.43, each). A source not under `refs/heads/` names no
branch, and is not asked here.
"""
if not source.startswith(HEADS_PREFIX):
return False
return destination != ORIGIN_BRANCH_NAMES.replace(
"*", source[len(HEADS_PREFIX):])


def origin_fetch_refspecs(configured: list[str]) -> list[str]:
"""The refspecs a fetch for a branch's answer carries: `ORIGIN_BRANCHES`,
then each value of the repository's own `remote.origin.fetch` in
Expand All @@ -1644,14 +1689,15 @@ def origin_fetch_refspecs(configured: list[str]) -> list[str]:
remote-tracking ref only mirrors the remote, and a refspec without it
would fail the fetch on a ref force-pushed.

THREE CLAUSES LEAVE A REFSPEC OUT, asked in this order, and the first
FOUR CLAUSES LEAVE A REFSPEC OUT, asked in this order, and the first
that matches is the one that leaves it out: a destination OUTSIDE
`refs/remotes/origin/`, A SOURCE THAT CAN NAME A TAG, and THE BRANCHES'
GLOB.
`refs/remotes/origin/`, A SOURCE THAT CAN NAME A TAG, THE BRANCHES'
GLOB, and A BRANCH UNDER ANOTHER NAME.

A DESTINATION OUTSIDE `refs/remotes/origin/`, a mirror's `+refs/*:refs/*`
or `+refs/heads/*:refs/heads/*`, is never carried, so no refspec the
fetch names writes outside it.
fetch names writes outside it. The `/` is the clause's: a destination
in `refs/remotes/originx/` is outside it too.

A SOURCE THAT CAN NAME A TAG IS LEFT OUT, wherever under `origin/` it
writes (#905, #459's U183, RV946's S1, #1009, #1037): a tag is not a
Expand Down Expand Up @@ -1680,20 +1726,53 @@ def origin_fetch_refspecs(configured: list[str]) -> list[str]:
fetch maps, and the repository's own next `git fetch origin` writes it
back. A refspec that can name no tag and writes a folder of its own
under `origin/` (pull requests, `+refs/pull/*/head:
refs/remotes/origin/pr/*`) still rides along.

THE BRANCHES' GLOB: ONE THAT GIVES ANOTHER NAMESPACE THE BRANCHES' NAMES
IS LEFT OUT (#905, #459's U183), a refspec whose destination is the
first refspec's own, `refs/remotes/origin/*`, and whose source is not
under `refs/heads/`. The tags onto the branches' names and the mirror
into `origin/` meet it too, and the clause before has left them out
already; this one alone leaves out a source that can name no tag, such
as pull requests onto the branches' names
(`+refs/pull/*/head:refs/remotes/origin/*`). Carried, that clashed with
the first refspec wherever a pull request and a branch share a name
("Cannot fetch both refs/heads/7 and refs/pull/7/head to
refs/remotes/origin/7") where `git fetch origin` exits 0 (measured, git
2.43).
refs/remotes/origin/pr/*`) still rides along, unless it puts a branch
under another name (below).

THE BRANCHES' GLOB: A REFSPEC ONTO IT IS LEFT OUT, WHATEVER ITS SOURCE
(#905, #459's U183, #1066): its destination is the first refspec's own,
`refs/remotes/origin/*`, so each ref it takes lands on the name the
first refspec gives the branch of that name. From another namespace it
gives other refs the branches' names: pull requests
(`+refs/pull/*/head:refs/remotes/origin/*`) clashed with the first
refspec wherever a pull request and a branch share a name ("Cannot
fetch both refs/heads/7 and refs/pull/7/head to
refs/remotes/origin/7"). From a folder of branches it gives a branch
another's name: `+refs/heads/release/*:refs/remotes/origin/*` clashed
wherever the remote has `x` and `release/x` ("Cannot fetch both
refs/heads/x and refs/heads/release/x to refs/remotes/origin/x"). Each
exited 128 where `git fetch origin` exits 0 (measured, git 2.43). #946
left out only a source not under `refs/heads/`, so the folder of
branches was carried, and failed both tools' fetch, until #1066. The
tags onto the branches' names and the mirror into `origin/` meet this
clause too, and the clause before has left them out already; the
branches' own glob is the first refspec.

A BRANCH UNDER ANOTHER NAME IS LEFT OUT (#1066): a source under
`refs/heads/` whose destination is not the name the first refspec gives
each branch it takes, such as one branch onto another's
(`+refs/heads/release/x:refs/remotes/origin/x`) or every branch into a
folder (`+refs/heads/*:refs/remotes/origin/all/*`, `x` onto `all/x`).
`gives_a_branch_another_name` says which, and how each clashed. One
that gives each branch its own name, a branch or a folder of them,
still rides along.

LEFT OUT BY EITHER OF THE LAST TWO, NOTHING IS LOST TO THE ANSWER: the
first refspec brings every branch under its own name, so a commit a
branch of the remote reaches is reached there, and the fetch works.
What the repository kept under another name is pruned, or holds the
branch of that name, until its own next `git fetch origin` writes it
back, as for a tag refspec left out; there a refspec written without
its `+` is refused as non-fast-forward where the two commits are not in
line, as one of the tags onto the branches' names has been since #905
(measured, git 2.43).

WHAT CAN STILL CLASH is a refspec carried into a folder of its own from
another namespace, such as the pull requests in `origin/pr/`, where the
remote has a branch whose name falls in that folder (`pr/7` beside pull
request 7). The repository's own fetch clashes there too wherever it
has the branches' glob, as a clone's default refspec is; left out, what
it keeps would be pruned on every run, which is why #686 carries it.
"""
refspecs = [ORIGIN_BRANCHES]
for spec in (value.strip() for value in configured):
Expand All @@ -1705,9 +1784,10 @@ def origin_fetch_refspecs(configured: list[str]) -> list[str]:
continue
if can_name_a_tag(source):
continue # a tag is not a branch (RV946's S1, #1009, #1037)
if (destination == ORIGIN_BRANCH_NAMES
and not source.startswith("refs/heads/")):
continue # it gives other refs the branches' names (U183)
if destination == ORIGIN_BRANCH_NAMES:
continue # it lands on the branches' own names (U183, #1066)
if gives_a_branch_another_name(source, destination):
continue # the first refspec brings that branch already (#1066)
if spec not in refspecs:
refspecs.append(spec)
return refspecs
Expand Down
Loading
Loading