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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,12 @@ avr repo clone foo/faa my-checkout --depth 1 --branch main
avr repo clone foo/faa --placement https://git.fi.a.avrea.io
```

Avrea discovers the repository's placements and picks the first mirror that
responds successfully from your machine. `--placement` accepts an advertised
mirror URL or hostname to select one explicitly. Give the `owner/repository` name
or an Avrea repository ID (`rep-...`).
The repository must already have Git mirroring enabled and a usable placement.
Avrea uses a single available placement directly. With multiple placements,
it picks the first mirror that responds successfully from your machine.
`--placement` accepts an advertised mirror URL or hostname to select one
explicitly. Give the `owner/repository` name or an Avrea repository ID (`rep-...`).
Requires Git 2.31 or newer. The repository must already have Git mirroring
enabled and a usable placement.

`origin` stays on the selected read-only mirror. The clone installs a
repository-local credential helper so ordinary `git fetch` and `git pull`
Expand Down
100 changes: 65 additions & 35 deletions avrea_cli/commands/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from avrea_cli.output import output_list
from avrea_cli.repo_context import resolve_repo
from avrea_cli.repo_context import resolve_repo_or_detect
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from urllib.parse import quote
import asyncio
Expand Down Expand Up @@ -193,8 +194,9 @@ def repo_clone(
"""Clone a repository from its nearest available Avrea mirror.

REPOSITORY is an owner/repository name or an Avrea repository ID (rep-...).
Available placements are raced from this client; the first to serve the
repository wins. Use --placement to select an exact URL or hostname.
When several placements are available, the first to serve the repository
wins. A single placement is used directly. Use --placement to select an
exact URL or hostname. Requires Git 2.31 or newer.

The origin remote stays on the selected read-only mirror. A repository-local
Avrea credential helper obtains fresh Git JWTs for subsequent fetch and pull
Expand Down Expand Up @@ -222,43 +224,71 @@ def repo_clone(
client: ApiClient = ctx.obj["client"]
config: CliConfig = ctx.obj["config"]
ensure_authenticated(config)
try:
session = client.public_get(f"/repos/{quote(repository, safe='/')}/mirror/session")
except httpx.HTTPStatusError as exc:
handle_http_error(exc, "discover Git mirror placements")
except httpx.HTTPError:
raise click.ClickException("Could not reach Avrea to discover Git mirror placements.") from None
if not isinstance(session, dict) or not isinstance(session.get("browsable"), bool):
raise click.ClickException("Avrea returned an invalid Git mirror session.")
if not session["browsable"]:
match session.get("unavailable_reason"):
case "mirror_off":
name = repository if "/" in repository else "OWNER/REPO"
raise click.ClickException(
"Git mirroring is disabled. List locations with `avr repo mirror locations`, then run "
f"`avr repo mirror create {name} --location <cluster-id>`."
)
case "not_ready":
raise click.ClickException(
"No Git mirror placement is ready. Check `avr repo mirror status` and try again."
)
case reason:
raise click.ClickException(f"Avrea reported an unknown mirror unavailability reason: {reason!r}.")
full_name = session.get("full_name")
endpoints = session.get("endpoints")
if not isinstance(full_name, str) or not isinstance(endpoints, list) or not endpoints:
raise click.ClickException("Avrea returned an incomplete Git mirror session.")
full_name = validate_repo_name(full_name)
if not all(isinstance(endpoint, str) for endpoint in endpoints):
raise click.ClickException("Avrea returned invalid Git mirror placements.")
endpoints = list(dict.fromkeys(mirror_endpoint(endpoint) for endpoint in endpoints))
endpoint = select_placement(endpoints, placement) if placement is not None else None
with ThreadPoolExecutor(max_workers=1) as executor:
# Names can be authorized while discovery runs. IDs need discovery's
# canonical name first. Only HTTP I/O runs in the worker so discovery
# errors take precedence over a speculative token request's errors.
pending = (
executor.submit(client.public_post, "/tokens/git", json={"repos": [repository]})
if "/" in repository
else None
)
try:
session = client.public_get(f"/repos/{quote(repository, safe='/')}/mirror/session")
except httpx.HTTPStatusError as exc:
handle_http_error(exc, "discover Git mirror placements")
except httpx.HTTPError:
raise click.ClickException("Could not reach Avrea to discover Git mirror placements.") from None
if not isinstance(session, dict) or not isinstance(session.get("browsable"), bool):
raise click.ClickException("Avrea returned an invalid Git mirror session.")
if not session["browsable"]:
match session.get("unavailable_reason"):
case "mirror_off":
name = repository if "/" in repository else "OWNER/REPO"
raise click.ClickException(
"Git mirroring is disabled. List locations with `avr repo mirror locations`, then run "
f"`avr repo mirror create {name} --location <cluster-id>`."
)
case "not_ready":
raise click.ClickException(
"No Git mirror placement is ready. Check `avr repo mirror status` and try again."
)
case reason:
raise click.ClickException(f"Avrea reported an unknown mirror unavailability reason: {reason!r}.")
full_name = session.get("full_name")
endpoints = session.get("endpoints")
if not isinstance(full_name, str) or not isinstance(endpoints, list) or not endpoints:
raise click.ClickException("Avrea returned an incomplete Git mirror session.")
full_name = validate_repo_name(full_name)
if not all(isinstance(endpoint, str) for endpoint in endpoints):
raise click.ClickException("Avrea returned invalid Git mirror placements.")
endpoints = list(dict.fromkeys(mirror_endpoint(endpoint) for endpoint in endpoints))
endpoint = select_placement(endpoints, placement) if placement is not None else None
if endpoint is None and len(endpoints) == 1:
Comment thread
Ormod marked this conversation as resolved.
Resolved
endpoint = endpoints[0]
if pending is not None and full_name != repository:
# Finish and discard the speculative request, including any error.
# A renamed or recased repository needs credentials for its exact
# advertised name; never send the old name's token to the mirror.
pending.exception()
pending = None
token, _ = git_token(client, full_name, pending=pending)
selected_directly = endpoint is not None
if endpoint is None:
token, _ = git_token(client, full_name)
click.echo("Finding the nearest available Git mirror…", err=True)
endpoint = asyncio.run(nearest_mirror(endpoints, full_name, token))
click.echo(f"Cloning {full_name} from {endpoint}", err=True)
ctx.exit(clone_mirror(config.public_api_url, endpoint, full_name, directory, branch=branch, depth=depth))
returncode = clone_mirror(
config.public_api_url, endpoint, full_name, directory, token=token, branch=branch, depth=depth
)
if returncode > 0 and selected_directly:
# Diagnose failures without adding a request to successful direct clones.
# A healthy mirror leaves Git's own error (e.g. a local path conflict) alone.
try:
asyncio.run(nearest_mirror([endpoint], full_name, token))
except click.ClickException as exc:
exc.show()
ctx.exit(returncode)


@repo.command("list")
Expand Down
41 changes: 35 additions & 6 deletions avrea_cli/git_mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from avrea_cli.api_client import ApiClient
from avrea_cli.config import require_secure_host
from avrea_cli.helpers import handle_http_error
from concurrent.futures import Future
from datetime import UTC
from datetime import datetime
from typing import Any
from urllib.parse import quote
from urllib.parse import urlsplit
import asyncio
Expand Down Expand Up @@ -60,10 +62,17 @@ def mirror_endpoint(value: str) -> str:
return value


def git_token(client: ApiClient, full_name: str) -> tuple[str, datetime]:
"""The issuer owns the TTL: one hour, or less for scoped API keys."""
def git_token(
client: ApiClient, full_name: str, *, pending: Future[dict[str, Any]] | None = None
) -> tuple[str, datetime]:
"""Obtain a token, optionally finishing a request already made for full_name.

The issuer owns the TTL: one hour, or less for scoped API keys.
"""
try:
response = client.public_post("/tokens/git", json={"repos": [full_name]})
response = (
pending.result() if pending is not None else client.public_post("/tokens/git", json={"repos": [full_name]})
)
except httpx.HTTPStatusError as exc:
handle_http_error(exc, "issue a Git token")
except httpx.HTTPError:
Expand Down Expand Up @@ -162,7 +171,10 @@ def credential_config(api_host: str, url: str, full_name: str) -> list[str]:
)
# Also fail closed if the installed executable has since moved or startup
# fails before the credential adapter runs. Git appends the operation.
helper = f'!f() {{ {command} "$@" || {{ printf "quit=1\\n\\n"; exit 1; }}; }}; f'
helper = (
'!f() { case "$1" in store|erase) return 0;; esac; '
f'{command} "$@" || {{ printf "quit=1\\n\\n"; exit 1; }}; }}; f'
)
return [
# Reset inherited helpers for this URL, so they neither supply stale
# JWTs nor store the temporary credentials returned by our helper.
Expand All @@ -179,19 +191,36 @@ def clone_mirror(
full_name: str,
directory: str | None,
*,
token: str,
branch: str | None,
depth: int | None,
) -> int:
url = clone_url(endpoint, full_name)
env = os.environ.copy()
# Secrets must not appear in argv, Git config, or user-enabled trace logs.
# Disable tracing enabled through the inherited environment.
for key in list(env):
if key.startswith("GIT_TRACE") or key == "GIT_CURL_VERBOSE":
del env[key]
env["GIT_TERMINAL_PROMPT"] = "0"
env["GIT_ASKPASS"] = ""
env["SSH_ASKPASS"] = ""
argv = ["git", "-c", f"http.{url}.extraHeader=", "clone"]
# Reuse the initial token without persisting it or exposing it in argv.
# Git re-exports it in GIT_CONFIG_PARAMETERS to child processes, including
# user-configured hooks and checkout filters.
# These process-only overrides also keep Git from invoking any credential
# helper during this clone; the repository-local helper serves later fetches.
env["AVR_GIT_CLONE_AUTHORIZATION"] = f"Authorization: Bearer {token}"
Comment thread
Ormod marked this conversation as resolved.
Resolved
argv = [
"git",
"-c",
f"http.{url}.extraHeader=",
f"--config-env=http.{url}.extraHeader=AVR_GIT_CLONE_AUTHORIZATION",
Comment thread
Ormod marked this conversation as resolved.
Resolved
Comment thread
Ormod marked this conversation as resolved.
Resolved
"-c",
f"credential.{url}.helper=",
"-c",
f"http.{url}.followRedirects=false",
"clone",
]
for setting in credential_config(api_host, url, full_name):
argv.extend(["--config", setting])
if branch is not None:
Expand Down
5 changes: 3 additions & 2 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2338,8 +2338,9 @@ avr repo clone [OPTIONS] REPOSITORY [DIRECTORY]
```

REPOSITORY is an owner/repository name or an Avrea repository ID (rep-...).
Available placements are raced from this client; the first to serve the
repository wins. Use --placement to select an exact URL or hostname.
When several placements are available, the first to serve the repository
wins. A single placement is used directly. Use --placement to select an
exact URL or hostname. Requires Git 2.31 or newer.

The origin remote stays on the selected read-only mirror. A repository-local
Avrea credential helper obtains fresh Git JWTs for subsequent fetch and pull
Expand Down
2 changes: 1 addition & 1 deletion docs/reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -10187,7 +10187,7 @@
"full": "avr repo clone",
"is_group": false,
"short_help": "Clone a repository from its nearest available Avrea mirror.",
"help": "Clone a repository from its nearest available Avrea mirror.\n\nREPOSITORY is an owner/repository name or an Avrea repository ID (rep-...).\nAvailable placements are raced from this client; the first to serve the\nrepository wins. Use --placement to select an exact URL or hostname.\n\nThe origin remote stays on the selected read-only mirror. A repository-local\nAvrea credential helper obtains fresh Git JWTs for subsequent fetch and pull\noperations using your Avrea login. Push changes to the source repository\nusing a separate remote. Tokens normally last one hour; scoped API keys have\na shorter server-enforced lifetime. Tokens are never saved in the repository.\n\n\b\nExamples:\n avr repo clone foo/faa\n avr repo clone foo/faa my-checkout --depth 1\n avr repo clone foo/faa --placement https://git.fi.a.avrea.io\n",
"help": "Clone a repository from its nearest available Avrea mirror.\n\nREPOSITORY is an owner/repository name or an Avrea repository ID (rep-...).\nWhen several placements are available, the first to serve the repository\nwins. A single placement is used directly. Use --placement to select an\nexact URL or hostname. Requires Git 2.31 or newer.\n\nThe origin remote stays on the selected read-only mirror. A repository-local\nAvrea credential helper obtains fresh Git JWTs for subsequent fetch and pull\noperations using your Avrea login. Push changes to the source repository\nusing a separate remote. Tokens normally last one hour; scoped API keys have\na shorter server-enforced lifetime. Tokens are never saved in the repository.\n\n\b\nExamples:\n avr repo clone foo/faa\n avr repo clone foo/faa my-checkout --depth 1\n avr repo clone foo/faa --placement https://git.fi.a.avrea.io\n",
"epilog": null,
"deprecated": false,
"hidden": false,
Expand Down
11 changes: 6 additions & 5 deletions docs/starlight/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ avr repo clone foo/faa my-checkout --depth 1 --branch main
avr repo clone foo/faa --placement https://git.fi.a.avrea.io
```

Avrea discovers the repository's placements and picks the first mirror that
responds successfully from your machine. `--placement` accepts an advertised
mirror URL or hostname to select one explicitly. Give the `owner/repository` name
or an Avrea repository ID (`rep-...`).
The repository must already have Git mirroring enabled and a usable placement.
Avrea uses a single available placement directly. With multiple placements,
it picks the first mirror that responds successfully from your machine.
`--placement` accepts an advertised mirror URL or hostname to select one
explicitly. Give the `owner/repository` name or an Avrea repository ID (`rep-...`).
Requires Git 2.31 or newer. The repository must already have Git mirroring
enabled and a usable placement.

`origin` stays on the selected read-only mirror. The clone installs a
repository-local credential helper so ordinary `git fetch` and `git pull`
Expand Down
5 changes: 3 additions & 2 deletions docs/starlight/reference/repo.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ avr repo clone [OPTIONS] REPOSITORY [DIRECTORY]
```

REPOSITORY is an owner/repository name or an Avrea repository ID (rep-...).
Available placements are raced from this client; the first to serve the
repository wins. Use --placement to select an exact URL or hostname.
When several placements are available, the first to serve the repository
wins. A single placement is used directly. Use --placement to select an
exact URL or hostname. Requires Git 2.31 or newer.

The origin remote stays on the selected read-only mirror. A repository-local
Avrea credential helper obtains fresh Git JWTs for subsequent fetch and pull
Expand Down
2 changes: 1 addition & 1 deletion man/man1/avr-repo-clone.1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ avr-repo-clone \- Clone a repository from its nearest available Avrea mirror.
.SH DESCRIPTION
Clone a repository from its nearest available Avrea mirror.
.PP
REPOSITORY is an owner/repository name or an Avrea repository ID (rep-...). Available placements are raced from this client; the first to serve the repository wins. Use --placement to select an exact URL or hostname.
REPOSITORY is an owner/repository name or an Avrea repository ID (rep-...). When several placements are available, the first to serve the repository wins. A single placement is used directly. Use --placement to select an exact URL or hostname. Requires Git 2.31 or newer.
.PP
The origin remote stays on the selected read-only mirror. A repository-local Avrea credential helper obtains fresh Git JWTs for subsequent fetch and pull operations using your Avrea login. Push changes to the source repository using a separate remote. Tokens normally last one hour; scoped API keys have a shorter server-enforced lifetime. Tokens are never saved in the repository.
.PP
Expand Down
Loading
Loading