On this page

The shared checkout's work

Every session works in one shared checkout. How its work is committed onto main, carried, gathered from a side tree and brought back when main moved without it; the queue that lands those commits is the submit queue.

Writing a commit of the shared tree

Every session works in one shared checkout and commits the whole of it: its own work and every other session's. stado change commit writes that commit onto origin's main through a private index, never through the checkout's own index, carrying every change the shared working tree holds against the checkout's HEAD, whoever made it — modified, deleted and untracked paths, ignored ones left out:

stado change commit <checkout> -m '<message>' [--push] [--json]

Before anything is read, GitHub is asked whether it keeps the checkout's origin repository archived. An archived repository is read-only, so no push could land the commit, and it is refused with nothing written (commit-archived): writing a commit in <checkout>: GitHub keeps <owner/name> archived, read-only: no push reaches it, so a commit here never reaches any main, fixed by writing the work in the checkout of the repository that holds the product now (stado product catalog --json names each product's owner repository and surfaces). A GitHub that does not answer is said on stderr (whether GitHub keeps <owner/name> archived is unknown (<cause>); the commit is written) and the commit goes on, since its push meets GitHub itself.

Each changed path is read as HEAD holds it, as the working tree holds it and as main holds it. A path main did not change since HEAD takes the working tree's content; one both changed alike is on main already (alreadyOnMain); one both changed otherwise takes both changes merged by git merge-file. A path whose changes overlap, or that one side deleted while the other changed it, refuses the carry naming every such path with nothing written (commit-tree-overlap; read main's change with git -C <checkout> diff <head> <tip> -- <path>, make the working file hold it beside its own, and commit again). A stale path — content HEAD's own history replaced and nobody wrote since, as stado change sync judges it — is main's past rather than work: it is named under stale and never carried. A copy made from an older version than HEAD's and written since — main moved without the working tree, then a session edited what the tree still held — is read from the version it was made from (of every version the path's history holds, the one whose content differs from the copy by the fewest lines), so its edits are merged onto HEAD's change instead of taking that change back; it is named under rebased with that version's commit (derivedFrom), and when its edits overlap what HEAD changed since, the carry refuses it as commit-tree-overlap, naming that commit. Commits a session made in the checkout itself (git commit) that main lacks are the tree's work too: every path they changed is carried the same way, from where HEAD left main (their merge base) to what the working tree holds, and the carried commit takes HEAD as its second parent, so those commits stay in main's history and the checkout can follow main; a checkout whose own commits main already holds in another form is brought into its history the same way, with nothing else to carry. Left out, as they once were (ownCommits, for stado change submit), they stranded the checkout's main eighteen commits ahead of origin's: no carry could follow, and what they changed never reached main. Once main holds what was carried, the checkout follows it (followed: where it moved, leaving every file written since it was read), and the branch's reflog says stado change commit: followed main to <commit>, <n> path(s) left as the working tree held them (git reflog <branch>), so a working tree later found behind its HEAD names the commit that moved it. The branch moves first, by compare-and-swap, and only then are main's files written and the index read from its tree: a branch another commit moved meanwhile leaves the working tree as it was, and when writing or removing a file fails after the branch moved, the branch goes back to the HEAD it held (its reflog says stado change commit: back to <head>, main's files not taken) and the commit answers the failure, so no file holding main's content stands beside a HEAD that lacks it for the next carry to take as work. With --json the answer is repository, checkout, base, head, paths, alreadyOnMain, stale, rebased, ownCommits (how many commits of its own the carry brought), commit, tree, pushed and followed; the text answer is one line: carried <paths> onto main as <commit> (or the commit written and the change submit that queues it, without --push), where the checkout is, and <n> commit(s) of its own carried with the tree. Real test: tests/change/tree.rs and tests/change/tree/own.rs (change-tree). stado change carry, below, carries the same work but refuses every file main rewrote instead of merging it.

Named files, for a tree whose carry overlaps main

Named files are for the one case the whole tree's carry refuses: a path whose change overlaps main's. Then the other paths may land at once, named, while the overlapping one waits for its merge:

stado change commit <checkout> --put <path> --put <path> --remove <path> -m '<message>' [--since <revision>] [--push] [--json]

While the whole tree carries with no overlap, named files are refused with nothing written, because they would leave the other sessions' work out of the commit (commit-tree-carries, exit 2): writing a commit of named files from <checkout>: the whole working tree carries onto main with no overlap (<n> path(s)), and every session commits the whole shared tree: named files would leave the other sessions' work out of the commit …; fix: commit the whole tree: stado change commit <checkout> --message <message> without --put or --remove. --since without a named file is refused too (commit-put-invalid): the whole tree is judged path by path against main.

A commit of named files fetches origin's main, reads that tree into a private index under the checkout's Git directory (stado-change/commit-<uuid>.index, removed however the command ends), takes each --put <path> from the checkout's own file at that path, records a file its owner may execute as 100755 and any other as 100644, deletes each --remove <path>, and writes the commit on main's tip with git commit-tree, so its author and committer are the identity git resolves for the checkout; the checkout's refs, index and working tree stay as they are, the overlapping path among them. A content taken from elsewhere is refused (--put <path>=<file> takes <path>'s content from outside the shared checkout, which keeps its older <path> for the next commit of the whole tree to put back over this one; commit-put-invalid, exit 2): work is written in the checkout, where every session sees and commits it. --since <revision> names the revision the files were read at: when main changed a named path since, nothing is written and the refusal names the paths and the git diff that shows what landed. Without --since the files are read as the checkout holds them, so the revision is where the checkout's HEAD left main (git merge-base HEAD <tip>): a checkout behind main holds main's earlier content of every path main changed since, and a commit of whole files from it is refused instead of taking back what landed there. Without --push it answers the commit for stado change submit <checkout> --commit <commit>; with --push it pushes it to main as a fast-forward through the repository's pre-push gate, as any push goes. The answer names repository, base (the tip it was written on), commit, tree, paths, pushed and moved (how many times main moved past it while it waited on the gate).

Its refusals are declared under stado.change.commit (stado errors show stado.change.commit): a path that is absolute, leaves the repository or is empty, or a path it names twice: put twice, removed twice, or both put and removed, which the private index would otherwise settle silently by keeping whichever came last (commit-put-invalid, exit 2); a source that is a link, a directory or cannot be read (commit-source-unread, exit 2); a file that still holds a Git merge conflict block (a line opening <<<<<<<, a bare =======, a line opening >>>>>>>), named by its path and the line the block opens on, so an unresolved merge never lands, whether named or carried with the tree (commit-conflicted, exit 2; a binary file is not read for it); a named path main changed since --since (commit-drift), including while the push waited on the gate; files that hold exactly what main holds (commit-empty); a carry of the whole tree pushed onto a main that moved meanwhile (commit-push-moved, run it again); and a push the remote or its gate refused, in its own words, with the written commit named for submit (commit-push-refused). A commit of named files whose push finds that another push landed on main during the minutes the pre-push gate runs (the remote answers cannot lock ref 'refs/heads/main': is at <new> but expected <old>) is not refused: the files were read at --since, so it says main moved past <old> while <commit> waited on the gate; writing it again on main as it now is and writes the commit again on the new main, until it lands or main changed one of its paths (commit-drift); the answer's moved counts how many times. Each refusal leaves main where it was. Real test: tests/change/commit.rs (kompilator test --manifest-path stado-rs/Cargo.toml --test change-commit), against a bare origin whose tree overlaps main on one path, and a pre-push hook of its own.

Carrying the shared checkout's work onto main

stado change carry lands every session's uncommitted work of a shared checkout on origin's main as one commit, judging each file against what main holds:

stado change carry <checkout> -m '<message>' [--push] [--json]

Every path the working tree holds otherwise than its HEAD, untracked files included, is one of four:

  • identical to what main holds (a file main gained since the checkout's HEAD that the checkout holds the same way, or one both removed): left out, listed as as_main;
  • changed only in the checkout: carried, listed as carried (or removed for a file the checkout deleted);
  • rewritten on main since the checkout's HEAD left it: the carry is refused with nothing written (carry-rewritten): carrying <checkout>'s uncommitted work onto <owner/name>'s main (<tip>): main rewrote <n> of its files since <base>, where the checkout's HEAD left main, so carrying them as the checkout holds them would take back what landed there; observed: <path> (main's <commit> <subject>); …, fixed by bringing each file onto what main holds (git diff <base> <tip> -- <path> shows what landed) and carrying again.
  • stale, as stado change sync judges it — content an earlier commit of HEAD's own history gave the path, not written since a later commit replaced it, as a working tree left behind when main moved without it holds it: the carry is refused with nothing written (carry-superseded): carrying <checkout>'s uncommitted work onto <owner/name>'s main (<tip>): <n> file(s) the checkout holds are a copy an earlier commit of its HEAD's history held and a later one replaced: the branch moved without its working tree, so carrying them would take the newer copy back; observed: <path> (replaced by <commit> <subject>); …, fixed by bringing each file onto what HEAD holds (git -C <checkout> checkout HEAD -- <path>, or stado change sync <checkout> --apply for every such path) and carrying again.

A whole-tree commit of every dirty file, made from a checkout whose HEAD was behind main, put each file back as the working tree held it: a file main had rewritten went back to its older copy, a module main declared lost its mod line, and tests main had split came back whole. The carried files are written as change commit writes them, onto main's tip through a private index, with --since set to where the checkout's HEAD left main; the checkout's refs, index and working tree stay as they were. A checkout whose every dirty file already holds what main holds is refused as carry-nothing (stado change sync <checkout> --apply brings its HEAD to main). Every refusal of change commit applies to the carried commit too. Real test: tests/change/carry.rs (kompilator test --manifest-path stado-rs/Cargo.toml --test change-carry).

Bringing a side tree's work into the shared checkout

A session that builds the tree of a commit in a folder of its own — most often under the checkout's ignored build directory — and ends before it commits leaves work no commit carries, that git status never lists and that the next cleaning of that folder deletes. stado change gather brings that work into the shared checkout's working tree, where every session sees it and change carry lands it with everyone's work:

stado change gather <checkout> --from <folder> [--base <revision>] [--apply] [--json]

--from names the folder that mirrors the repository's root (the one whose children are the repository's own, as rust/, Sources/ or docs/). Only its folders that are directories main tracks are walked, so a build's outputs and a package manager's caches beside the work are never read, and a file counts when its path is one main tracks or lies in a directory main tracks. Each file is judged against main's tip and the commit the tree was made from, its base: --base, else the commit a base file at the top of the tree names, else the newest commit of main made before the tree's oldest file was written (baseFrom in the answer says which). Every file is one of:

  • as-main: it holds what main holds;
  • as-base: it holds its base's copy, which the tree never changed (main may have changed it since);
  • superseded: it holds a copy main's history held and replaced;
  • as-here: the working tree already holds it;
  • held-here: the working tree holds another uncommitted change of it — some session's work that bringing the tree's in would overwrite;
  • carried: changed only in the tree, main unchanged since the base;
  • merged: main changed it since the base too, and git merge-file keeps both changes;
  • overlapping: both changed it and the changes overlap, or main added or removed it since the base.

Without --apply the verdicts are answered and nothing is written. With it the carried and merged files are written into the working tree, and the answer lists them under written; stado change carry <checkout> -m '<message>' then lands them. A held-here or overlapping file refuses --apply with nothing written (gather-held, exit 1): bringing the side tree <folder> into the working tree of <checkout> (base <base>, main <tip>): <n> of its file(s) cannot be brought in without losing work …; observed: <path> (the working tree holds another uncommitted change of it); <path> (main's change and the tree's overlap), fixed by making the tree's copy hold both changes (git -C <checkout> diff <base> <tip> -- <path> shows main's) or by carrying the working tree's change first, then gathering again.

The other refusals: a --from that cannot be read, is not a directory or is the checkout itself (gather-from-invalid, exit 2); a tree that holds no file at a path of main — build outputs, a cache, or a folder one level above the tree (gather-nothing, exit 1); a tree without a base that main holds no commit older than (gather-baseless, exit 1, fixed with --base); and a folder of the tree that cannot be listed, or a working-tree file that cannot be written (gather-unread, exit 1, naming the place and the system's words). Real test: tests/change/gather.rs (kompilator test --manifest-path stado-rs/Cargo.toml --test change-gather).

A checkout whose main moved without its working tree

A commit made from a temporary index — as a session does in a checkout that other sessions' uncommitted work shares, so as not to commit theirs — moves main and leaves the working tree as it was. The tree then reads every such change taken back: git diff shows it reverted, the next session reads the old file as the current one, and the next commit made from the working tree reverts it for real.

stado change sync <checkout> [--apply] [--follow [--carried <commit>]...] [--json]

names each path whose working tree differs from HEAD as one of two:

  • stale: its content is what an earlier commit of main gave it, and the file was not written since the commit that replaced that content (for a missing file: the commit that added it is newer than the nearest directory above it that still exists). Nobody edited it; main moved past it.
  • kept: content no commit gave it, or a file written after the commit that replaced its content. It is somebody's work.

Without --apply it only reads. With --apply each stale path takes HEAD's content and index entry (git checkout HEAD -- <path>, or removal when HEAD no longer holds it) and every kept path stays exactly as it was. --json answers {checkout, head, applied, stale: [{path, superseded_by, superseded_at}], kept: [path], followed}. A path that is no Git checkout is refused as stado.change.checkout-invalid (exit 2); a git step or a file it cannot read fails as stado.change.git and stado.files say. tests/change/sync/stale.rs builds both kinds in a real repository and proves --apply restores only the stale ones.

Following origin's main

stado change commit writes onto origin's main and never moves the checkout's refs, so a shared checkout's own main stays where it was while origin's moves on, and a git commit made in the checkout gives it commits of its own that no fast-forward brings back. --follow then moves the checkout's main to origin's with git reset --keep, so uncommitted work stays where it is, once every commit of its own is accounted for:

  • a commit whose change origin's main already holds (git cherry marks it -) is followed past;
  • a commit that landed in another form is named with --carried <commit> (repeat it for each) and kept under refs/stado/carried/<commit>, so it stays reachable;
  • any other commit, and every merge (the tree a merge resolved is in no other commit), refuses the follow.

followed in the answer is {moved, from, to, equivalent: [commit], carried: [commit]}, or null without --follow; without --json the last line is main followed origin's: <from> -> <to>, past N commit(s) whose change main holds and M named carried, or main already is origin's <tip>. The follow is checked before --apply writes a path, so a refused follow gives no path HEAD's content. Refusals, each under stado.change with nothing moved:

  • sync-off-main (exit 1): the checkout is on another branch; switch it to main and follow again;
  • sync-unlanded (exit 1): the checkout's main holds commits origin's lacks whose change is not on it, each named; land each (stado change submit <checkout> --commit <commit>), or name one landed in another form with --carried;
  • sync-carried-unknown (exit 2): a --carried commit is none of the checkout's own that origin's main lacks; the refusal lists those that are;
  • sync-follow-blocked (exit 1): git reset --keep refused the move, as it does when uncommitted work changes a file origin's main changed too; the checkout's main and that file stay as they were. Carry that work onto main (stado change commit <checkout> --message <message> --push) or give stale paths HEAD's content with --apply, then follow again. This one comes after --apply wrote, since only git's move finds it.

--carried without --follow is refused by the parser (exit 2). tests/change/sync/follow.rs follows a real checkout past an equivalent commit, refuses one origin lacks and a merge, keeps a carried one, and provokes each refusal.

stado change submit does the same for the paths its own commit changes, after the candidate is queued, because a temporary-index commit leaves exactly those behind in the checkout it was submitted from: each stale one takes HEAD's content and index entry and is named on stderr (<path> held the content <commit> replaced; it now holds HEAD's, so a commit made from this working tree does not take the change back), and each one holding somebody's edits is kept and named (<path> differs from HEAD with edits made since; it is kept, and a commit made from it as it is may take <commit>'s change back). The candidate stays queued when this step fails; the note names the cause and stado change sync <checkout> --apply. tests/change/submit_leaves_no_stale.rs submits such a commit and reads both paths back.

Source: this website