diff --git a/CHANGELOG.md b/CHANGELOG.md index b7eb9088..8a0bd565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Workspace ids may now be 47 characters instead of 38, so long branch names + keep their tail.** The budget was never a limit anything enforced: devpod's own + ceiling is 48 and fatal (`workspace name cannot be longer than 48 characters` — + a 49-character id is refused, not truncated), and 38 was devlaunch reserving 10 + characters for downstream tooling that stacks prefixes onto the container name + against a 64-byte limit. 47 spends nine of those characters on legibility and + keeps one against devpod's wall, leaving ~17 for whatever bolts itself on. + `kinisi_ros@ags-devcontainer-tooling-support` reads + `kinisi-ros-ags-devcontainer-tooling-su-lenevere` rather than + `kinisi-ros-ags-devcontainer-t-lenevere`, and a dependabot ref keeps the whole + action name (`devlaunch-dependabot-codecov-action-6-sifivasa`). Truncation + policy has no effect on collisions — the eight-character suffix is hashed over + the full `(owner, repo, ref)` triple before anything is cut — so nothing about + uniqueness moves with this. Existing containers are unaffected: `dl` addresses + a workspace by the devpod id recorded in `metadata.json` (#88), so an id that + was derived under the old budget keeps opening the same container. A clone + directory whose leaf does change is re-cloned under the new name on the next + launch, leaving the old directory for `dl --prune` to report. + ## [0.2.2] - 2026-08-20 ### Fixed diff --git a/README.md b/README.md index 2a1d4ae5..33e9bfab 100644 --- a/README.md +++ b/README.md @@ -593,16 +593,16 @@ them. If that is too often, unset the variable and use `dl dotfiles`. see in `dl --ls`) and the clone directory under `~/.cache/devlaunch/repos/`: ``` --- at most 38 characters +-- at most 47 characters blooop/devlaunch@main -> devlaunch-main-zovomobo blooop/devlaunch@feature/auth -> devlaunch-feature-auth-poliseno blooop/devlaunch@feature-auth -> devlaunch-feature-auth-nesatabe blooop/test_renv@nb4 -> test-renv-nb4-polenita kinisi-robotics/kinisi_ros@ags-devcontainer-tooling-support - -> kinisi-ros-ags-devcontainer-t-lenevere + -> kinisi-ros-ags-devcontainer-tooling-su-lenevere blooop/devlaunch@dependabot/github_actions/codecov/codecov-action-6 - -> devlaunch-dependabot-codecov-sifivasa + -> devlaunch-dependabot-codecov-action-6-sifivasa ``` The eight-character syllable suffix is a hash of the full `(owner, repo, branch)` triple. @@ -620,8 +620,11 @@ Branch names are case-sensitive, because git refs are. URL specs (`dl github.com/owner/repo`) get an id in the same shape, with the suffix hashed over the URL. -The id is also the container hostname, so it stays well inside the 38-character budget -to leave room for tools that add their own prefixes. +The id is also the container hostname. 47 characters is one inside devpod's own hard +ceiling of 48 — a 49-character id is refused outright, not truncated — and well inside +the 64-byte hostname limit on its own, but tools that stack their own prefixes onto the +container name have about 17 characters to work with, so a tool that wants more is the +one that has to shorten. Branch names must be safe as both git refs and directory names — a name with a space or a leading dash is rejected rather than quietly rewritten. diff --git a/devlaunch/workspace_id.py b/devlaunch/workspace_id.py index f42136eb..7e17bd3f 100644 --- a/devlaunch/workspace_id.py +++ b/devlaunch/workspace_id.py @@ -37,12 +37,23 @@ import re from dataclasses import dataclass -# Total id budget. devpod's own ceiling is 48, but the setup pass's hostname stage +# Total id budget, one character inside devpod's own ceiling of 48. +# +# 48 is hard and fatal: ``devpod up --id`` with a 49-character id is rejected outright +# (``workspace name cannot be longer than 48 characters``, verified against the pinned +# v0.26.1), so an id derived here that overshoots is a launch that does not happen. 47 +# keeps one character of margin from that wall. +# +# This was 38, for the reason it is still not 48: the setup pass's hostname stage # (``tools.setup_stages``) names the container after the workspace id, and downstream -# tooling stacks prefixes and -# suffixes on it against a 64-byte limit (kinisi-robotics/kinisi_ros#9766 already sat -# at 62/64). 48 is a ceiling others eat into, not a budget to fill, so aim at 38. -TARGET_LENGTH = 38 +# tooling stacks its own prefixes and suffixes onto that against a 64-byte limit +# (kinisi-robotics/kinisi_ros#9766 already sat at 62/64 with a 38-char id). Widening to +# 47 spends nine characters of that reserve on legibility — branch names that used to +# lose their tail now keep it — and leaves ~17 characters for whatever a downstream +# caller bolts on, so a caller that wants more is the one that has to shorten. Nothing +# here breaks at any value up to 48, and the id alone is a legal hostname at 47, well +# inside 64. +TARGET_LENGTH = 47 # The repo slug is cut to this length when the id would otherwise overflow, and is # never cut below it. It carries no identity — only legibility — so trimming it is @@ -56,7 +67,7 @@ # This was 3 syllables / 18 bits when the scheme was first decided. 18 bits put a # birthday collision inside a single repo's plausible branch count — 500 branches # already produced one in test — so it was widened by one syllable. The extra two -# characters come out of the readable budget; the 38-char cap did not move. +# characters come out of the readable budget; the cap did not move to pay for them. _CONSONANTS = "bdfghjklmnprstvz" _VOWELS = "aeio" _SYLLABLES = 4 @@ -210,7 +221,7 @@ def value(self) -> str: suffix = self.suffix repo_part = slug(self.repo) # Cut the repo slug only when the id would otherwise overflow. Capping it at - # REPO_SLUG_LENGTH leaves at least TARGET_LENGTH - 20 - 1 - 1 - 8 = 8 + # REPO_SLUG_LENGTH leaves at least TARGET_LENGTH - 20 - 1 - 1 - 8 = 17 # characters for the ref, so the ref budget can never go non-positive — the # hole that let a 47-char repo name skip truncation altogether. if len(_join(repo_part, _fit_ref(self.ref, TARGET_LENGTH), suffix)) > TARGET_LENGTH: diff --git a/rust/devlaunch-core/src/domain/spec.rs b/rust/devlaunch-core/src/domain/spec.rs index 81a50e99..bce62428 100644 --- a/rust/devlaunch-core/src/domain/spec.rs +++ b/rust/devlaunch-core/src/domain/spec.rs @@ -457,10 +457,10 @@ mod tests { Case { spec: "blooop/python_template", shape: Shape::OwnerRepo { owner: "blooop", repo: "python_template", branch: None }, expanded: "git@github.com:blooop/python_template.git", identity: Expect::RepoLabel("python-template") }, Case { spec: "blooop/python_template@nb4", shape: Shape::OwnerRepo { owner: "blooop", repo: "python_template", branch: Some("nb4") }, expanded: "git@github.com:blooop/python_template.git@nb4", identity: Expect::Workspace("python-template-nb4-foganeje") }, Case { spec: "loft-sh/devpod", shape: Shape::OwnerRepo { owner: "loft-sh", repo: "devpod", branch: None }, expanded: "git@github.com:loft-sh/devpod.git", identity: Expect::RepoLabel("devpod") }, - Case { spec: "owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::OwnerRepo { owner: "owner", repo: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", branch: None }, expanded: "git@github.com:owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.git", identity: Expect::RepoLabel("rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr") }, + Case { spec: "owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::OwnerRepo { owner: "owner", repo: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", branch: None }, expanded: "git@github.com:owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.git", identity: Expect::RepoLabel("rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr") }, Case { spec: "owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr@main", shape: Shape::OwnerRepo { owner: "owner", repo: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", branch: Some("main") }, expanded: "git@github.com:owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.git@main", identity: Expect::Workspace("rrrrrrrrrrrrrrrrrrrr-main-rerajezi") }, - Case { spec: "owner/repo@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", shape: Shape::OwnerRepo { owner: "owner", repo: "repo", branch: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }, expanded: "git@github.com:owner/repo.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", identity: Expect::Workspace("repo-aaaaaaaaaaaaaaaaaaaaaaaa-pokipete") }, - Case { spec: "owner/myrepo@feature/some-very-long-branch-name-here", shape: Shape::OwnerRepo { owner: "owner", repo: "myrepo", branch: Some("feature/some-very-long-branch-name-here") }, expanded: "git@github.com:owner/myrepo.git@feature/some-very-long-branch-name-here", identity: Expect::Workspace("myrepo-feature-some-very-long-lafazota") }, + Case { spec: "owner/repo@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", shape: Shape::OwnerRepo { owner: "owner", repo: "repo", branch: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }, expanded: "git@github.com:owner/repo.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", identity: Expect::Workspace("repo-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-pokipete") }, + Case { spec: "owner/myrepo@feature/some-very-long-branch-name-here", shape: Shape::OwnerRepo { owner: "owner", repo: "myrepo", branch: Some("feature/some-very-long-branch-name-here") }, expanded: "git@github.com:owner/myrepo.git@feature/some-very-long-branch-name-here", identity: Expect::Workspace("myrepo-feature-some-very-long-branch-n-lafazota") }, Case { spec: "user.name/repo.name", shape: Shape::OwnerRepo { owner: "user.name", repo: "repo.name", branch: None }, expanded: "git@github.com:user.name/repo.name.git", identity: Expect::RepoLabel("repo-name") }, Case { spec: "my_user/my_repo", shape: Shape::OwnerRepo { owner: "my_user", repo: "my_repo", branch: None }, expanded: "git@github.com:my_user/my_repo.git", identity: Expect::RepoLabel("my-repo") }, Case { spec: "owner/repo@main", shape: Shape::OwnerRepo { owner: "owner", repo: "repo", branch: Some("main") }, expanded: "git@github.com:owner/repo.git@main", identity: Expect::Workspace("repo-main-gemetato") }, @@ -491,7 +491,7 @@ mod tests { Case { spec: "github.com/loft-sh/devpod", shape: Shape::HostPath, expanded: "github.com/loft-sh/devpod", identity: Expect::Workspace("github-com-loft-sh-devpod-vatomiha") }, Case { spec: "github.com/Blooop/DevLaunch", shape: Shape::HostPath, expanded: "github.com/Blooop/DevLaunch", identity: Expect::Workspace("github-com-blooop-devlaunch-lakatoje") }, Case { spec: "github.com/blooop/devlaunch", shape: Shape::HostPath, expanded: "github.com/blooop/devlaunch", identity: Expect::Workspace("github-com-blooop-devlaunch-lakatoje") }, - Case { spec: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::HostPath, expanded: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", identity: Expect::Workspace("github-com-oooooooooooooooooo-rasijome") }, + Case { spec: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::HostPath, expanded: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", identity: Expect::Workspace("github-com-ooooooooooooooooooooooooooo-rasijome") }, Case { spec: "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/owner/repo", shape: Shape::Url, expanded: "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/owner/repo", identity: Expect::Workspace("github-com-owner-repo-lokolede") }, Case { spec: "/owner/repo@main", shape: Shape::Url, expanded: "/owner/repo@main", identity: Expect::Workspace("github-com-owner-repo-lokolede") }, Case { spec: "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/owner/repo.git", shape: Shape::Url, expanded: "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/owner/repo.git", identity: Expect::Workspace("github-com-owner-repo-lokolede") }, diff --git a/rust/devlaunch-core/src/domain/workspace_id.rs b/rust/devlaunch-core/src/domain/workspace_id.rs index 14a04ec8..c2aabc3e 100644 --- a/rust/devlaunch-core/src/domain/workspace_id.rs +++ b/rust/devlaunch-core/src/domain/workspace_id.rs @@ -67,12 +67,23 @@ use std::fmt; -/// Total id budget. devpod's own ceiling is 48, but the setup pass's hostname +/// Total id budget, one character inside devpod's own ceiling of 48. +/// +/// 48 is hard and fatal: `devpod up --id` with a 49-character id is rejected +/// outright (`workspace name cannot be longer than 48 characters`, verified +/// against the pinned v0.26.1), so an id derived here that overshoots is a +/// launch that does not happen. 47 keeps one character of margin from that wall. +/// +/// This was 38, for the reason it is still not 48: the setup pass's hostname /// stage names the container after the workspace id, and downstream tooling -/// stacks prefixes and suffixes on it against a 64-byte limit -/// (kinisi-robotics/kinisi_ros#9766 already sat at 62/64). 48 is a ceiling -/// others eat into, not a budget to fill, so aim at 38. -pub(crate) const TARGET_LENGTH: usize = 38; +/// stacks its own prefixes and suffixes onto that against a 64-byte limit +/// (kinisi-robotics/kinisi_ros#9766 already sat at 62/64 with a 38-char id). +/// Widening to 47 spends nine characters of that reserve on legibility — branch +/// names that used to lose their tail now keep it — and leaves ~17 characters +/// for whatever a downstream caller bolts on, so a caller that wants more is the +/// one that has to shorten. Nothing here breaks at any value up to 48, and the +/// id alone is a legal hostname at 47, well inside 64. +pub(crate) const TARGET_LENGTH: usize = 47; /// The repo slug is cut to this length when the id would otherwise overflow, and /// is never cut below it. It carries no identity — only legibility — so trimming @@ -87,8 +98,8 @@ pub(crate) const REPO_SLUG_LENGTH: usize = 20; // This was 3 syllables / 18 bits when the scheme was first decided. 18 bits put // a birthday collision inside a single repo's plausible branch count — 500 // branches already produced one in test — so it was widened by one syllable. The -// extra two characters come out of the readable budget; the 38-char cap did not -// move. +// extra two characters come out of the readable budget; the cap did not move +// to pay for them. const CONSONANTS: &[u8; 16] = b"bdfghjklmnprstvz"; const VOWELS: &[u8; 4] = b"aeio"; const SYLLABLES: usize = 4; @@ -274,7 +285,7 @@ impl WorkspaceId { let suffix = self.suffix(); let mut repo_part = slug(&self.repo); // Cut the repo slug only when the id would otherwise overflow. Capping it - // at REPO_SLUG_LENGTH leaves at least TARGET_LENGTH - 20 - 1 - 1 - 8 = 8 + // at REPO_SLUG_LENGTH leaves at least TARGET_LENGTH - 20 - 1 - 1 - 8 = 17 // characters for the ref, so the ref budget can never go non-positive — // the hole that let a 47-char repo name skip truncation altogether. let untruncated = join(&[&repo_part, &fit_ref(&self.git_ref, TARGET_LENGTH), &suffix]); @@ -468,42 +479,42 @@ mod tests { "devlaunch", "dependabot/github_actions/codecov/codecov-action-6", "sifivasa", - "devlaunch-dependabot-codecov-sifivasa", + "devlaunch-dependabot-codecov-action-6-sifivasa", ), ( "blooop", "devlaunch", "dependabot/github_actions/blooop/prek-action-2", "zakogozo", - "devlaunch-dependabot-prek-act-zakogozo", + "devlaunch-dependabot-prek-action-2-zakogozo", ), ( "blooop", "python_template", "dependabot/pip/lib/dependencies-1", "hinarami", - "python-template-dependabot-de-hinarami", + "python-template-dependabot-dependencie-hinarami", ), ( "blooop", "lifetime_foc_rig", "dependabot/github_actions/codecov/codecov-action-6", "matagere", - "lifetime-foc-rig-dependabot-c-matagere", + "lifetime-foc-rig-dependabot-codecov-ac-matagere", ), ( "kinisi-robotics", "kinisi_ros", "ags-devcontainer-tooling-support", "lenevere", - "kinisi-ros-ags-devcontainer-t-lenevere", + "kinisi-ros-ags-devcontainer-tooling-su-lenevere", ), ( "blooop", "devlaunch", "fix/gh-auth-in-devcontainer", "pedoveho", - "devlaunch-fix-gh-auth-in-devc-pedoveho", + "devlaunch-fix-gh-auth-in-devcontainer-pedoveho", ), ( "blooop", @@ -517,14 +528,21 @@ mod tests { "dl", "a/bbbbbbbb/cccccccc/dddddddd/zzz", "jobiriti", - "dl-a-cccccccc-dddddddd-zzz-jobiriti", + "dl-a-bbbbbbbb-cccccccc-dddddddd-zzz-jobiriti", ), ( "blooop", "dl", - "aa/mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm/nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn/zz", - "rakibasi", - "dl-aa-zz-rakibasi", + "a/bbbbbbbbbbbb/cccccccccccc/dddddddddddd/zzz", + "gigisini", + "dl-a-cccccccccccc-dddddddddddd-zzz-gigisini", + ), + ( + "blooop", + "dl", + "aa/mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm/nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn/zz", + "degosare", + "dl-aa-zz-degosare", ), ( "blooop", @@ -538,7 +556,7 @@ mod tests { "devlaunch", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "zedabezi", - "devlaunch-aaaaaaaaaaaaaaaaaaa-zedabezi", + "devlaunch-aaaaaaaaaaaaaaaaaaaaaaaaaaaa-zedabezi", ), ( "owner", @@ -552,28 +570,28 @@ mod tests { "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", "a/very/long/branch/name/that/eats/the/budget", "bijizoli", - "rrrrrrrrrrrrrrrrrrrr-a-budget-bijizoli", + "rrrrrrrrrrrrrrrrrrrr-a-eats-the-budget-bijizoli", ), ( "owner", "rrrrrrrrrrrrrrrrrrrr", "a/very/long/branch/name/that/eats/the/budget", "tafifipo", - "rrrrrrrrrrrrrrrrrrrr-a-budget-tafifipo", + "rrrrrrrrrrrrrrrrrrrr-a-eats-the-budget-tafifipo", ), ( "owner", "repo", "release/9999999999999999999999999176", "sehirani", - "repo-release-9999999999999999-sehirani", + "repo-release-9999999999999999999999999-sehirani", ), ( "owner", "repo", "release/9999999999999999999999999234", "zivalero", - "repo-release-9999999999999999-zivalero", + "repo-release-9999999999999999999999999-zivalero", ), ("a", "bc", "main", "bajovafa", "bc-main-bajovafa"), ("ab", "c", "main", "mofijihe", "c-main-mofijihe"), @@ -589,14 +607,14 @@ mod tests { "a-repo-name-that-is-long", "feature/shared-prefix-1", "lapajiha", - "a-repo-name-that-is-feature-s-lapajiha", + "a-repo-name-that-is-feature-shared-pre-lapajiha", ), ( "blooop", "a-repo-name-that-is-long", "feature/shared-prefix-2", "sopalopi", - "a-repo-name-that-is-feature-s-sopalopi", + "a-repo-name-that-is-feature-shared-pre-sopalopi", ), ( "owner", @@ -617,14 +635,14 @@ mod tests { "r", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "sigivapa", - "r-bbbbbbbbbbbbbbbbbbbbbbbbbbb-sigivapa", + "r-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-sigivapa", ), ( "owner", "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "bagakino", - "rrrrrrrrrrrrrrrrrrrr-bbbbbbbb-bagakino", + "rrrrrrrrrrrrrrrrrrrr-bbbbbbbbbbbbbbbbb-bagakino", ), ( "owner", @@ -695,11 +713,11 @@ mod tests { ), ( "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", - "github-com-oooooooooooooooooo-rasijome", + "github-com-ooooooooooooooooooooooooooo-rasijome", ), ( "github.com/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", - "github-com-oooooooooooooooooo-bepolero", + "github-com-ooooooooooooooooooooooooooo-bepolero", ), ("github.com/o/r", "github-com-o-r-deparori"), ("o", "o-rotebife"), @@ -1247,16 +1265,40 @@ mod tests { assert!(!value.contains("github-actions"), "{value}"); } + /// The segment widths are sized to the budget: this ref is over it by less + /// than one segment, so exactly one middle has to go. A ref that fits whole + /// would pin nothing, which is what the 8-wide version of it became when the + /// cap moved from 38 to 47. #[test] fn only_as_many_middle_segments_drop_as_needed() { + let git_ref = format!( + "a/{}/{}/{}/zzz", + "b".repeat(12), + "c".repeat(12), + "d".repeat(12) + ); + let value = id("blooop", "dl", &git_ref).value(); + let kept = format!("dl-a-{}-{}-zzz-", "c".repeat(12), "d".repeat(12)); + assert!(value.starts_with(&kept), "{value}"); + assert!(!value.contains(&"b".repeat(12)), "{value}"); + } + + /// The other side of the same rule: nothing drops until it has to. + #[test] + fn a_ref_that_fits_whole_keeps_every_segment() { let value = id("blooop", "dl", "a/bbbbbbbb/cccccccc/dddddddd/zzz").value(); - assert!(value.starts_with("dl-a-cccccccc-dddddddd-zzz-"), "{value}"); - assert!(!value.contains("bbbbbbbb"), "{value}"); + assert!( + value.starts_with("dl-a-bbbbbbbb-cccccccc-dddddddd-zzz-"), + "{value}" + ); } + /// Both middles are far wider than the budget rather than just over it: at 30 + /// each this passed by a single character, so one more character of cap would + /// have turned it into a test of dropping *one* middle without saying so. #[test] fn the_first_and_last_segments_survive_heavy_truncation() { - let git_ref = format!("aa/{}/{}/zz", "m".repeat(30), "n".repeat(30)); + let git_ref = format!("aa/{}/{}/zz", "m".repeat(40), "n".repeat(40)); let value = id("blooop", "dl", &git_ref).value(); assert!(value.starts_with("dl-aa-zz-"), "{value}"); assert!( diff --git a/rust/golden_vectors.py b/rust/golden_vectors.py index 8a38c148..36f9e65d 100644 --- a/rust/golden_vectors.py +++ b/rust/golden_vectors.py @@ -65,7 +65,8 @@ def rs(s): ("blooop", "devlaunch", "fix/gh-auth-in-devcontainer"), ("blooop", "test_renv", "nb4"), ("blooop", "dl", "a/bbbbbbbb/cccccccc/dddddddd/zzz"), - ("blooop", "dl", "aa/" + "m" * 30 + "/" + "n" * 30 + "/zz"), + ("blooop", "dl", "a/" + "b" * 12 + "/" + "c" * 12 + "/" + "d" * 12 + "/zzz"), + ("blooop", "dl", "aa/" + "m" * 40 + "/" + "n" * 40 + "/zz"), ("blooop", "devlaunch", "a//b///c/d"), ("blooop", "devlaunch", "a" * 60), ("owner", "r" * 47, "main"), diff --git a/test/test_workspace_id.py b/test/test_workspace_id.py index 11b7963d..9147bc12 100644 --- a/test/test_workspace_id.py +++ b/test/test_workspace_id.py @@ -280,14 +280,32 @@ def test_dependabot_ref_keeps_the_action_name(self): assert "github-actions" not in value def test_only_as_many_middle_segments_drop_as_needed(self): - """Segments go leftmost-middle first, and only until the ref fits.""" + """Segments go leftmost-middle first, and only until the ref fits. + + The segment widths are sized to the budget: this ref is over it by less + than one segment, so exactly one middle has to go. A ref that fits whole + would pin nothing, which is what the 8-wide version of it became when + the cap moved from 38 to 47. + """ + ref = "a/" + "b" * 12 + "/" + "c" * 12 + "/" + "d" * 12 + "/zzz" + value = WorkspaceId("blooop", "dl", ref).value + assert value.startswith("dl-a-" + "c" * 12 + "-" + "d" * 12 + "-zzz-") + assert "b" * 12 not in value + + def test_a_ref_that_fits_whole_keeps_every_segment(self): + """The other side of the same rule: nothing drops until it has to.""" value = WorkspaceId("blooop", "dl", "a/bbbbbbbb/cccccccc/dddddddd/zzz").value - assert value.startswith("dl-a-cccccccc-dddddddd-zzz-") - assert "bbbbbbbb" not in value + assert value.startswith("dl-a-bbbbbbbb-cccccccc-dddddddd-zzz-") def test_first_and_last_segments_survive_heavy_truncation(self): - """However many middles have to go, the ends stay.""" - ref = "aa/" + "m" * 30 + "/" + "n" * 30 + "/zz" + """However many middles have to go, the ends stay. + + Both middles are far wider than the budget rather than just over it: at + 30 each this passed by a single character, so one more character of cap + would have turned it into a test of dropping *one* middle without saying + so. + """ + ref = "aa/" + "m" * 40 + "/" + "n" * 40 + "/zz" value = WorkspaceId("blooop", "dl", ref).value assert value.startswith("dl-aa-zz-") assert "mmmm" not in value and "nnnn" not in value