Path Syntax¶
vost paths identify files on disk, files in the repo, or files on a specific branch/tag/commit. This document describes the full syntax, how each command interprets it, and how paths interact with flags.
Quick reference¶
| Syntax | Meaning |
|---|---|
file.txt |
Local filesystem path |
:file.txt |
Repo path on the current branch |
: |
Repo root on the current branch |
main:file.txt |
Repo path on the main branch |
main: |
Repo root on the main branch |
v1.0:data/file |
Repo path on the v1.0 tag |
main~3:file.txt |
file.txt three commits back on main |
~2:file.txt |
file.txt two commits back on the current branch |
~1: |
Repo root one commit back on the current branch |
Anatomy of a path¶
A path argument is parsed into three components:
[ref[~N]]:path
| Component | Optional | Description |
|---|---|---|
| ref | yes | Branch name, tag name, or commit hash. Empty string means "current branch" (determined by -b or repo default). Absent (no colon at all) means "local filesystem". |
| ~N | yes | Ancestor suffix. Walk back N parent commits from the ref. N must be a positive integer. |
| path | yes | File or directory path. May be empty (meaning the repo root). |
Parsing rules¶
The parser looks for the first : in the argument and applies these rules in
order:
-
No colon -- the entire argument is a local filesystem path.
-
Colon at position 0 (
:path) -- repo path on the current branch. The ref is the empty string; the path is everything after the colon. -
Single letter before colon, followed by
/or\(e.g.C:/Users,D:\data) -- treated as a Windows drive letter. The entire argument is a local path. -
/or\appears anywhere before the colon (e.g../local:file,/tmp/my:data) -- the colon is part of a filesystem path. The entire argument is a local path. -
Otherwise -- everything before the colon is the ref (possibly with a
~Nsuffix); everything after is the repo path.
Ancestor syntax¶
If the ref portion (the part before :) contains a tilde, the last ~ splits
it into a base ref and an integer suffix:
main~3:file.txt -> ref="main", back=3, path="file.txt"
v1.0~1:data/ -> ref="v1.0", back=1, path="data/"
~2:file.txt -> ref="" (current branch), back=2, path="file.txt"
Constraints:
- The suffix after
~must be a positive integer.main~abc:fis an error. ~0is invalid. Usemain:finstead.
Path normalization¶
Once the repo path is extracted:
- A leading
/is silently stripped::/foois equivalent to:foo. ..components are rejected (to prevent traversal above the repo root).- An empty path means the repo root.
How each command uses paths¶
Commands where the : prefix is optional¶
For ls, cat, rm, and write, arguments are always repo paths, so the
colon is optional. vost cat file.txt and vost cat :file.txt are
equivalent. However, the colon is required to use explicit ref syntax:
vost cat main:file.txt reads from the main branch, while
vost cat main:file.txt without the colon would look for a local file
named main:file.txt -- but since there's no / or \ before the colon, the
parser treats it as a ref:path anyway. In practice, the : matters only for
distinguishing ref:path from plain filenames that happen to contain colons
(rare on Unix, common on Windows -- handled by rule 3/4).
Commands where : is required¶
For cp, sync, and mv, source and destination may be either local or repo
paths (for cp/sync) or must all be repo paths (for mv).
The colon prefix is what distinguishes them.
ls¶
vost ls [PATH...]
Each PATH is parsed independently. Different paths may reference different branches, tags, or ancestors. Results are coalesced and deduplicated.
vost ls # root of current branch
vost ls :src # subdirectory
vost ls main:src # subdirectory on main
vost ls main:src dev:docs # from two branches at once
vost ls main: --back 2 # main, two commits back
vost ls -R main~3:src # recursive listing, 3 back on main
vost ls '*.py' # glob expansion
Glob patterns (*, ?, **) are expanded within the resolved repo tree.
Quote them to prevent shell expansion.
Implicit root: with no arguments, lists the root of the current branch.
cat¶
vost cat PATH [PATH...]
Each PATH is parsed independently. File contents are written to stdout in
argument order, concatenated with no separator (like UNIX cat).
vost cat :file.txt
vost cat main:config.json
vost cat v1.0:data/file.txt
vost cat main:file.txt --back 1 # one commit back on main
vost cat ~1:file.txt # one commit back on current branch
rm¶
vost rm PATH [PATH...]
Each PATH is parsed. All paths must target the same branch -- you cannot remove files from different branches in a single command. When a path has an explicit ref, it determines the target branch.
vost rm :old.txt # remove from current branch
vost rm dev:old.txt # remove from dev
vost rm dev:a.txt dev:b.txt # ok -- same branch
vost rm dev:a.txt main:b.txt # ERROR -- different branches
Tags and commit hashes are not writable:
vost rm v1.0:file.txt # ERROR -- cannot write to tag
Accepts glob patterns and -R for directories.
mv¶
vost mv SOURCE... DEST
All arguments must be repo paths (colon prefix required). All paths must target the same branch — cross-branch moves are not supported.
vost mv :old.txt :new.txt # rename
vost mv ':*.txt' :archive/ # glob move
vost mv -R :data :backup # rename directory
vost mv dev:old.txt dev:new.txt # explicit branch
vost mv main:a.txt dev:b.txt # ERROR — cross-branch
Tags and ancestors are not writable:
vost mv v1.0:a.txt v1.0:b.txt # ERROR — cannot write to tag
vost mv :a.txt main~1:b.txt # ERROR — can't write to history
Accepts glob patterns and -R for directories.
write¶
vost write PATH
Reads stdin and writes it as a single file. The PATH is parsed:
echo "data" | vost write :file.txt # current branch
echo "data" | vost write dev:file.txt # write to dev
echo "data" | vost write ~1:file.txt # ERROR -- can't write to history
When the path has an explicit ref, it overrides -b:
echo "x" | vost write dev:f.txt -b main # ERROR -- conflicting
echo "x" | vost write dev:f.txt # writes to dev
cp¶
vost cp SOURCE... DEST
The last argument is the destination. Direction is determined by which arguments are repo paths:
| Sources | Destination | Direction |
|---|---|---|
| All local | repo (:...) |
disk -> repo |
All repo (:...) |
local | repo -> disk |
All repo (:...) |
repo (:...) |
repo -> repo |
| Mixed local+repo | any | ERROR |
# disk -> repo
vost cp file.txt :
vost cp file.txt :dest/file.txt
vost cp dir/ :dest
# repo -> disk
vost cp :file.txt ./local.txt
vost cp main:file.txt ./local.txt
vost cp main~1:file.txt ./out/
# repo -> repo (cross-branch copy)
vost cp session:/ : # overlay session onto current branch
vost cp main:a.txt :backup/ # copy file from main to backup/ on current branch
vost cp main~1:a.txt dev:b.txt :dest/ # ERROR -- mixed local+repo (just kidding, all repo is fine)
Per-source ref resolution: each source can specify its own ref. Sources
without an explicit ref use the default (set by -b / --ref / snapshot
filters, or the repo default branch).
vost cp main:a.txt :backup/a.txt # source from main
vost cp main:a.txt dev:b.txt :merged/ # two sources, two branches
Destination ref resolution: the destination ref determines which branch is written to.
vost cp file.txt : # write to current branch (from -b or default)
vost cp file.txt dev: # write to dev branch
vost cp file.txt dev~1:path # ERROR -- can't write to history
vost cp file.txt v1.0:path # ERROR -- can't write to tag
sync¶
vost sync PATH # 1-arg: local dir -> repo root
vost sync SOURCE DEST # 2-arg: direction from colon prefix
Sync makes the destination identical to the source (like rsync --delete).
One argument: always a local directory synced to the repo root.
Two arguments: direction is determined by the colon prefix:
| Source | Destination | Direction |
|---|---|---|
| local | :... |
disk -> repo |
:... or ref:... |
local | repo -> disk |
ref:... |
:... or ref:... |
repo -> repo |
vost sync ./dir # dir -> repo root
vost sync ./local :data # disk -> repo
vost sync :data ./local # repo -> disk
vost sync main:data ./local # explicit source ref
vost sync main: dev: # repo -> repo (main overwrites dev)
vost sync main~1: dev: # from one commit back on main
vost sync main: dev: --back 2 # ERROR if -b is also given
log¶
vost log [TARGET]
The optional TARGET is parsed with ref:path syntax. It sets the starting ref, ancestor depth, and/or path filter:
vost log # all commits on current branch
vost log main:config.json # log of config.json on main
vost log ~3: # starting 3 back on current branch
vost log main~3: # starting 3 back on main
vost log config.json # plain path (no colon) -> --path filter
The positional TARGET merges with flags. If both specify the same thing, it's an error:
vost log main: --ref main # ERROR -- ref specified twice
vost log main~3: --back 1 # ERROR -- ancestor specified twice
vost log main:file.txt --path file.txt # ERROR -- path specified twice
vost log main: -b dev # ERROR -- branch conflicts with ref
diff¶
vost diff [BASELINE]
The optional BASELINE selects what to compare against HEAD. Parsed with ref:path syntax:
vost diff # needs --back, --ref, etc.
vost diff ~3: # HEAD vs 3 commits back
vost diff dev: # HEAD vs dev branch
vost diff main~2: # HEAD vs main, 2 back
Same conflict rules as log.
Interaction with flags¶
-b / --branch¶
Sets the default branch for the command. When no explicit ref appears in any
path, -b determines which branch is used.
Conflicts with explicit ref:. If any path argument contains an explicit
ref (non-empty string before :), -b is an error:
vost ls main: -b dev # ERROR
vost cat main:file.txt -b main # ERROR
vost cp main:a.txt : -b dev # ERROR
vost sync main: dev: -b main # ERROR
Bare-colon paths (:path) do not conflict with -b:
vost ls :src -b dev # ok -- :src uses the -b branch
vost cp :file.txt ./out -b dev # ok -- reads from dev
--ref¶
Selects a branch, tag, or commit hash to read from. It is another way to specify the source ref.
Conflicts with explicit ref:. If any path argument has an explicit ref,
--ref is an error:
vost ls main: --ref main # ERROR
vost cp main:a.txt ./out --ref v1.0 # ERROR
Bare-colon paths do not conflict -- --ref fills in the ref for : paths:
vost cp :a.txt ./out --ref v1.0 # ok -- reads a.txt from v1.0
--back N¶
Walk back N parent commits from the resolved ref.
Conflicts with ~N. If any path argument uses the ~N ancestor suffix,
--back is an error:
vost cat ~1:file.txt --back 1 # ERROR
vost cat main~2:file.txt --back 1 # ERROR
When there is no ~N in any path, --back applies to the resolved ref
(whether from explicit ref: or from -b/--ref/default):
vost cat main:file.txt --back 2 # ok -- main, 2 commits back
vost ls main: --back 1 # ok -- main, 1 commit back
--before, --path, --match¶
These snapshot filters narrow the commit selection. They always apply to the
resolved ref and work alongside --back and explicit ref::
vost ls main: --before 2024-06-01 # main as of June 1
vost cat main:config.json --before 2024-01-01 # config.json on main before Jan 1
vost log main: --match "deploy*" # deploy commits on main
Multiple different refs with filters¶
When a command has multiple paths targeting different explicit refs, snapshot
filters (--back, --before, --path, --match) are ambiguous -- which ref
do they apply to?
vost ls main: dev: --back 1 # ERROR -- different refs + filter
vost cp main:a.txt dev:b.txt : --back 1 # ERROR -- different source refs + filter
Paths targeting the same explicit ref are fine:
vost ls main:src main:docs --back 1 # ok -- both from main, 1 back
And bare-colon paths mixed with a single explicit ref are unambiguous:
vost ls main:src :docs --back 1 # ok -- main:src gets --back 1,
# :docs uses default + --back 1
Summary table¶
| Flag | Conflicts with | OK with |
|---|---|---|
-b |
any explicit ref: in path |
bare :path, no-colon paths |
--ref |
any explicit ref: in path |
bare :path, no-colon paths |
--back |
any ~N in path |
explicit ref: (applied to that ref) |
--before |
multiple different explicit refs | single ref or same ref |
--path |
multiple different explicit refs | single ref or same ref |
--match |
multiple different explicit refs | single ref or same ref |
Writability¶
Some commands modify the repo. Only branches are writable -- tags and commit
hashes are read-only, and historical commits (via ~N) are immutable.
| Path | Writable? |
|---|---|
:path |
Yes (current branch) |
dev:path |
Yes (if dev is a branch) |
v1.0:path |
No (tag) |
abc123:path |
No (commit hash) |
dev~1:path |
No (historical commit) |
~1:path |
No (historical commit) |
Commands that write (rm, mv, write, cp destination, sync destination)
validate this and produce a clear error:
Error: Cannot write to tag 'v1.0' -- use a branch
Error: Cannot write to a historical commit (remove ~N from destination)
Direction detection in cp and sync¶
cp¶
The parser examines all arguments:
- Parse every argument with the ref:path rules.
- All sources local + dest is repo -> disk to repo.
- All sources repo + dest is local -> repo to disk.
- All sources repo + dest is repo -> repo to repo.
- Mixed local and repo sources -> error.
- All local (including dest) -> error (no repo involvement).
sync¶
- One argument (no colon): always local directory -> repo root.
- Two arguments: parse both. If both are repo paths, it's repo-to-repo.
Otherwise, the one with
:is the repo side.
Cross-branch workflows¶
The ref:path syntax enables cross-branch operations without a dedicated
transaction system. A typical pattern:
# 1. Create a temporary working branch
vost branch set session
# 2. Make changes on the temporary branch
echo "data" | vost write session:file.txt
vost cp ./new-files/ session:data/
# 3. Copy results to the main branch
vost cp session:/ :
# 4. Clean up
vost branch delete session
Or sync an entire branch:
# Make dev identical to main
vost sync main: dev:
# Sync from a historical snapshot
vost sync main~5: dev:
Ref name restrictions¶
Branch and tag names must not contain:
| Character | Reason |
|---|---|
: (colon) |
Used as the ref:path delimiter |
(space) |
Ambiguous in shell arguments |
\t (tab) |
Whitespace |
\n (newline) |
Whitespace |
Attempting to create a branch or tag with these characters produces an error.
Names with /, ., -, and _ are allowed (e.g. feature/login,
release-1.0).