Skip to main content

Module push

Module push 

Source
Expand description

Pushes: the automatic push after a commit, manual push handling, bounded Git queries, retries and progress watching. What: Pushes: the automatic push after a commit, manual push handling, bounded Git queries, retries and progress watching. Why: Push has a long lifecycle with many waits.

In TS you’d write (pseudocode):

// push/index.ts: one `export * from './file'` per module below.

Modules§

auto_push
Post-commit auto-push of a landed commit: the push argv, the surfaced output and the failure reporting. What: The commit service’s push of a landed commit’s branch: which push runs, what its output shows, and how a failure is reported without failing the commit. Why: Every commit in this repository is backed up (AGENTS.md rule APG), the service pushes the branch its record names and never reads HEAD, prints only GitHub remote: lines on success and the full output on failure, and leaves a notice for the next command when the push fails. The service is the only pusher of the repositories it serves and coalesces pending pushes of one branch into one call, so the push needs no lock.
auto_push_branch_key
The per-branch file name of the push failure notice: a reversible, flat, case-safe key. What: The reversible, flat, case-insensitive-safe file name of one branch’s push files. Why: The failure notice of a push is kept per branch (push_failure_notice), and the incumbent’s per-branch push files (src/auto-push-branch-key.ts) used the same names, so a name is the same in both wrappers. Uppercase letters are escaped too, so refs that differ only in case never share a file on a case-insensitive filesystem.
auto_push_destination
Where a plain git push of one named branch goes, from the configuration and the local refs. What: Where a plain git push of one named branch would send it, decided from the configuration and the local refs, or why Git would send it nowhere. Why: The service pushes the branch a record names, which may not be the branch checked out, so it must name both sides of the push itself; naming them from a partial reading of Git’s rules pushed to places a plain git push refuses or never goes (a matching push recreating a branch the remote deleted, one of several upstream branches chosen at random, a destination that another configured refspec had already taken). Everything here follows setup_default_push_refspecs in Git’s builtin/push.c and match_push_refs in remote.c; a setting Git refuses or a form this module does not interpret becomes a skip with a note that names the setting, never a push.
auto_push_dirty_paths
The paths of a dirty worktree and of a fast-forward, and their overlap. What: The paths a dirty worktree has uncommitted work in, the paths a fast-forward would change, and the overlap between them. Why: After an auto-push publishes a merge, local main can follow it even in a worktree that holds uncommitted work, provided the move changes none of the files that work touches. Another session’s staged renames then never keep this branch behind the remote.
auto_push_reconcile
Publishing a landed commit that a moved remote branch rejected: the merge is made from objects alone and pushed. What: Publishing a landed commit when the remote branch advanced after the commit landed: the merge of the remote tip and the local tip is computed in the object store, committed without a worktree or an index, and pushed with a plain push. Why: A rejected push of a branch that the remote moved cannot succeed again, and an ordinary merge in the active worktree can be blocked, or made unsafe, by another consumer’s uncommitted files. git merge-tree --write-tree and git commit-tree read and write only objects, so the active worktree, its index and its untracked files are never touched while merging, and both histories stay reachable from the published tip. Only after the push succeeds is the branch fast-forwarded, and only where nothing can be lost: a dirty worktree follows too when the move changes none of the files its uncommitted work touches. An amended or rebased branch is merged like any other: nothing already published is lost, and the replaced commits stay in the remote history beside the new ones.
auto_push_single_flight
The real Git of a push, its query runner and the failure types of a push. What: The service’s Git for a push: the real Git, its working directory and environment, the command builder, the deadline-bound read-only query and the failure types of a push. Why: The push, its reconcile and the queries before them all start Git the same way, so none re-enters the wrapper and every Git child receives the same environment.
auto_push_trigger
The push a merge, pull, cherry-pick, revert or am hands to the service when it leaves the branch ahead of its upstream. What: Hands a push to the commit service after a command other than git commit added commits to the branch: a merge, a pull’s merge, a cherry-pick, a revert or an applied patch. Why: A clean git merge used to stay local until the next commit pushed it. The owner chose (2026-11-02) that any command adding commits queues the push as a commit does, when the branch tip moved and the branch is now ahead of its upstream and not diverged from it, and that a rebase does not, because a rebased branch would need a forced push. The check reads the branch, its upstream tracking ref and the commit parents from the files, with no Git process: the tip is read before and after the forwarded command, and the push is handed over only when the old tip and the upstream tip are both ancestors of the new tip. The old tip being an ancestor is what leaves a rebase (a rewritten branch) out, pull --rebase and pull.rebase included, without reading the configuration for it.
bounded_push
The auto-push’s real git push with a deadline and its low-speed abort. What: Run the auto-push’s real git push (and its fetch and Git LFS upload) until it ends, or stops showing any sign of work with no connection to a remote open. Why: A push that shows nothing and holds no connection would otherwise hold the service’s push of that branch forever, and every later push of the repository would wait behind it. Such a push is stopped when its stall window passes, the commit stays saved locally, and the failure is reported like any other failed push. A push whose connection to the remote is open is never stopped for time (owner, 2026-11-03: “Wait while connected (Recommended)”; pick 5 of doc/planning/cli-git-shipped-limits-audit.md, the coordinator’s under the owner’s delegation and open to the owner’s veto): it has no ceiling, and the wrapper sets no low-speed abort for it.
bounded_query
Read-only Git queries of the auto-push, each with a deadline and separate output streams. What: Run one read-only Git query that is ended only when it shows no sign of work, with separate output streams. Why: The auto-push asks Git a handful of questions before it pushes, and a question that never returns would hold the service’s push for good. A query that shows no sign of work for the quiet window is stopped and reported, so the commit stays saved. A query that works is never stopped for being slow: no total time bounds it (owner, 2026-11-03: “Stop only on no progress”; the coordinator’s scope addition to picks 4 and 9 of doc/planning/cli-git-shipped-limits-audit.md, open to the owner’s veto). Before this, a query shared the push’s wall-clock stop and the journal showed a git status stopped at 2026-11-03T08:28 with outcome=retrying push_ms=97398. The longest quiet gap of every query is kept in the ledger.
child_class
The classes of Git step a commit starts for itself, with the bound and the notice grace of each. What: The two kinds of Git step a commit starts for itself, and the quiet window and the notice grace of each. A step is ended only when it has shown no sign of work for its window (child_watch.rs); elapsed time alone never ends one (owner, 2026-11-03: “Stop only on no progress”; pick 4 of doc/planning/cli-git-shipped-limits-audit.md, the coordinator’s under the owner’s delegation and open to the owner’s veto). Why: One window for every step had to be long enough for the slowest, so a stalled rev-parse held a commit for as long as a stalled pack-objects, and the first notice came after one second for steps that only needed a moment. The classes follow what a step touches, measured on the repository this crate lives in (doc/planning/cli-git-commit-timeouts.md): a query reads refs, commits and trees and ends in milliseconds whatever the worktree holds, while a step that writes objects, or that compares the index with the worktree, costs time in proportion to the bytes it reads (reset, update-index --refresh and ls-files --modified took 2.9 s on a stat-dirty 20 000-file index against 0.03 s on a clean one, and a cold disk multiplies that). The user’s own git commit with its hooks is not a class: its runtime belongs to the user and it is never bounded.
child_watch
A watcher for waits that block on a child process: notices at the cadence and the child stopped at the bound. What: A watcher for a wait that blocks inside a read or a wait on a child process: it prints a notice after the grace and at every interval while the wait lasts, and ends the child when it has shown no sign of work for the quiet window. Why: The wrapper reads Git objects from a long-lived git cat-file --batch and runs Git steps of a commit with their output captured. Both block in the operating system with nothing printed and no end, so a stalled child looks the same as a slow one and holds the commit for good (issue 1811). A watcher thread sees the wait from outside: the waiting thread marks where a wait begins and ends, the watcher prints, and when the child has shown no sign of work for the window it ends the child so the blocked call returns. Elapsed time never ends a child (owner, 2026-11-03: “Stop only on no progress”; pick 4 of doc/planning/cli-git-shipped-limits-audit.md, the coordinator’s under the owner’s delegation and open to the owner’s veto). The signs of work are those the push reads (push_progress.rs): bytes moved, processor time, a task able to run or waiting for the disk. A child that is slow and works is waited for however long it takes. The window is not measured: no log held a quiet gap before this change, so the class numbers stay as labelled unmeasured windows (child_class.rs), and the longest quiet gap of every watched step is kept for the timing records (quiet_ledger.rs) so a measured interval can replace them. A person’s hook is never ended: every Git the wrapper starts carries core.hooksPath below /dev/null and the hook switches (hooks_off.rs), so no hook of the person is ever below a watched child, and the stop signals the child and what it started, never a process beside it.
manual_push_cause
Why a manual push’s scope could not be established, as the cause its hint is chosen by. What: Why a manual push’s scope could not be established, as a cause the hint is chosen by. Why: Every such failure stops the push with a content-unavailable engine failure, and that one code covers causes the person answers differently: a remote that moved is retried, a remote that could not be read is checked for network and credentials, and output the wrapper cannot read is a defect to report. A hint chosen by the code alone named missing content for all of them, which was true for one. The cause travels with the failure so the hint is chosen by it, never by matching the message text.
manual_push_lifecycle
The manual-push candidate scope of one push: the commits and trees the destination lacks. What: The manual-push candidate scope of one git push: the commits and trees the destination lacks. Why: The manual-push policies (final-newline, forbidden-strings) scan the content a push publishes. The incumbent (src/policy-engine/manual-push-lifecycle.ts) discovers the updates, bounds each by what its destination already has, and hands the scan to the policy engine; a failure stops the push with exit 2.
manual_push_porcelain
Authoritative manual-push updates discovered by a private dry-run push and confirmed with ls-remote. What: The parser of git push --dry-run --porcelain output, which tells the manual-push probe what a push would send. Why: The probe used to learn the update lines from a recording hook written as shell text. Git’s own dry-run report carries the same facts without any program of ours: for each destination a To <url> line, then one tab-separated line per ref (<flag>\t<from>:<to>\t<summary>), then Done. With core.abbrev=no the summary of an update holds the full old and new object names (old..new, or old...new when forced); a new ref, a deletion and a refusal hold a bracketed word instead.
manual_push_probe
The updates a manual git push would send, learned from Git’s own dry-run report and confirmed against the remote. What: The authoritative ref updates a git push would send, discovered by a dry-run push whose report Git writes itself. Why: The manual-push policies must scan exactly what Git would publish, so the probe asks Git: git push --dry-run --porcelain reports each destination and ref update (manual_push_porcelain), the local objects come from that report or from one rev-parse, and git ls-remote --refs confirms each destination value. A remote ref that changed between negotiation and confirmation stops the probe. No program of ours is started for this, and no hook or shell text is written: an earlier version recorded Git’s update lines with a #!/bin/sh hook (doc/planning/cli-git-no-self-invocation.md). Every Git child here is bounded by the query deadline, so a remote that never answers cannot hold the push.
manual_push_range
Published-set and range computation for manual-push scans: which pushed commits a remote does not have. What: The commits a push newly publishes, relative to what each destination remote already has. Why: The manual-push policies scan only what becomes public. A pushed commit is already published when a remote-tracking ref of the destination, or an authoritative prior value of an update to that remote, reaches it (src/policy-engine/manual-push-published.ts). The rest of the history is the scan’s range.
push_deadline_advice
The words that say how to change the push deadline, for the terminal and for the resident commit service. What: The words that tell a person how to change the push deadline, for the program that is running the push. Why: A push in a terminal reads CLI_GIT_PUSH_DEADLINE_SECONDS from that terminal’s environment, but the resident commit service pushes after the commit command returned and reads it from the environment the user manager gave the service when it started, so advice to set the variable is not something a person can act on there. In the service the advice names the user manager’s own command and the restart that makes the service read the new value.
push_failure_notice
The record of a failed backup push and the rule for when a wrapper command shows it. What: The record of a backup push that failed after its commit was saved, and the rule for when the wrapper shows it: the resident service writes one record per failed push under the Git directory of the worktree whose commit it was, a later wrapper command in that worktree shows each record once on standard error, and a later push of the same branch that succeeds removes the records of that branch. Why: The push runs in the service after the commit command returned, so nothing is attached to the command that could print its failure; without a record the person would never learn that their backup push was refused (doc/planning/cli-git-resident-service.md, “After-commit work”). Records are plain files in the worktree’s own Git directory so the check on every command is one directory read that finds nothing when nothing failed. A record holds the branch, the whole commit name and Git’s words with credentials already masked; the wrapper words the notice when it shows it, so the commit name is abbreviated the way Git abbreviates it in the repository as it is then. Records are shown on standard error only, and not at all to a command whose output a program reads (--porcelain, -z, plumbing), so that program neither sees the text in its output nor consumes the record before a person can read it.
push_progress
The bytes a push and its processes move, sampled, and the clock that stops it on a stall. What: The progress of a push: whether the push and everything it started are at work, sampled at a small interval, and the clock that stops it on a stall (no sign of work for the stall window, and no open connection to a remote). Why: A fixed deadline stops a slow push that is still working and lets a stuck push take its whole deadline. A process shows that it works in three ways, each read from /proc, and only a tree that shows none of them is stalled: bytes moved: rchar plus wchar of /proc/<pid>/io, summed over the tree; processor time: utime plus stime of /proc/<pid>/stat growing; a task able to run or waiting for the disk: state R or D in /proc/<pid>/task/<tid>/stat. Measured on 2026-11-03, which decided the three: a 30 MB push to a local bare remote moved 94 MB of rchar and 156 MB of wchar over its four processes while read_bytes was 64 KB and write_bytes came late and in bursts, so the block-layer counters see neither pipes nor sockets and lag behind the transfer; rchar and wchar count read and write calls, not send and recv (102 bytes for 100000 bytes through send/recv, 100102 through read), so a stretch that only curl performs shows through the pipe that feeds it; a dd of 64 MB with conv=fsync was in state D in six of seven looks and used 0 processor ticks (a tick is 10 ms), so a wait for the disk shows in neither bytes nor processor time, only in the state; a parent in vfork is in state D with 0 ticks, the way the controls hold a tree in disk wait exactly; a process reading a silent socket and a sleep are in state S with a constant tick count, so a wait on a server that says nothing shows nothing in the process; only the connection’s state shows it. What each costs: bytes miss work that moves none; processor time makes a process that spins or polls look alive, and its resolution is a tick; state D makes a process wedged for ever in the kernel (a dead network file system) look alive, and state R one that is only waiting for a processor; no total time bounds them (owner decision, see below). None of them sees a server that checks the push before it answers: that tree is in state S, indistinguishable from a dead peer by its processes alone. What tells them apart is the connection: a process of the tree that holds a TCP socket in state ESTABLISHED (read from the kernel’s socket diagnostics, tcp_sockets.rs) has a remote that has not closed it, and a tree with such a socket is waited for, however long it shows nothing. The owner’s answer “Wait while connected (Recommended)” (2026-11-03) is the rule; the rest is pick 5 of doc/planning/cli-git-shipped-limits-audit.md, which the coordinator made under the owner’s delegation (“Pick options as if I had picked them”, 2026-11-03) and the owner may veto: no stall window for a connected push, no ceiling for any push, no low-speed abort. What the code cannot observe: a peer that vanished without a reset leaves the socket ESTABLISHED until TCP gives up (retransmission for unacknowledged data, keepalive probes for an idle connection), which the owner accepted. Where /proc/<pid>/io does not exist (no /proc, or a kernel without task I/O accounting) nothing is measured and the stall window is a fixed deadline, as before.
push_retry
The service’s own retry of a push it stopped or that failed: the pauses, the waiting branches, the final failure. What: The service’s own retry of a push it stopped or that failed: the pause before each new try, the list of branches waiting for one, and the test for the one failure no retry can mend. Why: The owner’s rule is that auto-push must land (SPEC.md, “Merges and commits never fail”). A push stopped by the stall window, by the network, by an LFS server error or by a reference lock passes by itself, so the service tries again on its own, with pauses that grow, until the push lands or a conflict needs the person. Nothing waits for the next commit: the work loop is woken when the pause is over. The list is owned by the service’s work loop and kept on disk beside the instance’s journal (push_retry_store.rs), so a restarted service rebuilds it; the person’s failure record is not the plan, since the person’s next command shows it and removes it. A conflict, a login the service does not have and a hook on the server that declined the push stop the retry until the next commit (owner decision, 2026-11-03).
push_retry_store
The retry plan on disk, in the service instance’s journal, so that a restarted service keeps trying the pushes it was trying. What: The retry plan on disk: one small file per branch whose push waits for a new try, in a directory of the service instance’s own journal, written when a try is planned, removed when the plan ends, and read when a service starts. Why: A service that is stopped (a release, a restart of the unit, a crash) must not forget the pushes it keeps trying: the defect the retry mends is a stopped push that is never tried again, and a restart that forgot the plan would bring it back. The person’s failure record cannot carry the plan: the person’s next command shows it and removes it. The journal is the instance’s own and survives a restart (%S/cli-git/service/%i), and one instance is the only writer of its plan, so no lock is taken. A plan is replaced by publishing the new file whole before the old one is removed, so a kill leaves one of them, never none; when a kill leaves both, the newer one is the plan.
push_stacking
An explicit push that lost a race: run as a child, merge in what landed on the remote and publish the merge. What: An explicit git push that lost a race: the commits that landed on the remote since the local base are merged in the object store, and the merge is published by the wrapper pushing it again through its own gates. Why: Several sessions push to one branch all day; a push rejected because the remote moved cannot succeed again, and every caller would fetch, merge or rebase and retry by hand while racing the others. The wrapper runs an eligible push as a child with the caller’s streams, so Git’s own output is unchanged. Only a failed push is looked at again, and only a divergence of the destination is acted on; an authentication, offline, hook or rule refusal keeps the exit status and the text Git gave. The merge is pushed through the same push planning, run inside this process (stacked_merge_push) with STACKED_PUSH_MARKER set in the planned environment, so the policies that gate any push read the integrated result and the marker stops that push from stacking again. The wrapper never starts itself for this. A push that carries the Git LFS upload also runs here, as a child, so this parent can explain a failed upload.
push_trigger_log
The record of each decision of the push trigger: handed over, or declined with its reason. What: The record of every decision of the push trigger after a command that adds commits: handed over, or declined with the step that declined and why. Why: A merge that was never pushed left nothing to look at, because the trigger returned silently at eight places and the reason sat only in the code. The record is one JSON line appended to a size-capped file in the common Git directory beside the commit timing records, and a debug line under MONOCHROMATIC_VERBOSE=true; the person’s terminal shows neither.
stacked_merge_push
The push of a merge that integrated a lost race, planned and run inside the running wrapper. What: The push of a merge that integrated a lost race, planned and run inside the running wrapper. Why: The merge push must pass every gate a typed git push passes, so the policies read the integrated result. An earlier version started the wrapper’s own executable with a marker variable to get those gates; the wrapper never starts itself, so the same planning (plan_invocation) now runs in this process, with the marker set only in the environment handed to the planner, never in the process environment, and the planned action is run here. The marker stops that planned push from stacking again.
start_retry
Starting a child again when the operating system refuses it for lack of processes, within a bounded wait. What: Starting a child process again when the operating system refuses it for lack of processes. Why: A host that has run out of processes (a process limit, a cgroup limit) refuses a start with EAGAIN, which Rust reports as ErrorKind::WouldBlock. A refused start started nothing, so starting again is always safe, and such a shortage usually ends within moments. Without this the first refusal ends a commit that the next attempt would have finished. The wait is bounded, so the wrapper still never stalls: a shortage that outlasts the budget fails with the same error as before plus how often the start was refused.
tcp_sockets
The connected TCP sockets of the machine with their byte counts, and the sockets a process holds. What: The TCP sockets of this machine that are connected, with the bytes each has sent and had acknowledged and the bytes it has received, and the sockets a process holds. Why: rchar and wchar of /proc/<pid>/io count read and write, not send and recv, so a transfer done through libcurl (git-remote-http) moves no byte the push meter can see, and /proc/net/tcp has queue lengths and no running count. The kernel’s own socket diagnostics (NETLINK_SOCK_DIAG, man 7 sock_diag, the interface ss reads) return, for every socket, its state, its inode and a struct tcp_info with tcpi_bytes_acked and tcpi_bytes_received; a socket a process holds is the socket:[<inode>] link of its /proc/<pid>/fd (man 5 proc_pid_fd).
wait_progress
The shared clock of every wait notice: the grace, the repeat interval and a queued commit’s place in line. What: The shared clock of every wait notice: when the first line of a wait is due, how often it repeats, and the place in line a queued commit holds. Why: A commit that queues behind other commits, a lock, a reservation or a child process must say so, and say it again at a steady interval, or the caller cannot tell a queued commit from a hung one (issue 1811). Each wait loop feeds its elapsed time to a Ticker and prints a line when the ticker says one is due, so the decision is a function of elapsed time alone and a test can drive it with chosen durations. The interval comes from CLI_GIT_WAIT_NOTICE_SECONDS at the production entry of each wait.