Skip to content

Keep a leading ssh. endpoint out of the project path - #23

Merged
juev merged 2 commits into
mainfrom
hermes/ssh-host-prefix
Nov 13, 2026
Merged

juev merged 2 commits into
mainfrom
hermes/ssh-host-prefix

Conversation

@juev

@juev juev commented Nov 13, 2026

Copy link
Copy Markdown
Owner

What this changes

The host of a repository URL used to go into the project path exactly as written, so a forge that serves Git over SSH on a subdomain produced a second tree next to the repository that already existed:

repository URL directory before directory now
git@ssh.bitbucket.org:team/service.git ssh.bitbucket.org/team/service bitbucket.org/team/service
ssh://git@ssh.bitbucket.org:443/team/service.git ssh.bitbucket.org/team/service bitbucket.org/team/service
ssh://git@ssh.github.com:443/juev/gclone.git ssh.github.com/juev/gclone github.com/juev/gclone
git@SSH.BitBucket.ORG:team/service.git SSH.BitBucket.ORG/team/service bitbucket.org/team/service
git@git.example.com:team/service.git git.example.com/team/service unchanged

A leading ssh label marks an SSH endpoint of the service, not a service of its own, so it is dropped before the path is built; a host without that label keeps its directory exactly. The host is lowercased in every mode, since a hostname is case-insensitive and an uppercase host used to add a third spelling of the same tree.

--full-host keeps the label for the rare host where ssh.<domain> serves a different repository than <domain>.

normalize and getProjectDir take a normalizeOptions value now, and the URL cache is keyed per mode so one URL cannot be served from the wrong normalization.

Background and the full acceptance list: closes #22.

Verification

go test ./... passes, including new cases for the SSH forms, an uppercase host, --full-host, the whole rule (Test_projectHost) and cache separation between the two modes. TestNormalizeSecurity and TestGetProjectDirSecurity are untouched and green. golangci-lint run reports the same 17 goconst findings as main and none from this change.

End-to-end with the built binary and a real clone:

GIT_PROJECT_DIR=… ./gclone --quiet --shallow https://github.com/juev/gclone.git
  → …/github.com/juev/gclone
GIT_PROJECT_DIR=… ./gclone ssh://git@ssh.github.com:443/juev/gclone.git
  → repository already exists: …/github.com/juev/gclone   (no network access)
GIT_PROJECT_DIR=… ./gclone --full-host ssh://git@ssh.github.com:443/juev/gclone.git
  → Cloning into 'gclone'…   (…/ssh.github.com/juev/gclone, the escape hatch)

Notes

  • altssh.gitlab.com, GitLab's SSH-over-443 endpoint, does not start with ssh. and keeps its own directory; if that ever matters the rule becomes a set of leading labels.
  • A repository already cloned through ssh.<host> during a forge's brownout window keeps its tree, and a later clone through <host> creates a second one. The README documents the rule; a migration helper is out of scope here.

juev added 2 commits November 13, 2026 23:52
A forge that moves Git over SSH to a subdomain (bitbucket.org ->
ssh.bitbucket.org, and GitHub's ssh.github.com) produced a second tree
next to the repository that already existed, because the host went into
the path exactly as written. processOneRepository only checks whether
the computed directory is non-empty, so the duplicate was cloned again
instead of reporting "repository already exists".

The leading ssh label now marks a transport endpoint rather than a
service name: it is dropped before the path is built, and the host is
lowercased in every mode. Hosts without that label keep their directory
untouched, so self-hosted instances (git.example.com) are unaffected.
--full-host keeps the label for hosts where ssh.<domain> serves a
different repository than <domain>.

The URL cache is keyed per mode so one URL cannot be served from the
wrong normalization.
@juev
juev merged commit e2c2bf7 into main Nov 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bitbucket.org → ssh.bitbucket.org clones into a second tree: derive the directory from the second-level domain by default

1 participant