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
43 changes: 34 additions & 9 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@
make_folder, refuse_unless_not_found, refuse_unmakeable_folder,
target_in_the_way, target_remediation,
)
# How the READMEs' `git clone` line spells the clone URL, one word (#693),
# in the quoting `shell_line` gives the NEXT STEPS clone (#486).
from repo_shape import shell_quoted # noqa: E402

#: How a refusal names each value the manifest records (#258, #261). The leg
#: paths are not here: `refuse_bad_leg_paths` asks for them (#179, #225).
Expand Down Expand Up @@ -902,18 +905,34 @@ def _refuse_unwritable_values(fields) -> None:
#: What a `scaffold-unwritable-value` for the clone URL is told to do (#433).
#: It is not `UNWRITABLE_REMEDIATION`, which says to retype a value: the path
#: is a directory, so the way out is another directory, or GitHub.
#:
#: THE URL IS THE PATH `--local-remote-dir` RESOLVES TO (#693), so one typed
#: as text can lead through a symbolic link to a directory whose name is not,
#: or be relative to one; told only to pass a path that is "one line of UTF-8
#: text", which it was, a person renames the link and is refused again, so
#: the remedy says where the fault is. And as `UNWRITABLE_REMEDIATION` says
#: (#282), a lone surrogate, which a Windows name can hold, cannot be
#: written either: the READMEs quote the URL for a shell (#693) and write
#: every character of it as itself but an apostrophe.
CLONE_URL_REMEDIATION = (
"Remediation: nothing was created, and `--dry-run` refuses the same. "
"Pass a --local-remote-dir whose path is one line of UTF-8 text with no "
"control character in it, a directory renamed or made anew, and run "
"again; or scaffold against GitHub, without --local-remote-dir, where "
"the clone URL is `https://github.com/<org>/<name>.git`. The READMEs "
"print the clone URL as it stands: a byte that is not UTF-8 in the path "
"(a directory named in a legacy code page) cannot be written at all, a "
"line break or a control character would break or garble the line it is "
"written into, and a bidirectional embedding, override or isolate "
"control would show the rest of that line in another order than it is "
"written.")
"the clone URL is `https://github.com/<org>/<name>.git`. The clone URL "
"is the path --local-remote-dir resolves to, so where the path given is "
"text and the URL named above is not, the fault is in what it leads "
"through: a symbolic link on it whose target is not text, or, for a "
"relative path, the directory it was given from. Rename or replace the "
"directory whose name is not text, or pass a path that does not lead "
"through it; renaming a link changes nothing. The READMEs print the "
"clone URL in a `git clone` line, in quotes where a shell needs them, "
"every character but an apostrophe written as itself: a byte that is "
"not UTF-8 in the path (a directory named in a legacy code page), or a "
"lone surrogate, cannot be written at all, a line break or a control "
"character would break or garble the line it is written into, and a "
"bidirectional embedding, override or isolate control would show the "
"rest of that line in another order than it is written.")


def _refuse_an_unwritable_clone_url(local: bool, url: str) -> None:
Expand Down Expand Up @@ -1854,8 +1873,14 @@ def _scaffold(args) -> int:
"SHAPE_COMMIT": shape_commit,
"SHAPE_TREE_SHA256": shape_tree,
"DIGEST_DEFINITION": TREE_DIGEST_DEFINITION,
"CLONE_URL": urls["assembly"],
"ASSEMBLY_CLONE_URL": urls["assembly"],
# ONE WORD OF A COMMAND A PERSON PASTES (#693): every README prints
# it in `git clone --recurse-submodules <url>`, and under
# `--local-remote-dir` it is a path, which was rendered raw, so one
# under `a b/` split the line when pasted. POSIX quoting whatever the
# host, as `_next_steps_clone` prints it (#532): a README is a
# committed file read on every OS. A GitHub URL is left bare.
"CLONE_URL": shell_quoted(urls["assembly"], "posix"),
"ASSEMBLY_CLONE_URL": shell_quoted(urls["assembly"], "posix"),
"NEUTRAL_PRODUCT_PINS": _neutral_product_pins(pins),
"ASSEMBLY_NAMING": naming_block(policy, names["assembly"], "assembly",
declared_pins, chain=chain),
Expand Down
127 changes: 117 additions & 10 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,22 +1669,79 @@ def _holder_remote(args, family: str) -> tuple[bool, str, str]:
return local, repository, url


#: What every `family-unwritable-value` for a clone URL made under
#: `--local-remote-dir` tells a person to do, `init`'s for the holder (#433)
#: and `add`'s for a member (#693) alike: another directory, or GitHub, as
#: the scaffold's twin says (`CLONE_URL_REMEDIATION`), with the verb and the
#: last name of the GitHub URL filled in.
#:
#: THE URL IS THE PATH THE FLAG RESOLVES TO (#693). A `--local-remote-dir`
#: typed as text can lead through a symbolic link to a directory whose name
#: is not, or be relative to one, and the URL a refusal names is then not
#: the path that was typed. Told only to pass a path that is "one line of
#: UTF-8 text", which it already was, a person renames the link and is
#: refused again, so this says where the fault is.
#:
#: WHAT THE DIRECTORY MUST HOLD IS THE CALLER'S (`{holding}`, #693). `init`
#: makes the holder's bare repository there, so a directory made anew is a
#: way out of its refusal. `add` reads the member's bare `<Project>.git`
#: from it (`_probe_member`), so a new, empty directory is refused again
#: with `does not exist`: its remedy says the directory must hold the
#: repository, renamed or moved, as lane 1's review of #812 measured.
LOCAL_REMOTE_DIR_REMEDY = (
"Pass a --local-remote-dir whose path is one line of UTF-8 text with no "
"control character in it{holding}, and run again; or run `{verb}` "
"against GitHub, without --local-remote-dir, "
"where the clone URL is `https://github.com/<org>/<{name}>.git`. The "
"clone URL is the path --local-remote-dir resolves to, so where the path "
"given is text and the URL named above is not, the fault is in what it "
"leads through: a symbolic link on it whose target is not text, or, for "
"a relative path, the directory it was given from. Rename or replace "
"the directory whose name is not text, or pass a path that does not "
"lead through it; renaming a link changes nothing.")

#: What a `family-unwritable-value` for the holder's clone URL is told to do
#: (#433): another directory, or GitHub, as the scaffold's twin says
#: (`CLONE_URL_REMEDIATION`).
#: (#433). The holder's `README.md` quotes the URL for a shell (#693) and
#: writes every character of it as itself but an apostrophe, so a lone
#: surrogate, which a Windows name can hold, cannot be written either, as
#: #282's `UNWRITABLE_REMEDIATION` in `scaffold-project.py` says (#693).
CLONE_URL_REMEDIATION = (
"Remediation: nothing was created, and `--dry-run` refuses the same. "
"Pass a --local-remote-dir whose path is one line of UTF-8 text with no "
"control character in it, a directory renamed or made anew, and run "
"again; or run `init` against GitHub, without --local-remote-dir, where "
"the clone URL is `https://github.com/<org>/<Family>.git`. The holder's "
"`README.md` prints the clone URL as it stands: a byte that is not UTF-8 "
"in the path (a directory named in a legacy code page) cannot be "
+ LOCAL_REMOTE_DIR_REMEDY.format(
verb="init", name="Family",
holding=", a directory renamed or made anew")
+ " The holder's `README.md` prints the clone URL in a `git clone` "
"line, in quotes where a shell needs them, every character but an "
"apostrophe written as itself: a byte that is not UTF-8 in the path (a "
"directory named in a legacy code page), or a lone surrogate, cannot be "
"written at all, a line break or a control character would break or "
"garble the line it is written into, and a bidirectional embedding, "
"override or isolate control would show the rest of that line in "
"another order than it is written.")

#: What a `family-unwritable-value` for a member's clone URL is told to do
#: (#693). `git submodule add` records the URL in the holder's `.gitmodules`
#: as it is given (a line break escaped as `\n`, every other character
#: raw), and the holder's `make siblings` reads it back through `git config
#: --list` as UTF-8 text, a line a value. Measured on main: `caf<e9>` came
#: back as `caf\ufffd`, a line break cut the URL short at it, and an escape
#: and a bidi override came back as themselves.
MEMBER_URL_REMEDIATION = (
"Remediation: `--dry-run` refuses the same. "
+ LOCAL_REMOTE_DIR_REMEDY.format(
verb="add", name="Project",
holding=" and that holds the member's bare repository "
"`<Project>.git`, its directory renamed or the repository "
"moved")
+ " The holder's `.gitmodules` records the clone URL as git is given "
"it, and the holder's `make siblings` reads it back as UTF-8 text, a "
"line a value: a byte that is not UTF-8 in the path (a directory named "
"in a legacy code page), or a lone surrogate, reads back as another "
"path, a line break ends the URL where it falls, a control character "
"garbles the line it is shown in, and a bidirectional embedding, "
"override or isolate control shows the rest of that line in another "
"order than it is written.")


def _refuse_an_unwritable_clone_url(local: bool, url: str) -> None:
"""Refuse the holder's clone URL when no file can hold it, before any
Expand Down Expand Up @@ -1718,6 +1775,38 @@ def _refuse_an_unwritable_clone_url(local: bool, url: str) -> None:
CLONE_URL_REMEDIATION)


def _refuse_an_unwritable_member_url(local: bool, url: str) -> None:
r"""Refuse a member's clone URL that the holder's `.gitmodules` cannot
hold as text, before the member is read or anything is mounted, and
under `--dry-run` alike, as `init` refuses the holder's (#433, #693).

Only a local one can be refused: under `--local-remote-dir` the URL is
the bare repository's own path (`_member_url`), which `git submodule add`
writes into `.gitmodules`; against GitHub it is
`https://github.com/<org>/<Project>.git`, ASCII by construction
(`MEMBER_RE`). On main `add --local-remote-dir <dir>/caf<e9>` exited 0,
dry run and real, and committed the byte into `.gitmodules`, where the
holder's `make siblings` read the URL as `caf\ufffd`, another path than
the one the holder is pinned to; a line break, an escape and a bidi
override were committed too (#693).

`init`'s question (`unwritable_reason`) and `init`'s code
(`family-unwritable-value`), with `add`'s own words for where the URL
goes (`MEMBER_URL_REMEDIATION`).
"""
if not local:
return
reason = unwritable_reason(url)
if reason:
raise Refusal(
"family-unwritable-value",
"under --local-remote-dir the member's clone URL is the bare "
f"repository's own path, {url!r}, which `add` writes into the "
f"holder's `.gitmodules`, and {reason}. No git command that "
"writes was run and nothing was written.",
MEMBER_URL_REMEDIATION)


def _shape_snapshot() -> tuple[str, str]:
"""`(shape_commit, shape_tree)` of this checkout, warning to stderr when
it is dirty.
Expand Down Expand Up @@ -1750,6 +1839,18 @@ def _template_values(args, family: str, family_id: str, repository: str,
template.

Split from `cmd_init` for #136.

THE CLONE URL IS ONE WORD OF A COMMAND A PERSON PASTES (#693). The
holder's `README.md` prints it in `git clone --recurse-submodules
{{CLONE_URL}}`, and under `--local-remote-dir` it is a path, which was
rendered raw: under `a b/` the line split in two when pasted, and a path
holding `$`, `'`, `;` or `&` broke it too. It is spelled by
`shell_quoted` in the POSIX dialect whatever the host, since the README
is a committed file read on every OS and pasted commands are for a POSIX
shell (#532); a GitHub URL and an ordinary POSIX path are left bare (a
Windows path, spelled with `\\`, is quoted). Only this value is quoted:
the block's other placeholder, `{{FAMILY}}` in `cd {{FAMILY}}`, is a
name the naming policy spells in letters and digits.
"""
return {
"FAMILY": family,
Expand All @@ -1761,7 +1862,7 @@ def _template_values(args, family: str, family_id: str, repository: str,
"TRACKING_BRANCH": args.tracking_branch,
"CREATED_BY": created_by,
"CREATED_ON": args.created_on or _dt.date.today().isoformat(),
"CLONE_URL": url,
"CLONE_URL": shell_quoted(url, "posix"),
"SHAPE_REPOSITORY": SHAPE_REPOSITORY,
"SHAPE_COMMIT": shape_commit,
"SHAPE_TREE_SHA256": shape_tree,
Expand Down Expand Up @@ -4440,7 +4541,9 @@ def cmd_add(args) -> int:
file, `_checked_mount_path`, #612), the paths the sparse rules leave out
(`_checked_inside_sparse_rules`, #612), then the branch the commit lands
on, which the real run refuses on the tracking branch or none
(`_checked_branch`, #487) and a dry run only reports, then the member's
(`_checked_branch`, #487) and a dry run only reports, then the clone URL
`--local-remote-dir` makes, which `.gitmodules` will hold
(`_refuse_an_unwritable_member_url`, #693), then the member's
(`_probe_member`, of a throwaway clone), then the member's id against the
rows (`_checked_member_id`, #330), and only then the mount, the pin and
the commit (`_mount_and_pin`), inside a `MountRollback` that puts the
Expand Down Expand Up @@ -4473,6 +4576,10 @@ def cmd_add(args) -> int:
suggested)

url = _member_url(repository, args.local_remote_dir)
# THE CLONE URL IS A PATH `.gitmodules` WILL HOLD (#693): asked as
# `init` asks the holder's (#433), before the member is read or anything
# is mounted, and `--dry-run` refuses alike.
_refuse_an_unwritable_member_url(args.local_remote_dir is not None, url)
protocol = FILE_PROTOCOL if args.local_remote_dir is not None else []
# FLUSHED, so that in a log holding both streams the line saying what
# was being done comes before a refusal of it, not after.
Expand Down
48 changes: 34 additions & 14 deletions tests/test_clone_url_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,27 +73,45 @@
BAD_IDS = [row[0] for row in BAD_DIRECTORIES]

#: What the refusal tells a person to do, which is fixed text and is the same
#: in every case: the way out is another directory, or GitHub.
#: in every case: the way out is another directory, or GitHub. Since #693 it
#: says that the URL is the RESOLVED path, so that a path typed as text that
#: leads to one that is not is told where the fault is, and it names a lone
#: surrogate among the reasons, as #282's remedy does.
WHY_IT_IS_REFUSED = (
"a byte that is not UTF-8 in the path (a directory named in a legacy "
"code page) cannot be written at all, a line break or a control "
"character would break or garble the line it is written into, and a "
"bidirectional embedding, override or isolate control would show the "
"rest of that line in another order than it is written.")
"code page), or a lone surrogate, cannot be written at all, a line break "
"or a control character would break or garble the line it is written "
"into, and a bidirectional embedding, override or isolate control would "
"show the rest of that line in another order than it is written.")
THE_RESOLVED_PATH = (
"The clone URL is the path --local-remote-dir resolves to, so where the "
"path given is text and the URL named above is not, the fault is in what "
"it leads through: a symbolic link on it whose target is not text, or, "
"for a relative path, the directory it was given from. Rename or replace "
"the directory whose name is not text, or pass a path that does not lead "
"through it; renaming a link changes nothing. ")
#: How a README prints the clone URL since #693: one word of a POSIX shell,
#: in quotes where one is needed, an apostrophe the only character written as
#: something else.
QUOTED_ONE_WORD = ("the clone URL in a `git clone` line, in quotes where a "
"shell needs them, every character but an apostrophe "
"written as itself: ")
SCAFFOLD_REMEDIATION = (
"Remediation: nothing was created, and `--dry-run` refuses the same. "
"Pass a --local-remote-dir whose path is one line of UTF-8 text with no "
"control character in it, a directory renamed or made anew, and run "
"again; or scaffold against GitHub, without --local-remote-dir, where "
"the clone URL is `https://github.com/<org>/<name>.git`. The READMEs "
"print the clone URL as it stands: " + WHY_IT_IS_REFUSED)
"the clone URL is `https://github.com/<org>/<name>.git`. "
+ THE_RESOLVED_PATH + "The READMEs print " + QUOTED_ONE_WORD
+ WHY_IT_IS_REFUSED)
FAMILY_REMEDIATION = (
"Remediation: nothing was created, and `--dry-run` refuses the same. "
"Pass a --local-remote-dir whose path is one line of UTF-8 text with no "
"control character in it, a directory renamed or made anew, and run "
"again; or run `init` against GitHub, without --local-remote-dir, where "
"the clone URL is `https://github.com/<org>/<Family>.git`. The holder's "
"`README.md` prints the clone URL as it stands: " + WHY_IT_IS_REFUSED)
"the clone URL is `https://github.com/<org>/<Family>.git`. "
+ THE_RESOLVED_PATH + "The holder's `README.md` prints "
+ QUOTED_ONE_WORD + WHY_IT_IS_REFUSED)


@pytest.fixture
Expand Down Expand Up @@ -332,7 +350,9 @@ def test_a_utf8_directory_still_plans_a_scaffold(scaffold, capsys, tmp_path):
def test_a_utf8_directory_still_scaffolds(tmp_path):
"""End to end into local bare remotes under `<dir>/café`: exit 0, all
three remotes made, and the assembly root's README prints the clone URL
exactly as it stands (read as the bytes the scaffold wrote)."""
with every character as it stands (read as the bytes the scaffold
wrote), in the single quotes `shell_quoted` gives a word holding `é`
(#693)."""
cafe = tmp_path / "café"
cafe.mkdir()
work = tmp_path / "work"
Expand All @@ -343,7 +363,7 @@ def test_a_utf8_directory_still_scaffolds(tmp_path):
f"{PROJECT}.git"]
readme = (work / PROJECT / "README.md").read_bytes().decode("utf-8")
url = str(cafe.resolve() / f"{PROJECT}.git")
assert f"git clone --recurse-submodules {url}\n" in readme
assert f"git clone --recurse-submodules '{url}'\n" in readme


def test_a_utf8_directory_still_plans_a_family(capsys, tmp_path):
Expand All @@ -361,8 +381,8 @@ def test_a_utf8_directory_still_plans_a_family(capsys, tmp_path):

def test_a_utf8_directory_still_creates_a_holder(tmp_path):
"""End to end into a local bare remote under `<dir>/café`: exit 0,
`Contoso.git` made, and the holder's README prints the clone URL as it
stands."""
`Contoso.git` made, and the holder's README prints the clone URL with
every character as it stands, in single quotes (#693)."""
cafe = tmp_path / "café"
cafe.mkdir()
into = tmp_path / "into"
Expand All @@ -372,4 +392,4 @@ def test_a_utf8_directory_still_creates_a_holder(tmp_path):
readme = (into / HOLDER / HOLDER / "README.md").read_bytes().decode(
"utf-8")
url = str(cafe.resolve() / f"{HOLDER}.git")
assert f"git clone --recurse-submodules {url}\n" in readme
assert f"git clone --recurse-submodules '{url}'\n" in readme
Loading
Loading