A pull now takes the clone URL the repository was listed with, and only pulls a folder that is a clone of that repository.
IGitClient.FetchAndPullAsync(url, path, token, ct): a folder whose origin names a different repository fails its row before any request, with a message that names both (credentials stored in an origin are never shown).
The fetch always uses the listed URL. When the origin names the same repository in another spelling (SSH, plain HTTP, embedded credentials), the listed URL is fetched through a remote that exists only for that call; origin is never rewritten. Tags and pruning behave as before.
The git credential callback answers only HTTPS requests for the listed URL's host and port.
WslCloner: organization and repository validated as single names, HTTPS URL required and passed after --, credential helper scoped to the URL's host, an existing destination pulled only when its origin is the listed URL.
The repository lister refuses a name that is not one plain name before making a request.
New internal RemoteUrl holds the two decisions and is covered by table tests.
A folder with the right name is not proof of the right repository. A same-named clone of a fork, of another owner's repository, or of another host was reported Updated although the listed repository was never synced, and the transport was whatever that folder's configuration said.
Behaviour changes worth a release note
A same-named folder that is a clone of something else is now Failed with an explanatory message, where it used to be reported Updated. Nothing in the folder is changed.
A folder whose origin was switched to SSH or HTTP is now fetched over the listed HTTPS URL with the token.
IGitClient.FetchAndPullAsync gained a parameter (breaking for an implementer of the interface; gclo.Engine has no package consumers, ADR-0007).
How it was tested
gclo.Engine.Tests: 567 passed (was 480). New: RemoteUrlTests (same-repository and credential tables, redaction), OriginVerificationTests (different origin fails without fetching; differently spelled origin pulls from the listed URL, keeps origin untouched, still follows tags and prunes; a remote left by a killed run is replaced; message never shows stored credentials), lister name refusal, WSL argument and script checks, the view model passing the loaded organization, the engine passing the listed URL.
gclo.Cli.Tests: 142 passed. Line coverage 100% on gclo.Engine, gclo.ViewModels, gclo.
Zero-warning x64 build with locked restore; dotnet format --verify-no-changes clean.
WSL script: sh -n syntax check, and a run under Git Bash showing an existing destination with a different origin is refused before any pull.
Not run: the UI suite (it drives the desktop; the shell change is one pass-through signature). The credential callback itself cannot be reached offline, since local-path fixtures never ask for credentials; its decision logic is the covered RemoteUrl.MayReceiveCredentials. No end-to-end run against a real WSL distribution or a real HTTPS remote.
Checklist
Commits are signed and signed off (git commit -S -s)
Zero build warnings; format and lint are clean
Tests added or updated; the 100% coverage gate still passes
Docs updated for user-visible changes; threat model updated if the attack surface changed (README "How it works"; threat model T14 added, T4/T8/T9 updated)
No secrets, credentials, or personal data
New dependencies are justified below, or none were added (none)
Substantial AI assistance, if any, is noted here: written with Claude Code (Claude Fable 5.2)
## What
A pull now takes the clone URL the repository was listed with, and only pulls a folder that is a clone of that repository.
- `IGitClient.FetchAndPullAsync(url, path, token, ct)`: a folder whose `origin` names a different repository fails its row before any request, with a message that names both (credentials stored in an origin are never shown).
- The fetch always uses the listed URL. When the origin names the same repository in another spelling (SSH, plain HTTP, embedded credentials), the listed URL is fetched through a remote that exists only for that call; `origin` is never rewritten. Tags and pruning behave as before.
- The git credential callback answers only HTTPS requests for the listed URL's host and port.
- `WslCloner`: organization and repository validated as single names, HTTPS URL required and passed after `--`, credential helper scoped to the URL's host, an existing destination pulled only when its origin is the listed URL.
- The repository lister refuses a name that is not one plain name before making a request.
- New internal `RemoteUrl` holds the two decisions and is covered by table tests.
## Why
Closes #139.
A folder with the right name is not proof of the right repository. A same-named clone of a fork, of another owner's repository, or of another host was reported **Updated** although the listed repository was never synced, and the transport was whatever that folder's configuration said.
## Behaviour changes worth a release note
- A same-named folder that is a clone of something else is now **Failed** with an explanatory message, where it used to be reported Updated. Nothing in the folder is changed.
- A folder whose origin was switched to SSH or HTTP is now fetched over the listed HTTPS URL with the token.
- `IGitClient.FetchAndPullAsync` gained a parameter (breaking for an implementer of the interface; `gclo.Engine` has no package consumers, ADR-0007).
## How it was tested
- `gclo.Engine.Tests`: 567 passed (was 480). New: `RemoteUrlTests` (same-repository and credential tables, redaction), `OriginVerificationTests` (different origin fails without fetching; differently spelled origin pulls from the listed URL, keeps origin untouched, still follows tags and prunes; a remote left by a killed run is replaced; message never shows stored credentials), lister name refusal, WSL argument and script checks, the view model passing the loaded organization, the engine passing the listed URL.
- `gclo.Cli.Tests`: 142 passed. Line coverage 100% on `gclo.Engine`, `gclo.ViewModels`, `gclo`.
- Zero-warning x64 build with locked restore; `dotnet format --verify-no-changes` clean.
- WSL script: `sh -n` syntax check, and a run under Git Bash showing an existing destination with a different origin is refused before any pull.
Not run: the UI suite (it drives the desktop; the shell change is one pass-through signature). The credential callback itself cannot be reached offline, since local-path fixtures never ask for credentials; its decision logic is the covered `RemoteUrl.MayReceiveCredentials`. No end-to-end run against a real WSL distribution or a real HTTPS remote.
## Checklist
- [x] Commits are signed and signed off (`git commit -S -s`)
- [x] Zero build warnings; format and lint are clean
- [x] Tests added or updated; the 100% coverage gate still passes
- [x] Docs updated for user-visible changes; threat model updated if the attack surface changed (README "How it works"; threat model T14 added, T4/T8/T9 updated)
- [x] No secrets, credentials, or personal data
- [x] New dependencies are justified below, or none were added (none)
- [x] Substantial AI assistance, if any, is noted here: written with Claude Code (Claude Fable 5.2)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A folder with a repository's name used to be pulled through whatever
origin it had. A same-named clone of a fork, of another owner's
repository or of another host was reported Updated although the listed
repository was never synced, and the git transport was whatever that
folder's configuration said.
- IGitClient.FetchAndPullAsync takes the clone URL the repository was
listed with. A folder whose origin names a different repository fails
its row before any request; the message names both, without any
credentials stored in the origin.
- The fetch always uses the listed URL. An origin that names the same
repository in another spelling (SSH, plain HTTP, embedded credentials)
is fetched through a remote that exists only for that call; origin is
never rewritten, and tags and pruning behave as before.
- The git credential callback answers only HTTPS requests for the listed
URL's host and port. The default-branch retarget gets a provider of its
own instead of sharing the fetch's replay counter.
- WslCloner validates the organization and repository as single names,
requires an HTTPS URL passed after "--", scopes the credential helper
to the URL's host, and pulls an existing destination only when its
origin is the listed URL. The view model passes the organization the
table was loaded for.
- The repository lister refuses a name that is not one plain name before
making a request.
New internal RemoteUrl holds the two decisions (same repository, may
receive credentials) and is covered by table tests. Threat model: T14
added, T4, T8 and T9 updated.
Closes#139
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Follow-up to the independent review of this branch.
- A url.<base>.insteadOf entry in the folder's own configuration redirected
the fetch of the listed URL to another repository. It is now refused
before anything is fetched, and so is the folder's core.sshCommand when
the fetch is not HTTPS. The user's global configuration still applies.
- The temporary remote is marked as gclo's; a remote of that name the user
created is never removed, and cleanup can no longer mask the fetch's own
error. A marked leftover is removed on any later pull. The temporary
fetch follows origin's tag option.
- The mismatch message gives the git remote set-url command for a
repository that was renamed or moved.
- RemoteUrl: redaction rebuilds the URL from scheme, host, port and path,
so no shape leaves user information, query or fragment behind; a
non-default web port distinguishes two servers; an unreadable host is
nobody's instead of an exception; IPv6 hosts keep their brackets.
- The WSL script compares the stored origin URL, not the rewritten one.
- The credential callback is tested directly. The threat model and README
no longer claim more than the code does about redirects.
Refs #139
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Independent review of the first commit (a second reviewer session, read-only) found one blocker and several smaller defects; all are addressed in 510d3e0.
Blocker, fixed: a url.<base>.insteadOf entry in the folder's own .git/config redirected the fetch of the listed URL to another repository. Refused now, before anything is fetched; the folder's core.sshCommand is refused too when the fetch is not HTTPS. The user's global git configuration still applies.
Fixed: cleanup of the temporary remote could mask the fetch's own error and could delete a remote named gclo-sync that the user created. The remote is now marked, only marked remotes are removed, cleanup is best effort.
Fixed: redaction left part of a password for some URL shapes; it now rebuilds the URL from scheme, host, port and path.
Fixed: the WSL script compared the rewritten origin (remote get-url) and would refuse a destination gclo itself cloned under a user-level rewrite; it compares the stored URL.
Fixed: a renamed or transferred repository fails with a message that gives the git remote set-url command.
Wording corrected: the credential check is about the remote's URL; a redirect issued by the listed host is followed by libgit2. The threat model and README no longer claim otherwise.
Tests added: the credential callback is tested directly; the differently-spelled-origin test now asserts which URL was contacted (FETCH_HEAD).
Known and accepted: an SSH origin on ssh.github.com:443, a www. host, or an SSH origin against a sub-path install does not match its HTTPS listing and fails with the set-url hint.
Gates after the second commit: 606 engine tests, 142 CLI tests, 100% line coverage, zero-warning locked build, format clean.
Independent review of the first commit (a second reviewer session, read-only) found one blocker and several smaller defects; all are addressed in 510d3e0.
- **Blocker, fixed:** a `url.<base>.insteadOf` entry in the folder's own `.git/config` redirected the fetch of the listed URL to another repository. Refused now, before anything is fetched; the folder's `core.sshCommand` is refused too when the fetch is not HTTPS. The user's global git configuration still applies.
- **Fixed:** cleanup of the temporary remote could mask the fetch's own error and could delete a remote named `gclo-sync` that the user created. The remote is now marked, only marked remotes are removed, cleanup is best effort.
- **Fixed:** redaction left part of a password for some URL shapes; it now rebuilds the URL from scheme, host, port and path.
- **Fixed:** the WSL script compared the rewritten origin (`remote get-url`) and would refuse a destination gclo itself cloned under a user-level rewrite; it compares the stored URL.
- **Fixed:** a renamed or transferred repository fails with a message that gives the `git remote set-url` command.
- **Wording corrected:** the credential check is about the remote's URL; a redirect issued by the listed host is followed by libgit2. The threat model and README no longer claim otherwise.
- **Tests added:** the credential callback is tested directly; the differently-spelled-origin test now asserts which URL was contacted (FETCH_HEAD).
Known and accepted: an SSH origin on `ssh.github.com:443`, a `www.` host, or an SSH origin against a sub-path install does not match its HTTPS listing and fails with the set-url hint.
Gates after the second commit: 606 engine tests, 142 CLI tests, 100% line coverage, zero-warning locked build, format clean.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
A pull now takes the clone URL the repository was listed with, and only pulls a folder that is a clone of that repository.
IGitClient.FetchAndPullAsync(url, path, token, ct): a folder whoseoriginnames a different repository fails its row before any request, with a message that names both (credentials stored in an origin are never shown).originis never rewritten. Tags and pruning behave as before.WslCloner: organization and repository validated as single names, HTTPS URL required and passed after--, credential helper scoped to the URL's host, an existing destination pulled only when its origin is the listed URL.RemoteUrlholds the two decisions and is covered by table tests.Why
Closes #139.
A folder with the right name is not proof of the right repository. A same-named clone of a fork, of another owner's repository, or of another host was reported Updated although the listed repository was never synced, and the transport was whatever that folder's configuration said.
Behaviour changes worth a release note
IGitClient.FetchAndPullAsyncgained a parameter (breaking for an implementer of the interface;gclo.Enginehas no package consumers, ADR-0007).How it was tested
gclo.Engine.Tests: 567 passed (was 480). New:RemoteUrlTests(same-repository and credential tables, redaction),OriginVerificationTests(different origin fails without fetching; differently spelled origin pulls from the listed URL, keeps origin untouched, still follows tags and prunes; a remote left by a killed run is replaced; message never shows stored credentials), lister name refusal, WSL argument and script checks, the view model passing the loaded organization, the engine passing the listed URL.gclo.Cli.Tests: 142 passed. Line coverage 100% ongclo.Engine,gclo.ViewModels,gclo.dotnet format --verify-no-changesclean.sh -nsyntax check, and a run under Git Bash showing an existing destination with a different origin is refused before any pull.Not run: the UI suite (it drives the desktop; the shell change is one pass-through signature). The credential callback itself cannot be reached offline, since local-path fixtures never ask for credentials; its decision logic is the covered
RemoteUrl.MayReceiveCredentials. No end-to-end run against a real WSL distribution or a real HTTPS remote.Checklist
git commit -S -s)🤖 Generated with Claude Code
Independent review of the first commit (a second reviewer session, read-only) found one blocker and several smaller defects; all are addressed in
510d3e0.url.<base>.insteadOfentry in the folder's own.git/configredirected the fetch of the listed URL to another repository. Refused now, before anything is fetched; the folder'score.sshCommandis refused too when the fetch is not HTTPS. The user's global git configuration still applies.gclo-syncthat the user created. The remote is now marked, only marked remotes are removed, cleanup is best effort.remote get-url) and would refuse a destination gclo itself cloned under a user-level rewrite; it compares the stored URL.git remote set-urlcommand.Known and accepted: an SSH origin on
ssh.github.com:443, awww.host, or an SSH origin against a sub-path install does not match its HTTPS listing and fails with the set-url hint.Gates after the second commit: 606 engine tests, 142 CLI tests, 100% line coverage, zero-warning locked build, format clean.