Two facts about a workspace that are worth knowing but that you never have to
type: the id dl derives for it, and how recent the branch you land on is.
dl user/repo@branch derives one id that names both the devpod workspace (what you
see in dl --ls) and the clone directory under ~/.cache/devlaunch/repos/:
<repo-slug>-<branch-slug>-<hash> at most 47 characters
blooop/devlaunch@main -> devlaunch-main-3j1t
blooop/devlaunch@feature/auth -> devlaunch-feature-auth-np10
blooop/devlaunch@feature-auth -> devlaunch-feature-auth-lsi0
blooop/test_renv@nb4 -> test-renv-nb4-n95z
kinisi-robotics/kinisi_ros@ags-devcontainer-tooling-support
-> kinisi-ros-ags-devcontainer-tooling-suppor-17uu
blooop/devlaunch@dependabot/github_actions/codecov/codecov-action-6
-> devlaunch-dependabot-codecov-action-6-amlt
The four-character suffix is a hash of the full (owner, repo, branch) triple, written
in base 36. It is what makes the id unique: the readable part is shortened to fit the
length limit, and shortening it does not affect whether two branches share an id. Long
branch names drop whole /-separated middle segments before losing characters, so the
part that identifies the branch survives. Note the third and fourth lines above:
feature/auth and feature-auth read the same once slugged but are different branches,
and they get different ids.
The suffix was eight characters and spelled in pronounceable syllables (zovomobo,
hesirora). Base 36 packs 5.17 bits into a character where the syllable table managed
3.0, so the suffix halves in length, and the four characters it gives back go to the
branch, which is the part anyone reads. What it costs is collision headroom: a tenth of
the old room, 20.7 bits against the old 24, or roughly one chance in
37000 that ten near-identical long branches in one repository collide. Only branches
whose readable half truncates to the same string can contend at all, so the number that
matters is a repository's crop of release/999...176-shaped names rather than the count
of workspaces on the machine.
Owner and repo are matched case-insensitively, the way GitHub treats them, so
dl NVIDIA/cuda-samples@main and dl nvidia/cuda-samples@main are the same workspace.
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.
One workspace, one id, three ways of reading it. The id itself is what devpod is
addressed by, what dl --ls prints and what the container's hostname is set to:
everything that has to be unique, or has to be typed back, or has to fit in a DNS
label.
The other two are renderings of that same id, cut to what their surface is for:
| Where | Reads | Why that shape |
|---|---|---|
dl --ls, the hostname, devpod |
devlaunch-main-3j1t |
Addressed and typed back, so it must be unique and it must be one word |
| The terminal tab | devlaunch@main |
A handful of characters read at a glance beside a dozen others, so the suffix goes and the branch stays the id's slug |
| The selector | blooop | devlaunch | main |
One row at a time with the width of a terminal, so the owner comes back and the branch is spelled in full, out of the clone's HEAD |
They are renderings and not separate derivations, which is what keeps them
matchable: the tab is the id with the suffix off and one dash spelled @, so
devlaunch@main and devlaunch-main-3j1t are recognisably the same workspace.
Which dash the @ replaces is not readable off the id, since a repo slug holds
dashes of its own, so the tab's name travels with the launch that resolved it rather
than being recovered later. A workspace you name by its bare id on the command line
is therefore titled by that id. The selector is not: it read the triple to draw the
row, and hands it on with the pick.
The tab is the one that gives something up. It does not name the owner, since an id
never carried one, so a fork and its upstream read alike; and it spells the branch as
the id's slug, so feature/auth reads as feature-auth. Both are recoverable in the
selector, which is where a name is read carefully rather than glanced at.
At 47 characters the id leaves 17 of the 64-byte hostname limit for tools that stack their own prefixes onto the container name. That was about 26 when the hostname was the id without its suffix.
47 is held by devpod instead: it is one character inside devpod's own hard ceiling of 48, and a 49-character id is refused outright rather than truncated.
Branch names must be safe as both git refs and directory names, so a name with a space or a leading dash is rejected rather than quietly rewritten.
The ids on your machine were derived by an older rule, so the directories and containers
already there are named by it. The first dl user/repo… command after upgrading migrates
the cache once, and leaves what it did behind in the cache directory (the two listings
named below). dl --help, dl --version, dl --ls and opening an existing workspace by
name do not trigger it.
This has happened twice. Clone directories were once named after the flattened branch
alone (main), and the suffix was once eight characters of pronounceable syllables
(devlaunch-main-zovomobo). Both reach today's names through the same pass, which
re-derives every id from the (owner, repo, branch) its record already stores, so there
is nothing to do differently depending on which one your cache is on.
Your clone directories are renamed. What was
~/.cache/devlaunch/repos/blooop/devlaunch/main, or
~/.cache/devlaunch/repos/blooop/devlaunch/devlaunch-main-zovomobo, becomes
~/.cache/devlaunch/repos/blooop/devlaunch/devlaunch-main-3j1t. A workspace is a git
clone whose origin points at the .bare cache next to it, and .bare does not move, so
this is a plain rename: branches, history and uncommitted changes all survive, and only
the folder name changes. metadata.json is updated in the same pass, so nothing is left
pointing at the old name.
Your existing devpod containers keep their old ids and are orphaned, and they can
often be repaired rather than replaced. An orphaned container is sourced at the path this
migration just renamed, with the real clone sitting next to it under the new name, which is
precisely what dl --reconcile is for: it re-points
devpod's record at the renamed clone, and dl <workspace> recreate finishes the repair.
That gives you back the clone association and the workspace's identity, though not state that
lived only inside the old container, which nothing can bring back. The repair is
order-dependent: relaunching the branch claims the renamed clone for a fresh container,
and reconcile never re-points a clone a live container holds, so reconcile first, then
relaunch. Left alone, the next dl user/repo@branch simply builds a fresh container
under the new id, and deleting the old one is all that remains for it.
dl does not delete containers for you. Deleting by id is how a running sidecar got
destroyed the last time something tried (kinisi_ros#9766),
so it writes the old ids to
~/.cache/devlaunch/orphaned-workspaces.txt. For the workspaces you are finished with,
the disposal command reads from that listing:
xargs -r -n1 devpod delete < ~/.cache/devlaunch/orphaned-workspaces.txtA clone directory with no metadata record is left alone. Nothing records which branch
it was cloned for, and the old directory name cannot be turned back into one, since feature/auth
and feature-auth both became feature-auth, so a guessed name would be worse than no
rename. Those directories stay exactly where they are and are listed in
~/.cache/devlaunch/unmigrated-clones.txt.
Running dl again changes nothing: the migration is keyed on the version field in
metadata.json, not on directory names, so a branch that happens to look like a new-scheme
id is never mistaken for one. If a migration is interrupted, the next run finishes it. The
version is written last, in the same atomic save as the new paths, so it never claims more
than the filesystem has actually done. A rename the filesystem refuses, on a read-only mount
or under tightened permissions, is treated the same way: the version stays put and every
later run retries the refused directories and repeats the notice until the underlying
refusal is fixed by hand.
dl keeps one bare clone per repo at ~/.cache/devlaunch/repos/owner/repo/.bare/
and cuts every workspace's checkout from it as a sibling directory named after the
workspace id. That is an ordinary clone whose git objects are hardlinks into that cache,
not a git worktree. How much disk a workspace costs
is the accounting for that. What follows is the other half: how fresh the branch
you land on is, which is what decides whether the tip you just pushed is the tip
you get. A launch fetches only the one branch it is launching, so no launch waits
on a repo-wide refresh.
- Attaching to a workspace devpod already knows: no git at all. The workspace
is exactly as you left it; freshness inside it is your own
git pull. - A cold launch (first time this branch is launched on this machine, or a
clone devpod has forgotten): one targeted fetch of that branch, every time.
Push upstream and immediately
dlthe branch and you get the pushed tip. - A branch that does not exist yet: created from the default branch's freshly fetched tip.
- Offline: a warning, and the launch proceeds from whatever the cache holds. It only fails when there is nothing cached to launch from.
- Everything else (other branches, tags, prunes) is refreshed by the background updater within the configured interval (default: 1 hour), which never blocks a launch.
dl <workspace> up prepares one, and running it repeatedly is
cheap on purpose: against a container that is already up, a second up costs one
devpod status and nothing else. It used to also pay the tools setup pass,
~1.7s of devpod ssh to be told the tools it was told about last time, and now
reuses the recorded answer instead. See
The trip a launch can skip for what makes a recorded
answer stop being believed; the short version is that any completed devpod up,
by anything, does.
There is no flag that shares one container across several branches, and none that
warms a workspace in the background. An earlier revision of this section
documented --shared and --warm with worked examples; neither has ever existed
in the shipped dl, which exits 2 on both. test/test_readme_cli_doc.py is what
caught that, by handing every flag the README writes on a dl line to the
binary's own parser. It reads the README and not this page, so the protection
comes from documenting flags there rather than here.