slipway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +7 -0
  3. data/CHANGELOG.md +45 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +1013 -0
  6. data/exe/slipway +10 -0
  7. data/lib/slipway/cli/builtins.rb +241 -0
  8. data/lib/slipway/cli/completer.rb +158 -0
  9. data/lib/slipway/cli/completion_scripts.rb +163 -0
  10. data/lib/slipway/cli/context.rb +67 -0
  11. data/lib/slipway/cli/errors.rb +19 -0
  12. data/lib/slipway/cli/globals.rb +27 -0
  13. data/lib/slipway/cli/help_renderer.rb +135 -0
  14. data/lib/slipway/cli/manpage.rb +226 -0
  15. data/lib/slipway/cli/parser.rb +45 -0
  16. data/lib/slipway/cli/registry.rb +191 -0
  17. data/lib/slipway/cli/runner.rb +186 -0
  18. data/lib/slipway/cli/style.rb +82 -0
  19. data/lib/slipway/cli/theme.rb +85 -0
  20. data/lib/slipway/cli/validator.rb +61 -0
  21. data/lib/slipway/cli.rb +22 -0
  22. data/lib/slipway/command_line.rb +22 -0
  23. data/lib/slipway/commands/api_resources.rb +82 -0
  24. data/lib/slipway/commands/apply.rb +172 -0
  25. data/lib/slipway/commands/base.rb +50 -0
  26. data/lib/slipway/commands/config.rb +73 -0
  27. data/lib/slipway/commands/create.rb +218 -0
  28. data/lib/slipway/commands/delete.rb +82 -0
  29. data/lib/slipway/commands/describe.rb +74 -0
  30. data/lib/slipway/commands/diff.rb +122 -0
  31. data/lib/slipway/commands/edit.rb +130 -0
  32. data/lib/slipway/commands/explain.rb +97 -0
  33. data/lib/slipway/commands/fetch.rb +112 -0
  34. data/lib/slipway/commands/from_dir.rb +141 -0
  35. data/lib/slipway/commands/get.rb +167 -0
  36. data/lib/slipway/commands/label.rb +114 -0
  37. data/lib/slipway/commands/manual.rb +67 -0
  38. data/lib/slipway/commands/options.rb +73 -0
  39. data/lib/slipway/commands/results.rb +57 -0
  40. data/lib/slipway/commands/rollout.rb +114 -0
  41. data/lib/slipway/commands/rollout_spec.rb +99 -0
  42. data/lib/slipway/commands/rollout_undo.rb +126 -0
  43. data/lib/slipway/commands/scope.rb +156 -0
  44. data/lib/slipway/commands/sync.rb +140 -0
  45. data/lib/slipway/commands.rb +54 -0
  46. data/lib/slipway/drift.rb +87 -0
  47. data/lib/slipway/editor.rb +71 -0
  48. data/lib/slipway/error.rb +27 -0
  49. data/lib/slipway/fetcher.rb +99 -0
  50. data/lib/slipway/field_selector.rb +86 -0
  51. data/lib/slipway/git/branch_name.rb +32 -0
  52. data/lib/slipway/git/commit.rb +13 -0
  53. data/lib/slipway/git/distance.rb +13 -0
  54. data/lib/slipway/git/errors.rb +125 -0
  55. data/lib/slipway/git/fake.rb +147 -0
  56. data/lib/slipway/git/fast_forward.rb +12 -0
  57. data/lib/slipway/git/fast_forwarding.rb +148 -0
  58. data/lib/slipway/git/fetch_result.rb +22 -0
  59. data/lib/slipway/git/move_back.rb +12 -0
  60. data/lib/slipway/git/reflog.rb +25 -0
  61. data/lib/slipway/git/repository.rb +288 -0
  62. data/lib/slipway/git/rolling_back.rb +98 -0
  63. data/lib/slipway/git/runner.rb +175 -0
  64. data/lib/slipway/git/status.rb +110 -0
  65. data/lib/slipway/git/url.rb +95 -0
  66. data/lib/slipway/git.rb +20 -0
  67. data/lib/slipway/inspector.rb +103 -0
  68. data/lib/slipway/labels.rb +126 -0
  69. data/lib/slipway/manifest.rb +265 -0
  70. data/lib/slipway/names.rb +22 -0
  71. data/lib/slipway/outcome.rb +45 -0
  72. data/lib/slipway/output/age.rb +70 -0
  73. data/lib/slipway/output/describe.rb +71 -0
  74. data/lib/slipway/output/explain.rb +75 -0
  75. data/lib/slipway/output/serializer.rb +35 -0
  76. data/lib/slipway/output/table.rb +67 -0
  77. data/lib/slipway/output.rb +28 -0
  78. data/lib/slipway/paths.rb +65 -0
  79. data/lib/slipway/plan.rb +227 -0
  80. data/lib/slipway/pool.rb +94 -0
  81. data/lib/slipway/resources.rb +91 -0
  82. data/lib/slipway/rollback.rb +236 -0
  83. data/lib/slipway/rollout_history.rb +69 -0
  84. data/lib/slipway/runtime.rb +65 -0
  85. data/lib/slipway/scanner.rb +54 -0
  86. data/lib/slipway/schema.rb +128 -0
  87. data/lib/slipway/selector.rb +146 -0
  88. data/lib/slipway/settings.rb +174 -0
  89. data/lib/slipway/state.rb +82 -0
  90. data/lib/slipway/store.rb +170 -0
  91. data/lib/slipway/syncer.rb +139 -0
  92. data/lib/slipway/version.rb +5 -0
  93. data/lib/slipway/views/group.rb +35 -0
  94. data/lib/slipway/views/project.rb +148 -0
  95. data/lib/slipway/views.rb +10 -0
  96. data/lib/slipway/yaml.rb +14 -0
  97. data/lib/slipway.rb +32 -0
  98. data/man/man1/slipway-api-resources.1 +53 -0
  99. data/man/man1/slipway-apply.1 +45 -0
  100. data/man/man1/slipway-completion.1 +29 -0
  101. data/man/man1/slipway-config-path.1 +20 -0
  102. data/man/man1/slipway-config-view.1 +25 -0
  103. data/man/man1/slipway-config.1 +22 -0
  104. data/man/man1/slipway-create.1 +89 -0
  105. data/man/man1/slipway-delete.1 +46 -0
  106. data/man/man1/slipway-describe.1 +95 -0
  107. data/man/man1/slipway-diff.1 +120 -0
  108. data/man/man1/slipway-edit.1 +34 -0
  109. data/man/man1/slipway-explain.1 +36 -0
  110. data/man/man1/slipway-fetch.1 +77 -0
  111. data/man/man1/slipway-get.1 +155 -0
  112. data/man/man1/slipway-help.1 +19 -0
  113. data/man/man1/slipway-label.1 +53 -0
  114. data/man/man1/slipway-man.1 +36 -0
  115. data/man/man1/slipway-rollout-history.1 +28 -0
  116. data/man/man1/slipway-rollout-pause.1 +21 -0
  117. data/man/man1/slipway-rollout-resume.1 +21 -0
  118. data/man/man1/slipway-rollout-undo.1 +73 -0
  119. data/man/man1/slipway-rollout-unpin.1 +21 -0
  120. data/man/man1/slipway-rollout.1 +36 -0
  121. data/man/man1/slipway-sync.1 +90 -0
  122. data/man/man1/slipway-version.1 +17 -0
  123. data/man/man1/slipway.1 +243 -0
  124. metadata +173 -0
data/README.md ADDED
@@ -0,0 +1,1013 @@
1
+ # slipway
2
+
3
+ A kubectl-style registry of the git repositories on your machine: it shows where each one stands
4
+ against its upstream and its manifest, and fast-forwards the ones that can move safely.
5
+
6
+ [![CI](https://github.com/hvpaiva/slipway/actions/workflows/ci.yml/badge.svg)](https://github.com/hvpaiva/slipway/actions/workflows/ci.yml)
7
+
8
+ `slipway get projects` shows in one table which of the git repositories you registered are clean,
9
+ dirty, behind their upstream or missing from disk, and `slipway fetch` fetches them all without
10
+ ever stopping at a password prompt. Each registration is a manifest that can also declare where
11
+ its repository should be, such as the remote, the branch or a commit to hold it at: `slipway diff`
12
+ shows how each repository differs from that, `slipway sync` fast-forwards the branches that can
13
+ move without losing anything, and `slipway rollout undo` takes such a move back.
14
+
15
+ Three repositories are cloned under `~/dev`. Register them, two in a `personal` group and one in
16
+ the `default` group:
17
+
18
+ ```console
19
+ $ slipway create group personal --description "Personal projects"
20
+ group/personal created
21
+
22
+ $ slipway create project hldr --path '~/dev/hldr' -n personal --label lang=rust --description "Site and CLI for hvpaiva.dev" --remote git@github.com:hvpaiva/hldr.git --branch main
23
+ project/hldr created
24
+
25
+ $ slipway create project augur --path '~/dev/augur' -n personal --label lang=bash
26
+ project/augur created
27
+
28
+ $ slipway create project notes --path '~/dev/notes' --label kind=docs
29
+ project/notes created
30
+
31
+ $ slipway get projects -A
32
+ GROUP NAME BRANCH STATUS FETCHED AGE
33
+ default notes main Ahead 2d 0s
34
+ personal augur main Dirty <never> 1s
35
+ personal hldr main Clean 5h 1s
36
+ ```
37
+
38
+ STATUS is read from the repository on disk, so it is as fresh as the last fetch, whose age
39
+ FETCHED shows. Fetching every project brings in what was pushed from other machines since:
40
+
41
+ ```console
42
+ $ slipway fetch -A
43
+ project/notes fetched
44
+ origin/main 1c2d3e4..5f6a7b8
45
+ project/augur skipped (NoRemote)
46
+ no upstream, no origin and no single remote to fetch from
47
+ project/hldr fetched
48
+ origin/main e001395..4c5d6e7
49
+ 3 projects: 2 fetched, 1 skipped
50
+
51
+ $ slipway get projects -A
52
+ GROUP NAME BRANCH STATUS FETCHED AGE
53
+ default notes main Diverged 0s 1s
54
+ personal augur main Dirty <never> 1s
55
+ personal hldr main Behind 0s 1s
56
+ ```
57
+
58
+ hldr is now behind `origin/main`, and notes, which had a commit of its own, has diverged from
59
+ it. `diff` compares each repository with its manifest and says what `sync` would do, and `sync`
60
+ does it:
61
+
62
+ ```console
63
+ $ slipway diff -A
64
+ project/notes
65
+ Behind: 1 commit behind origin/main
66
+ Diverged: 1 ahead, 1 behind origin/main; sync never merges or rebases
67
+ git -C ~/dev/notes log --oneline --left-right HEAD...@{upstream}
68
+ project/augur
69
+ NoUpstream: main tracks no upstream; sync fast-forwards only a tracking branch
70
+ project/hldr
71
+ Behind: 3 commits behind origin/main; sync will fast-forward
72
+
73
+ $ slipway sync -A
74
+ project/notes skipped (Diverged)
75
+ 1 ahead, 1 behind origin/main; sync never merges or rebases
76
+ git -C ~/dev/notes log --oneline --left-right HEAD...@{upstream}
77
+ project/augur skipped (NoRemote)
78
+ no upstream, no origin and no single remote to fetch from
79
+ project/hldr fast-forwarded
80
+ main e001395..4c5d6e7 (3 commits); undo with 'slipway rollout undo project/hldr -n personal'
81
+ 3 projects: 1 fast-forwarded, 2 skipped
82
+ ```
83
+
84
+ A move you did not want is undone with the command printed under it:
85
+
86
+ ```console
87
+ $ slipway rollout undo project/hldr -n personal
88
+ project/hldr rolled back
89
+ main 4c5d6e7..e001395 (3 commits back to revision 1); held there by spec.revision
90
+ 'slipway rollout unpin project/hldr -n personal' follows origin/main again
91
+ ```
92
+
93
+ The command line follows kubectl: verbs over resource types, groups used the way kubectl uses
94
+ namespaces (`-n personal`, `-A`), label selectors (`-l lang=rust`), manifests you can
95
+ `apply -f`, and table, json or yaml output. STATUS words, drift and the result lines of
96
+ `fetch`, `sync` and `rollout` are slipway's own, and the sections below define each of them.
97
+
98
+ ## Installation
99
+
100
+ Slipway is not published on RubyGems yet. Until it is, install it from a checkout:
101
+
102
+ ```sh
103
+ git clone https://github.com/hvpaiva/slipway.git
104
+ cd slipway
105
+ bundle install
106
+ bundle exec rake install
107
+ ```
108
+
109
+ Once it is published, `gem install slipway` installs it, and so does `mise use -g gem:slipway`
110
+ with [mise](https://mise.jdx.dev).
111
+
112
+ Slipway needs Ruby 3.4 or newer and git 2.35 or newer on `PATH`, and has no runtime gem
113
+ dependencies. Git before 2.41 cannot tell `unchanged` from `fetched`, so every fetch that
114
+ succeeds reads `fetched`, without the refs that moved. Fetching over ssh without a prompt needs
115
+ OpenSSH 8.4 or newer; an older ssh may still ask on the terminal.
116
+
117
+ ## Usage
118
+
119
+ | Verb | What it does |
120
+ | --- | --- |
121
+ | `get TYPE [NAME...]` | List resources as a table, or as wide, json, yaml or name output. |
122
+ | `describe TYPE [NAME...]` | Print every field of the selected resources, including the repository state. |
123
+ | `create TYPE NAME` | Register a project (`--path DIR`, `--description`, `--label`, `--remote`, `--branch`) or create a group; `-o yaml` prints the manifest. |
124
+ | `create project --from-dir DIR` | Register every git repository at or under a directory, with its origin URL. |
125
+ | `apply -f FILE` | Create or update resources from manifests; prints `created`, `configured` or `unchanged`. |
126
+ | `delete TYPE NAME...` | Remove registrations, a deleted group's projects included, never the repositories on disk; prints `project "hldr" deleted from personal group`. `--ignore-not-found` makes an unknown name a success. |
127
+ | `edit TYPE NAME` | Open the manifest in your editor and save what comes back. |
128
+ | `label TYPE NAME KEY=VALUE...` | Set or remove labels on a resource. |
129
+ | `explain TYPE[.FIELD...]` | Print the fields of a manifest, or of one field, with the type, rule and default of each; `--recursive` prints the whole tree. |
130
+ | `fetch [NAME...]` | Run `git fetch` in the selected projects, without prompts; prints `fetched`, `unchanged`, `skipped`, `paused`, `denied` or `failed`. |
131
+ | `diff [NAME...]` | Show where projects differ from their manifests, without contacting a remote; exit status 3 when any does. |
132
+ | `sync [NAME...]` | Fetch the selected projects and fast-forward each clean branch that is behind; prints `fast-forwarded`, `fetched`, `unchanged`, `skipped`, `paused`, `denied` or `failed`. |
133
+ | `rollout history NAME` | List the revisions slipway moved a project's branch to, read from the branch reflog. |
134
+ | `rollout undo NAME` | Move the branch back to the previous revision, or to `--to-revision=N`, and hold it there with `spec.revision`. |
135
+ | `rollout unpin NAME...` | Remove `spec.revision`, so `sync` follows the upstream again. |
136
+ | `rollout pause NAME...`, `rollout resume NAME...` | Set or remove `spec.paused`, which keeps `fetch` and `sync` away from a project. |
137
+ | `config view`, `config path` | Show the configuration in effect and the file it came from. |
138
+ | `completion SHELL` | Print the completion script for bash, zsh or fish. |
139
+ | `man [COMMAND]` | Open the bundled manual page of a command. |
140
+ | `api-resources` | List the resource types with their short names and kind, and whether they live in a group; `-o wide` adds the verbs that act on each. |
141
+ | `version` | Print the version, the Ruby it runs on and the platform, as `slipway 0.1.0 (ruby 4.0.7) [x86_64-linux]`. |
142
+ | `help [COMMAND]` | Print the same text as `--help`. |
143
+
144
+ Two resource types exist: `projects` (also `project`, `proj`) and `groups` (also `group`), and
145
+ type words are case-insensitive; `slipway api-resources` lists them
146
+ ([Discovering resources](#discovering-resources)). A project is named bare (`hldr`) or as
147
+ `project/hldr`, the form `get -o name` prints. `create`, `apply`, `delete`, `label`, `fetch`,
148
+ `sync` and `rollout undo` accept `--dry-run`, which fetches and writes nothing and ends each
149
+ result line with `(dry run)`. `slipway VERB --help` describes each verb, `-h` prints help and
150
+ `-V` the version.
151
+
152
+ ### Groups
153
+
154
+ Projects live in groups the way pods live in namespaces. `-n NAME` (`--group`) selects one,
155
+ `-A` (`--all-groups`) lists across all of them and adds a GROUP column. Without either, the
156
+ `default` group is used, or the one set by `SLIPWAY_GROUP` or the `group` config key. The
157
+ default group is created the first time a write needs it; every other group has to be created
158
+ first, and `slipway delete group default` is refused.
159
+
160
+ ```console
161
+ $ slipway get groups
162
+ NAME PROJECTS AGE
163
+ default 1 1s
164
+ personal 2 1s
165
+
166
+ $ slipway describe group personal
167
+ Name: personal
168
+ Labels: <none>
169
+ Created: 2026-09-29T07:15:35Z
170
+ Age: 1s
171
+ Description: Personal projects
172
+ Projects: 2
173
+ ```
174
+
175
+ PROJECTS counts the projects registered in the group. `-o wide` adds DESCRIPTION, and
176
+ `--show-labels` adds LABELS for groups as it does for projects.
177
+
178
+ ### Registering existing clones
179
+
180
+ `slipway create project --from-dir ~/dev/personal -n personal` registers every git repository
181
+ at or under the directory, searching `--depth` levels down (1 by default, at most 8). The search
182
+ stops at a directory with a `.git` entry, so a linked worktree counts and nothing nested inside
183
+ a repository does, and it never follows a symbolic link below the directory. Each project is
184
+ named after its directory, lowercased, with each run of characters other than ASCII letters,
185
+ digits and dashes made one dash (`My.Notes_v2` becomes `my-notes-v2`). `spec.path` starts with
186
+ `~/` under `HOME`, and `spec.remote` is the URL of `origin` without its credentials: the whole
187
+ user part over http and https, the password elsewhere. An origin that still breaks the
188
+ `spec.remote` rule, such as a local path, is left out with a `warning:` line. The checked-out
189
+ branch is not recorded, since it may be a feature branch; set `spec.branch` with `edit` when you
190
+ want it.
191
+
192
+ A path the group already holds prints `project/NAME unchanged`, whatever its name, so running
193
+ the command again registers only the new clones. A derived name that another path already has
194
+ is reported after the other lines, as `error: ~/dev/foo: project "foo" already exists at
195
+ ~/dev/Foo`, with exit status 1. `--dry-run -o yaml` prints the projects as one
196
+ `kind: List` without writing anything, ready for `slipway apply -f` on another machine.
197
+
198
+ ### Describing
199
+
200
+ `describe` prints the manifest, the repository as git reports it, the last commit and the
201
+ [drift](#drift). hldr, after the rollback above, is held at a commit its upstream has moved past:
202
+
203
+ ```console
204
+ $ slipway describe project hldr -n personal
205
+ Name: hldr
206
+ Group: personal
207
+ Labels: lang=rust
208
+ Created: 2026-09-29T07:15:35Z
209
+ Age: 1s
210
+ Path: ~/dev/hldr
211
+ Description: Site and CLI for hvpaiva.dev
212
+ Remote: git@github.com:hvpaiva/hldr.git
213
+ Branch: main
214
+ Revision: e001395 (pinned)
215
+ Sync Policy: FastForward
216
+ Paused: false
217
+ Status: Behind
218
+ Repository:
219
+ Branch: main
220
+ Head: e001395
221
+ Upstream: origin/main
222
+ Ahead: 0
223
+ Behind: 3
224
+ Staged: 0
225
+ Unstaged: 0
226
+ Untracked: 0
227
+ Conflicted: 0
228
+ Stashes: 0
229
+ Remote: git@github.com:hvpaiva/hldr.git
230
+ Last Fetch: 2026-09-29T07:15:35Z
231
+ Last Commit:
232
+ Hash: e001395f1c8d1b0e8a7c0c56f7b0e1a4b3c2d1e0
233
+ Author: Highlander <contact@hvpaiva.dev>
234
+ Date: 2026-09-27T23:15:35Z
235
+ Subject: feat: list posts by year
236
+ Drift: <none>
237
+ ```
238
+
239
+ STATUS compares the branch with its upstream and ignores the pin, while drift compares the
240
+ repository with the manifest, so hldr is Behind with no drift. When git cannot read the
241
+ repository, the Repository block holds git's reason instead of the fields.
242
+
243
+ ### Labels
244
+
245
+ ```console
246
+ $ slipway label project hldr tier=web -n personal
247
+ project/hldr labeled
248
+
249
+ $ slipway label project hldr tier=web -n personal
250
+ project/hldr not labeled
251
+
252
+ $ slipway label project hldr tier=api -n personal
253
+ error: 'tier' already has a value (web), and --overwrite is false
254
+
255
+ $ slipway label project hldr --list -n personal
256
+ lang=rust
257
+ tier=web
258
+
259
+ $ slipway label project hldr tier- -n personal
260
+ project/hldr unlabeled
261
+ ```
262
+
263
+ `KEY=VALUE` sets a label and `KEY-` removes one. A key that already has a different value is
264
+ changed only with `--overwrite`. `not labeled` means nothing changed: the label already had
265
+ that value, or the label to remove was not set.
266
+
267
+ ### Editing
268
+
269
+ `slipway edit project hldr -n personal` writes the manifest to a temporary file, opens it in
270
+ `SLIPWAY_EDITOR`, then the `editor` config key, then `VISUAL`, then `EDITOR`, or `vi`, and
271
+ saves what comes back as `project/hldr edited`. Text that changes without changing the object
272
+ prints `project/hldr skipped`. An unchanged file prints `Edit cancelled, no changes made.` on
273
+ stderr; an invalid one is reopened with the failure as a comment block at the top, and saving
274
+ that reopened file unchanged aborts with `error: Edit cancelled, no valid changes were saved.`;
275
+ an empty file aborts with `error: Edit cancelled, saved file was empty.`. Both aborts exit
276
+ with status 1.
277
+
278
+ ### Fetching
279
+
280
+ `slipway fetch` runs `git fetch` in the projects of the current group, in the projects named,
281
+ in the ones `-l` selects, or with `-A` in every project. Git fetches from the remote of the
282
+ checked-out branch, else from the only remote, else from origin, as a `git fetch` typed in the
283
+ repository would: slipway passes no remote, and nothing from a manifest reaches git's arguments.
284
+ Git updates the refs the remote's fetch refspecs name (remote-tracking refs by default), tags and
285
+ `FETCH_HEAD`, never the checked-out branch or the working tree. A second fetch right after the
286
+ one above finds nothing new:
287
+
288
+ ```console
289
+ $ slipway fetch -A
290
+ project/notes unchanged
291
+ project/augur skipped (NoRemote)
292
+ no upstream, no origin and no single remote to fetch from
293
+ project/hldr unchanged
294
+ 3 projects: 2 unchanged, 1 skipped
295
+ ```
296
+
297
+ Up to `parallel` projects (4 by default) fetch at once. Each prints one result, in the order the
298
+ projects are listed, as soon as it and every project before it are done:
299
+
300
+ | Result | Meaning |
301
+ | --- | --- |
302
+ | `fetched` | The remote moved refs. Up to five follow, then `and N more`: `origin/main a1b2c3d..e4f5a6b` for a ref that moved, `origin/feature d09a085 (new)` for a new one and `origin/feature deleted (was d09a085)` for one `--prune` removed. Tags appear under their bare name. |
303
+ | `unchanged` | The remote answered and had nothing new. |
304
+ | `skipped (Reason)` | No fetch ran: git could not read the repository (`Missing`, `NotARepo`, `Unsafe`, `Unknown`), there is no upstream, no origin and no single remote (`NoRemote`), or the branch tracks a local branch (`LocalUpstream`). |
305
+ | `paused` | The manifest sets `spec.paused: true`, so no git command ran in the project. |
306
+ | `denied (AuthRequired)` | Git needed a password, a passphrase or a host key. Run the `git -C PATH fetch` printed below it once in a terminal to see what git needs. |
307
+ | `failed (Reason)` | The fetch ran past `networkTimeout` (`Timeout`), used a transport `protocols` leaves out (`ProtocolNotAllowed`), or git failed for another reason (`Unknown`). |
308
+
309
+ When more than one project ran, a count of the results closes the run on stderr. The exit
310
+ status is 1 when any project was denied or failed, once every line has printed.
311
+
312
+ `--prune` also removes the remote-tracking refs of branches deleted on the remote, and it is
313
+ what makes a branch whose upstream was deleted read `Gone`: a plain fetch leaves the old ref in
314
+ place, unless git's `fetch.prune` is set. To bring every STATUS up to date, whatever the fetch
315
+ reports:
316
+
317
+ ```sh
318
+ slipway fetch -A --prune; slipway get projects -A
319
+ ```
320
+
321
+ Git never prompts during a fetch: slipway sets `GIT_TERMINAL_PROMPT=0`, points `GIT_ASKPASS` and
322
+ `SSH_ASKPASS` at `false`, and sets `SSH_ASKPASS_REQUIRE=force`, so an ssh from OpenSSH 8.4 on
323
+ never reads the terminal.
324
+ Your ssh configuration, `SSH_AUTH_SOCK` and credential helpers are used as they are. Only ssh
325
+ and https remotes are fetched unless the `protocols` setting adds more, so an `http://`,
326
+ `git://` or local path remote fails with `ProtocolNotAllowed` until it does. A fetch that runs
327
+ past `networkTimeout` is killed with every process it started, submodules are not fetched, and
328
+ gc, automatic maintenance and bundle URIs are off. On Ctrl-C slipway stops the git processes it
329
+ started and exits with status 130.
330
+
331
+ ### Diffing
332
+
333
+ `slipway diff` compares every project of the current group, the ones named, the ones a selector
334
+ matches, or with `-A` every project, with its manifest. Each project that differs prints its
335
+ name and one line per [drift](#drift) item, and an item that has a git command to show or
336
+ resolve it is followed by that command, as in the example at the top of this page. A project
337
+ that matches its manifest prints nothing. Slipway never runs these commands, never writes to a
338
+ repository or to the registry, and contacts no remote.
339
+
340
+ The exit status is 0 when every project matches its manifest and 3 when any differs or is
341
+ blocked, `NotARepo` and `Unsafe` included. It is 1 on an error, such as an unreadable manifest
342
+ or a project whose state is `Unknown`, so a script or a timer can tell drift from failure.
343
+
344
+ ### Syncing
345
+
346
+ `slipway sync` fetches the projects of the current group, the ones named, the ones `-l` selects,
347
+ or with `-A` every project, as `slipway fetch` does. It then compares each one with its manifest
348
+ as `slipway diff` does and fast-forwards the checked-out branch onto its upstream with
349
+ `git merge --ff-only --no-autostash` when nothing blocks it: the branch has commits and tracks
350
+ an upstream that still exists, is behind it and not ahead of it, and has no staged, unstaged or
351
+ conflicted changes and no merge, rebase, cherry-pick, revert, bisect or `git am` in progress.
352
+ Untracked files do not block it; git refuses a fast-forward that would overwrite one, and sync
353
+ reports that. Sync never pulls, merges, rebases, stashes, resets, cleans, pushes, switches a
354
+ branch, changes a remote or removes a lock. [SECURITY.md](SECURITY.md#safety-promises) lists
355
+ every promise slipway makes about the repositories it touches.
356
+
357
+ | Result | Meaning |
358
+ | --- | --- |
359
+ | `fast-forwarded` | The branch moved. For a move onto the upstream, the detail names the commits it gained, as `main a1b2c3d..e4f5a6b (3 commits)`, and the command that undoes the move. |
360
+ | `fetched` | The fetch of a `FetchOnly` project moved refs; the refs follow as in `fetch`. |
361
+ | `unchanged` | The branch stayed where it was and nothing blocked it; the fetch may still have moved remote-tracking refs. |
362
+ | `skipped (Reason)` | The branch stayed where it was: the project was skipped as in `fetch`, a [blocker](#drift) stopped it, or git refused the fast-forward for untracked files in the way (`WouldOverwrite`), changes `git status` hides (`WouldLoseChanges`), another git process's lock (`Busy`) or a commit the branch gained since the check (`NotFastForward`). |
363
+ | `paused` | The manifest sets `spec.paused: true`, so no git command ran in the project. |
364
+ | `denied (AuthRequired)` | The fetch or the fast-forward needed a password, a passphrase or a host key, as in `fetch`. |
365
+ | `failed (Reason)` | The fetch or the fast-forward failed as in `fetch` (`Timeout`, `ProtocolNotAllowed`, `Unknown`). A fast-forward stopped at `networkTimeout` leaves the branch where it was but keeps the files git wrote; the detail names the command that lists them. |
366
+
367
+ A difference sync leaves alone, such as a `Remote` or a `Branch` [drift](#drift), and a branch
368
+ that `FetchOnly` keeps behind follow as detail lines. A project pinned by `spec.revision` is
369
+ fast-forwarded up to that commit, `main a1b2c3d..b2c3d4e (to the pinned revision)`, and then
370
+ stays there whatever its upstream brings. hldr, held by the rollback above, stays put:
371
+
372
+ ```console
373
+ $ slipway sync hldr -n personal
374
+ project/hldr unchanged
375
+ held at e001395 by spec.revision
376
+ ```
377
+
378
+ A pin the repository lacks is skipped as `RevisionNotFound`, a HEAD past the pin as
379
+ `PastRevision`, and a pin its upstream does not hold, such as a commit on another branch or a
380
+ fork, as `OffUpstream`: a manifest can hold a project back but never send it where its upstream
381
+ has not been.
382
+
383
+ Up to `parallel` projects fetch at once, while fast-forwards run one at a time; each result prints
384
+ in the order the projects are listed, and a count of the results closes the run on stderr. Each
385
+ fast-forward leaves `slipway sync: Fast-forward` in the branch's reflog. The exit status is 1
386
+ when a fetch or a fast-forward was denied or failed, once every line has printed; a skipped
387
+ project never changes it, so a dirty tree does not fail the run. With `--dry-run`, sync
388
+ plans from the last fetch and warns about the projects no fetch has reached.
389
+
390
+ ### Rolling back
391
+
392
+ Slipway undoes its own moves. Each fast-forward of `sync` runs with `GIT_REFLOG_ACTION` set to
393
+ `slipway sync` and each move of `rollout undo` with `slipway rollout undo`, so the branch's reflog
394
+ records them and slipway keeps no history of its own. `slipway rollout history NAME` lists them as
395
+ revisions, oldest first: every commit slipway moved the checked-out branch to, and the commit the
396
+ branch stood at before such a move.
397
+
398
+ ```console
399
+ $ slipway rollout history hldr -n personal
400
+ REVISION COMMIT DATE CHANGE-CAUSE PINNED
401
+ 1 e001395 2026-09-27T23:15:35Z <none> false
402
+ 2 4c5d6e7 2026-09-29T07:15:35Z sync: fast-forward 3 commits false
403
+ 3 e001395 2026-09-29T07:15:35Z rollout undo to revision 1 true
404
+
405
+ $ slipway rollout unpin hldr -n personal
406
+ project/hldr unpinned
407
+
408
+ $ slipway sync hldr -n personal
409
+ project/hldr fast-forwarded
410
+ main e001395..4c5d6e7 (3 commits); undo with 'slipway rollout undo project/hldr -n personal'
411
+ ```
412
+
413
+ CHANGE-CAUSE names the move and PINNED marks the revision `spec.revision` holds. `history`
414
+ fetches and writes nothing; a project without history prints
415
+ `No rollout history found for project/NAME.` on stderr and exits with 0.
416
+
417
+ `slipway rollout undo NAME` moves the checked-out branch to the revision before the current one,
418
+ or to the one `--to-revision=N` names, and then writes that commit to `spec.revision`, so `sync`
419
+ holds the project there. The manifest is written only after git moved the branch. A move back runs
420
+ `git reset --keep`, the one form of reset slipway ever runs, and only when the upstream holds every
421
+ commit the move drops; a move forward, which undoes an undo, runs `git merge --ff-only`. Untracked
422
+ files and unstaged changes to files the move leaves alone are kept. A move back needs an index
423
+ without staged changes, because `reset --keep` resets every index entry.
424
+
425
+ | Result | Meaning |
426
+ | --- | --- |
427
+ | `rolled back` | The branch moved, or it already stood at the revision and only `spec.revision` changed. The detail names the commits it crossed and the command that lets `sync` follow the upstream again. |
428
+ | `unchanged` | The branch already stood at the revision and `spec.revision` already held it. |
429
+ | `skipped (Reason)` | Nothing moved and nothing was written. git could not read the repository (`Missing`, `NotARepo`, `Unsafe`, `Unknown`); the branch cannot move (`Conflicted`, `Detached`, `Unborn`, `NoUpstream`, `Gone`, `InProgress`); the history has no such revision (`NoHistory`, `NoPrevious`, `UnknownRevision`) or the repository no such commit (`RevisionNotFound`); a move back would drop commits the upstream lacks (`LocalCommits`) or staged changes (`Dirty`), or the revision is off the branch's history (`Diverged`); a move forward needs a tree without staged or unstaged changes (`Dirty`) and a revision on the upstream (`OffUpstream`); or git refused the move (`WouldLoseChanges`, `WouldOverwrite` for untracked or ignored files in the way, `Busy`, `NotFastForward` for a branch that moved since the check). |
430
+ | `denied (AuthRequired)` | A partial clone had to fetch the files the move writes and the remote asked for a password, a passphrase or a host key, as in `fetch`. |
431
+ | `failed (Reason)` | The move ran past `networkTimeout` (`Timeout`) or git failed for another reason (`Unknown`), and `spec.revision` was not written, though a move stopped at the deadline keeps the files git had already written, as in `sync`; or the branch moved but `spec.revision` could not be written (`NotPinned`), and the detail names the `--to-revision` command that writes it without moving the branch again. |
432
+
433
+ The exit status is 1 when the project was skipped, denied or failed.
434
+
435
+ `slipway rollout unpin NAME...` removes `spec.revision` and prints `unpinned`, or `not pinned`
436
+ when there was none; the next `sync` fast-forwards onto the upstream as usual.
437
+ `slipway rollout pause NAME...` sets `spec.paused` and `slipway rollout resume NAME...` removes
438
+ it, printing `paused` or `already paused` and `resumed` or `not paused`. None of the three runs
439
+ git. A paused project can still be rolled back: pausing keeps only `fetch` and `sync` away.
440
+
441
+ The history is the local reflog, so it lasts as long as git keeps it (`gc.reflogExpire`, 90 days
442
+ by default; slipway never runs `git gc`) and is empty when `core.logAllRefUpdates` is off. The pin
443
+ lives in the manifest and outlasts the reflog. Rollout moves commits only.
444
+
445
+ ### Periodic fetch
446
+
447
+ `fetch` moves no branch and touches no working tree, so it can run on a timer that keeps
448
+ FETCHED current, and with it every STATUS word that compares a branch with its upstream. `sync`
449
+ is not meant for a timer: it moves checked-out branches, and a branch should not move under an
450
+ open editor or a half-done change unless you ask. A systemd user timer that fetches every
451
+ project once an hour:
452
+
453
+ ```ini
454
+ # ~/.config/systemd/user/slipway-fetch.service
455
+ [Unit]
456
+ Description=Fetch every project in the slipway registry
457
+
458
+ [Service]
459
+ Type=oneshot
460
+ ExecStart=/path/to/slipway fetch -A
461
+ ```
462
+
463
+ ```ini
464
+ # ~/.config/systemd/user/slipway-fetch.timer
465
+ [Unit]
466
+ Description=Fetch every project in the slipway registry hourly
467
+
468
+ [Timer]
469
+ OnCalendar=hourly
470
+ RandomizedDelaySec=5m
471
+ Persistent=true
472
+
473
+ [Install]
474
+ WantedBy=timers.target
475
+ ```
476
+
477
+ ```sh
478
+ systemctl --user daemon-reload
479
+ systemctl --user enable --now slipway-fetch.timer
480
+ journalctl --user -u slipway-fetch.service
481
+ ```
482
+
483
+ `ExecStart` takes an absolute path: replace `/path/to/slipway` with what `command -v slipway`
484
+ prints. The service runs with the environment of the systemd user manager, which
485
+ `systemctl --user show-environment` prints, not with your shell's: git has to be on its `PATH`,
486
+ and a `SLIPWAY_*` variable exported in your shell profile does not reach it, so put settings in
487
+ the config file or in `Environment=` lines of the service. `Persistent=true` runs a fetch the
488
+ timer missed while the machine was off as soon as the timer starts again. `journalctl` shows
489
+ the result lines of each run. A run in which a project was denied or failed exits with status 1,
490
+ and `systemctl --user status slipway-fetch.service` reports it as failed; a skipped project does
491
+ not fail the run.
492
+
493
+ The ssh client finds your agent through `SSH_AUTH_SOCK`, which the user manager has only when
494
+ your session imported it. When `show-environment` does not list it, add
495
+ `Environment=SSH_AUTH_SOCK=...` with the agent's socket to the service, run
496
+ `systemctl --user import-environment SSH_AUTH_SOCK` from a shell that has it, or name the agent
497
+ with `IdentityAgent` in `~/.ssh/config`, as the setup of the 1Password SSH agent does.
498
+
499
+ An agent that asks before it signs, as the 1Password one does while it is locked or before it
500
+ has approved the program asking, shows its prompt when the timer runs, whether or not you are
501
+ there to answer. Slipway keeps git and ssh from prompting, but the agent's own dialog is outside
502
+ ssh: the fetch waits until `networkTimeout` (60 seconds by default) ends it and every process it
503
+ started, and reports `failed (Timeout)`; a dismissed prompt reports `denied (AuthRequired)`. The
504
+ other projects fetch either way. `Environment=SLIPWAY_NETWORK_TIMEOUT=20` in the service shortens
505
+ that wait for the timer's runs alone.
506
+
507
+ ### A daily routine
508
+
509
+ With the timer running, the first look of the day needs no network:
510
+
511
+ ```sh
512
+ slipway get projects -A --field-selector status.state!=Clean
513
+ slipway diff -A
514
+ slipway sync -A
515
+ ```
516
+
517
+ `get` lists the projects that need attention, with STATUS as of the timer's last fetch, whose
518
+ age FETCHED shows. `diff` says which of them sync will fast-forward, what blocks the others and
519
+ the git command that resolves each blocker, still without contacting a remote. `sync` fetches
520
+ once more, fast-forwards each clean branch that is behind and reports what it leaves alone with
521
+ the reason. To move only some projects, name them (`slipway sync hldr augur -n personal`) or
522
+ select them with `-l`. `slipway get projects -A --field-selector status.lastFetch=never` lists
523
+ the projects no fetch has reached, including those whose last fetch was denied or failed.
524
+ A fast-forward you did not want is undone with the `slipway rollout undo` command sync prints
525
+ under it, as [Rolling back](#rolling-back) describes.
526
+
527
+ ## Output formats
528
+
529
+ `-o table` is the default. `-o wide` adds PATH, HEAD, LAST-COMMIT and DRIFT to projects and
530
+ DESCRIPTION to groups. `-o json` and `-o yaml` print the manifest plus a `status` section, as
531
+ one object when a single name is given and as a `kind: List` otherwise. `-o name` prints
532
+ `project/hldr` lines. `--no-headers` drops the header row and `--show-labels` appends a LABELS
533
+ column with `lang=rust` style pairs.
534
+
535
+ ```console
536
+ $ slipway get projects -n personal -o wide
537
+ NAME BRANCH STATUS FETCHED AGE PATH HEAD LAST-COMMIT DRIFT
538
+ augur main Dirty <never> 1s ~/dev/augur 8f9cdbb 11h NoUpstream
539
+ hldr main Clean 0s 1s ~/dev/hldr 4c5d6e7 120m <none>
540
+ ```
541
+
542
+ AGE is the time since the resource was registered and LAST-COMMIT the age of the checked-out
543
+ commit, in kubectl's units (`3s`, `4m12s`, `11h`, `2y319d`). FETCHED is the time since the
544
+ repository was last fetched, by slipway or by git itself and from any of its worktrees. It reads
545
+ `<never>` when git answered and no fetch is on record, which includes a last fetch that failed,
546
+ and `<none>` when git could not read the repository at all (`Missing`, `NotARepo`, `Unsafe`,
547
+ `Unknown`), like every other cell that comes from git. BRANCH reads `(detached)` on a detached HEAD.
548
+ DRIFT lists the [drift](#drift) words.
549
+
550
+ `-o json` prints the same object as `-o yaml`. A project's `status` holds `branch`, `head`,
551
+ `upstream`, `ahead`, `behind`, the counts `staged`, `unstaged`, `untracked`, `conflicted` and
552
+ `stashes`, the STATUS word as `state`, `lastFetch`, the [drift](#drift) items as `drift` and the
553
+ checked-out commit as `lastCommit`:
554
+
555
+ ```console
556
+ $ slipway get project augur -n personal -o yaml
557
+ kind: Project
558
+ metadata:
559
+ name: augur
560
+ group: personal
561
+ labels:
562
+ lang: bash
563
+ creationTimestamp: '2026-09-29T07:15:35Z'
564
+ spec:
565
+ path: "~/dev/augur"
566
+ status:
567
+ branch: main
568
+ head: 8f9cdbb
569
+ staged: 0
570
+ unstaged: 1
571
+ untracked: 1
572
+ conflicted: 0
573
+ stashes: 0
574
+ state: Dirty
575
+ drift:
576
+ - type: NoUpstream
577
+ message: main tracks no upstream; sync fast-forwards only a tracking branch
578
+ blocker: true
579
+ lastCommit:
580
+ hash: 8f9cdbbc5ff8982322347d8e6a76ea3ab8821b51
581
+ author: Highlander
582
+ email: contact@hvpaiva.dev
583
+ date: '2026-09-28T20:15:35Z'
584
+ subject: 'refactor: split history reader'
585
+ ```
586
+
587
+ A field git could not answer is left out: augur has no `upstream`, `ahead`, `behind` or
588
+ `lastFetch`, and a project git could not read, such as a Missing one, keeps only `state` and
589
+ `drift`. A group's status holds `projects`, its project count.
590
+
591
+ ## Status words
592
+
593
+ STATUS is one word per project, chosen in this order of precedence:
594
+
595
+ | STATUS | Meaning |
596
+ | --- | --- |
597
+ | `Missing` | The registered path is relative, or is not a directory on this machine. |
598
+ | `NotARepo` | The directory exists but no repository contains it. |
599
+ | `Unsafe` | git refused the repository because another user owns it (`safe.directory`); `describe`, `diff` and `fetch` print the git command that trusts it. |
600
+ | `Conflicted` | The working tree has unmerged paths. |
601
+ | `Detached` | HEAD points at a commit rather than a branch. |
602
+ | `Unborn` | The branch has no commits yet. |
603
+ | `Dirty` | Staged, modified or untracked files are present. |
604
+ | `Gone` | An upstream is configured but its remote-tracking ref is gone, as of the last `fetch --prune` (or a fetch with `fetch.prune` set). |
605
+ | `Diverged` | The branch is both ahead of and behind its upstream, as of the last fetch (FETCHED). |
606
+ | `Ahead` | Commits not yet pushed to the upstream, as of the last fetch (FETCHED). |
607
+ | `Behind` | Commits on the upstream not yet pulled, as of the last fetch (FETCHED). |
608
+ | `Clean` | Nothing to do. |
609
+ | `Unknown` | git could not answer: it is not installed, it did not finish within 10 seconds, or it failed for a reason slipway does not classify. Each distinct reason is printed once on stderr per run. |
610
+
611
+ `get`, `describe` and `diff` never contact a remote, so the words that compare a branch with its
612
+ upstream are as fresh as the last fetch. [Fetching](#fetching) refreshes them.
613
+
614
+ ## Selectors
615
+
616
+ `-l EXPR` (`--selector`) filters by labels with kubectl's grammar. Equality:
617
+
618
+ ```console
619
+ $ slipway get projects -A -l lang=rust
620
+ GROUP NAME BRANCH STATUS FETCHED AGE
621
+ personal hldr main Clean 0s 1s
622
+ ```
623
+
624
+ Set-based:
625
+
626
+ ```console
627
+ $ slipway get projects -A -l 'lang in (rust,bash)'
628
+ GROUP NAME BRANCH STATUS FETCHED AGE
629
+ personal augur main Dirty <never> 1s
630
+ personal hldr main Clean 0s 1s
631
+ ```
632
+
633
+ `key!=value`, `key notin (a,b)`, `key` (exists) and `!key` (does not exist) work as well, and
634
+ comma-separated terms must all hold.
635
+
636
+ `--field-selector EXPR` filters `get` and `describe` on the fields of the object `-o json`
637
+ prints, with kubectl's field grammar: `path=value` (or `path==value`) and `path!=value`,
638
+ comma-separated, all of which must hold. Projects support `metadata.name`, `metadata.group`,
639
+ `spec.path`, `status.state`, `status.branch` and `status.lastFetch`; groups support
640
+ `metadata.name`.
641
+
642
+ ```console
643
+ $ slipway get projects -A --field-selector status.state!=Clean
644
+ GROUP NAME BRANCH STATUS FETCHED AGE
645
+ default notes main Diverged 0s 1s
646
+ personal augur main Dirty <never> 1s
647
+ ```
648
+
649
+ `status.lastFetch=never` selects the projects no fetch has reached. The `--field-selector` help
650
+ and slipway-get(1) give the matching rules. Neither kind of selector, nor `-A`, can be combined
651
+ with explicit names.
652
+
653
+ ## Manifests
654
+
655
+ Every registration is a manifest. `slipway get project hldr -n personal -o yaml` prints it
656
+ followed by `status`; without the status, a Project and a Group look like this:
657
+
658
+ ```yaml
659
+ kind: Project
660
+ metadata:
661
+ name: hldr
662
+ group: personal
663
+ labels:
664
+ lang: rust
665
+ creationTimestamp: '2026-09-29T07:15:35Z'
666
+ spec:
667
+ path: "~/dev/hldr"
668
+ description: Site and CLI for hvpaiva.dev
669
+ remote: git@github.com:hvpaiva/hldr.git
670
+ branch: main
671
+ ```
672
+
673
+ ```yaml
674
+ kind: Group
675
+ metadata:
676
+ name: personal
677
+ labels: {}
678
+ creationTimestamp: '2026-09-29T07:15:35Z'
679
+ spec:
680
+ description: Personal projects
681
+ ```
682
+
683
+ Names follow the RFC 1123 label rule (lowercase letters, digits and dashes, at most 63
684
+ characters) and labels follow the Kubernetes rules. `metadata.group` defaults to the current
685
+ group and `creationTimestamp` is set on creation; written by hand, it must be a quoted string.
686
+ Unknown fields are errors; `slipway explain` lists the known ones
687
+ ([Discovering resources](#discovering-resources)).
688
+
689
+ A project's spec says where its repository is and declares the state it is expected to be in.
690
+ Only `spec.path` is required, and a field at its default is not written:
691
+
692
+ | Field | Meaning | Default |
693
+ | --- | --- | --- |
694
+ | `spec.path` | The directory of the repository: an absolute path, or one starting with `~/`, which is expanded against `HOME` when used so that the manifest means the same on every machine. A relative path is reported as `Missing`. Must not be empty. | required |
695
+ | `spec.description` | What the project is, in free text. | none |
696
+ | `spec.remote` | The URL the `origin` remote is expected to have; `diff` reports another one as `Remote` drift, and no command changes a remote. A password, or any user name over http and https, where it often carries a token, is refused; use a credential helper. Must be `scheme://host/path` with scheme `ssh`, `https`, `http`, `git` or `file`, or `[user@]host:path`, where the host is letters, digits, dots and dashes starting with a letter or digit; at most 2048 characters, without whitespace or control characters. | none |
697
+ | `spec.branch` | The branch expected to be checked out; `diff` reports another one as `Branch` drift, and no command switches branches. Must be letters, digits, ".", "_", "/" and "-", starting with a letter or digit, at most 255 characters, with no "..", no "//", no component that starts with "." or ends with ".lock", no trailing "/" or ".", and not `HEAD`. | none |
698
+ | `spec.revision` | The commit the project is held at, named in full because an abbreviation can become ambiguous. `sync` fast-forwards the branch up to it instead of the upstream, never past it and never back to it. `rollout undo` writes it and `rollout unpin` removes it. Must be a full object name, 40 or 64 lowercase hexadecimal characters. | none |
699
+ | `spec.syncPolicy` | What `sync` may do to the repository: `FastForward` lets it fast-forward the checked-out branch, and `FetchOnly` lets it fetch only. Must be `FastForward` or `FetchOnly`. | `FastForward` |
700
+ | `spec.paused` | When `true`, `fetch` and `sync` leave the project alone: they report it as `paused` and run no git command there. `rollout pause` sets it and `rollout resume` removes it. | `false` |
701
+
702
+ A `~` path is stored as written. `create` resolves any other relative `--path`, `.` included,
703
+ against the current directory and stores it absolute. The shell expands an unquoted `~` before
704
+ slipway sees it, so quote it, as the examples here do, to keep the manifest portable.
705
+
706
+ `fetch` and `sync` leave a project with `spec.paused: true` alone, and `sync` follows
707
+ `spec.syncPolicy` and `spec.revision`. STATUS does not take any of the fields into account; the
708
+ repository is compared with them as [drift](#drift). The fields are checked whenever a manifest
709
+ is read, and a value that breaks its rule is refused with that rule, so nothing that could reach
710
+ git as an option or carry a control character is accepted. A file in the registry that cannot
711
+ be read is reported with a `warning:` line and left out of listings.
712
+
713
+ `slipway apply -f FILE` reads every YAML document in the file, `-f DIR` reads every `*.yaml`
714
+ and `*.yml` file in the directory sorted by name (without descending), and `-f -` reads stdin.
715
+ `-f` may be repeated. A document of kind `List` stands for each manifest under its `items`, in
716
+ order, and one that fails is named by its position (`FILE:3`). Each document prints
717
+ `project/hldr created`, `configured` or `unchanged`; problems are collected and printed as
718
+ `error: FILE[:N]: ...` after the successes, with exit status 1.
719
+
720
+ ### Drift
721
+
722
+ Drift is where a project's repository differs from its manifest, and what keeps sync from
723
+ fast-forwarding it. `-o wide` lists the words in the DRIFT column, `-o json` and `-o yaml` list
724
+ the items in `status.drift` (each with `type`, `message` and `blocker`), and `describe` ends
725
+ with a Drift block of one line per item. Nothing is fetched: the repository is read as it is on
726
+ disk, so Behind is as of the last fetch (FETCHED), and `slipway fetch` refreshes it.
727
+
728
+ | Drift | Reported when |
729
+ | --- | --- |
730
+ | `Missing` | The registered path is relative or is not a directory. For a path that is not a directory, a project with `spec.remote` shows the `git clone` command that recreates it. |
731
+ | `Remote` | origin is absent or differs from `spec.remote`. Sync never changes a remote. |
732
+ | `Branch` | HEAD is detached or on another branch than `spec.branch`. Sync never switches branches. |
733
+ | `Revision` | HEAD is not the commit `spec.revision` pins. The pin replaces the upstream, so a pinned project is never Behind; under `FastForward` sync will fast-forward a branch behind the pin to it unless a blocker stops it, and never moves a branch back. |
734
+ | `Behind` | The checked-out branch is behind its upstream. Under `FastForward` sync will fast-forward it unless a blocker stops it; under `FetchOnly`, or while `spec.paused` is true, it is only reported. |
735
+
736
+ A blocker comes after the drift and says why the checked-out branch cannot be fast-forwarded,
737
+ onto its upstream or, for a project pinned by `spec.revision`, onto the pin. The first three mean
738
+ git could not read the repository and apply to every project; the others apply only under
739
+ `FastForward` to a project that is not paused, and `RevisionNotFound`, `PastRevision` and
740
+ `OffUpstream` only to a pinned one. Behind means behind the upstream or, when pinned, behind the
741
+ pin:
742
+
743
+ | Blocker | Meaning |
744
+ | --- | --- |
745
+ | `NotARepo` | The directory exists but holds no repository. |
746
+ | `Unsafe` | git refused the repository because another user owns it (`safe.directory`); the git command that trusts it follows. |
747
+ | `Unknown` | git could not answer; the reason is also printed once on stderr. |
748
+ | `Detached` | HEAD points at a commit rather than a branch. |
749
+ | `Unborn` | The branch has no commits yet. |
750
+ | `Gone` | The upstream is configured but its ref no longer exists. |
751
+ | `NoUpstream` | The branch tracks no upstream. |
752
+ | `RevisionNotFound` | The repository has no commit by the name `spec.revision` pins. |
753
+ | `PastRevision` | HEAD is past the pinned commit or on another line of history, so reaching the pin would move the branch back. |
754
+ | `OffUpstream` | The upstream does not hold the pinned commit, which may be on another branch or a fork; sync moves a branch only along its upstream. |
755
+ | `Conflicted` | The branch is behind and the working tree has unmerged paths. |
756
+ | `Dirty` | The branch is behind and has staged or unstaged changes; untracked files do not block. |
757
+ | `Diverged` | The branch is behind and has commits of its own. |
758
+ | `InProgress` | The branch would be fast-forwarded, but a merge, rebase, cherry-pick, revert, bisect or `git am` is in progress. |
759
+
760
+ ## Discovering resources
761
+
762
+ `slipway api-resources` lists the resource types, as `kubectl api-resources` lists the ones a
763
+ cluster serves:
764
+
765
+ ```console
766
+ $ slipway api-resources
767
+ NAME SHORTNAMES KIND GROUPED
768
+ groups <none> Group false
769
+ projects proj Project true
770
+
771
+ $ slipway api-resources -o wide
772
+ NAME SHORTNAMES KIND GROUPED VERBS
773
+ groups <none> Group false apply,create,delete,describe,edit,explain,get,label
774
+ projects proj Project true apply,create,delete,describe,diff,edit,explain,fetch,get,label,rollout,sync
775
+ ```
776
+
777
+ A command accepts a type by its NAME, its singular or one of its SHORTNAMES, and KIND is what a
778
+ manifest of the type declares in `kind`. GROUPED plays the part of kubectl's NAMESPACED: it says
779
+ whether the resources of a type live in a group, so that `-n` and `-A` scope them. VERBS lists
780
+ the commands that act on the type, and `-o name` prints the names alone.
781
+
782
+ `slipway explain` prints the fields of a manifest in the terminal, in the layout of
783
+ `kubectl explain`: the type of each field in angle brackets, `-required-` when a manifest must
784
+ have it, and a description with the rule its value follows and its default. A type word prints
785
+ the fields at the top of its manifest:
786
+
787
+ ```console
788
+ $ slipway explain group
789
+ KIND: Group
790
+
791
+ DESCRIPTION:
792
+ A namespace that holds projects, the way a Kubernetes namespace holds pods.
793
+ Deleting a group removes the registrations of its projects.
794
+
795
+ FIELDS:
796
+ kind <string> -required-
797
+ The kind of resource the manifest describes. Must be Group.
798
+
799
+ metadata <Object> -required-
800
+ Identifies the group: its name, its labels, and when it was created.
801
+
802
+ spec <Object>
803
+ What the group is for.
804
+ ```
805
+
806
+ A dotted path after the type word names a field, and `--recursive` prints the whole tree of names
807
+ and types instead of the descriptions:
808
+
809
+ ```console
810
+ $ slipway explain project.spec.syncPolicy
811
+ KIND: Project
812
+
813
+ FIELD: syncPolicy <string>
814
+
815
+ DESCRIPTION:
816
+ What sync may do to the repository: FastForward lets it fast-forward the
817
+ checked-out branch, and FetchOnly lets it fetch only. Must be FastForward or
818
+ FetchOnly. Defaults to FastForward.
819
+
820
+ $ slipway explain project --recursive
821
+ KIND: Project
822
+
823
+ DESCRIPTION:
824
+ A registered git repository: where it is on this machine and where it is
825
+ expected to be, as the remote, the branch and a commit to hold it at.
826
+
827
+ FIELDS:
828
+ kind <string> -required-
829
+ metadata <Object> -required-
830
+ name <string> -required-
831
+ group <string>
832
+ labels <map[string]string>
833
+ creationTimestamp <string>
834
+ spec <Object> -required-
835
+ path <string> -required-
836
+ description <string>
837
+ remote <string>
838
+ branch <string>
839
+ revision <string>
840
+ syncPolicy <string>
841
+ paused <boolean>
842
+ ```
843
+
844
+ Type words take their aliases and any case, as everywhere else, while field names are exact, as
845
+ in a manifest. A field that does not exist is a usage error, `error: field "x" does not exist`,
846
+ with exit status 2. [Shell completion](#shell-completion) offers the type words and then, after
847
+ each dot, the fields one level down as whole paths: `slipway explain project.spec.<TAB>` lists
848
+ `project.spec.path` through `project.spec.paused`, and no space follows a field that has fields
849
+ under it, so you can go on with a dot.
850
+
851
+ ## Configuration
852
+
853
+ Settings are resolved in this order: command-line flags, then `SLIPWAY_*` environment
854
+ variables, then the config file, then the built-in defaults. The file lives at
855
+ `$XDG_CONFIG_HOME/slipway/config.yaml` (`~/.config/slipway/config.yaml`) unless `--config PATH`
856
+ or `SLIPWAY_CONFIG` names another one. The default file is optional, and so is every key in it;
857
+ a file named by `--config` or `SLIPWAY_CONFIG` must exist.
858
+
859
+ ```yaml
860
+ # ~/.config/slipway/config.yaml
861
+ color: auto # auto, always or never
862
+ theme: light # dark or light
863
+ editor: code --wait
864
+ group: personal # used when -n is not given
865
+ networkTimeout: 60 # seconds before a git network command is killed, from 1 to 86400
866
+ parallel: 4 # git network commands at once, from 1 to 16
867
+ protocols: [ssh, https] # transports git may use; add file for local mirrors
868
+ ```
869
+
870
+ The defaults are `color: auto`, `theme: dark`, `group: default`, `networkTimeout: 60`,
871
+ `parallel: 4` and `protocols: [ssh, https]`; `editor` has none, so `edit` falls back to
872
+ `VISUAL`, `EDITOR` and `vi`. `slipway config view` prints the values in effect with the file
873
+ path as a comment on the first line, and `slipway config path` prints the path alone. An unknown
874
+ key or a wrong value is an error naming the file. `protocols` refuses `ext` and `fd` even when
875
+ listed: `ext` runs a command named in the URL, and `fd` reads from file descriptors.
876
+
877
+ | Variable | Effect |
878
+ | --- | --- |
879
+ | `SLIPWAY_CONFIG` | Path of the configuration file; `--config` outranks it. |
880
+ | `SLIPWAY_DATA_HOME` | Directory holding the registry. |
881
+ | `SLIPWAY_COLOR` | `auto`, `always` or `never`; `--color` outranks it. |
882
+ | `SLIPWAY_THEME` | `dark` or `light`. |
883
+ | `SLIPWAY_EDITOR` | Editor for `edit`; outranks the config key, `VISUAL` and `EDITOR`. |
884
+ | `SLIPWAY_GROUP` | Group used when `-n` is not given. |
885
+ | `SLIPWAY_NETWORK_TIMEOUT` | Seconds a git network command may run before it is killed, from 1 to 86400. |
886
+ | `SLIPWAY_PARALLEL` | How many git network commands run at once, from 1 to 16. |
887
+ | `SLIPWAY_PROTOCOLS` | Transports git may use in network commands, separated by colons: `ssh:https`. |
888
+ | `SLIPWAY_DEBUG` | When non-empty, unexpected errors also print their class and backtrace. |
889
+ | `NO_COLOR` | When non-empty, disables color in `auto` mode. |
890
+ | `FORCE_COLOR` | When non-empty, enables color in `auto` mode even on a pipe. |
891
+ | `CLICOLOR_FORCE` | Same as `FORCE_COLOR`. |
892
+ | `VISUAL` | Editor for `edit` when `SLIPWAY_EDITOR` and the `editor` config key are unset. |
893
+ | `EDITOR` | Editor for `edit` when `VISUAL` is unset as well. |
894
+ | `XDG_CONFIG_HOME` | Base of the configuration directory (default `~/.config`). |
895
+ | `XDG_DATA_HOME` | Base of the data directory (default `~/.local/share`). |
896
+ | `TERM` | `dumb` turns color off in `auto` mode. |
897
+ | `MANPAGER` | When non-empty, `slipway man` leaves the pager palette alone. |
898
+ | `MANROFFOPT` | Same as `MANPAGER`. |
899
+ | `LESS_TERMCAP_md` | Same as `MANPAGER`. |
900
+ | `GROFF_NO_SGR` | Same as `MANPAGER`. |
901
+
902
+ The registry is a directory of plain YAML files under `$SLIPWAY_DATA_HOME`, by default
903
+ `$XDG_DATA_HOME/slipway` (`~/.local/share/slipway`):
904
+
905
+ ```
906
+ groups/<group>.yaml
907
+ projects/<group>/<name>.yaml
908
+ ```
909
+
910
+ Each file is the manifest shown above and nothing else is stored, so the directory can be
911
+ backed up, versioned or synced between machines. To rebuild a registry from a copy, apply
912
+ `groups/` first and then each `projects/<group>/` directory:
913
+ `slipway apply -f copy/groups -f copy/projects/personal`.
914
+
915
+ ## Colors
916
+
917
+ `--color[=auto|always|never]` decides per stream; a bare `--color` means `always`. In `auto`
918
+ mode (the default) a non-empty `NO_COLOR` turns color off, then a non-empty `FORCE_COLOR` or
919
+ `CLICOLOR_FORCE` turns it on, then `TERM=dumb` turns it off, and otherwise stdout and stderr
920
+ are colored only when they are terminals. `SLIPWAY_COLOR` or the `color` key set the mode
921
+ without a flag. Two themes exist, `dark` (default) and `light`, selected with `SLIPWAY_THEME`
922
+ or the `theme` key. The palette follows kubecolor's defaults: bold headers, cycling column
923
+ colors, green for `Clean`, yellow for the states that ask for a git action (`Detached` through
924
+ `Behind` in the [STATUS table](#status-words)), red for `Missing`, `NotARepo`, `Unsafe` and
925
+ `Conflicted` and for the `-required-` mark of `explain`, and grey for `Unknown`.
926
+
927
+ ## Shell completion
928
+
929
+ `slipway completion SHELL` prints the script; the header of each script says where it goes.
930
+
931
+ ```sh
932
+ # bash: load it in the current session, or add the line to ~/.bashrc
933
+ eval "$(slipway completion bash)"
934
+ # bash: install it for bash-completion to load on demand
935
+ mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions"
936
+ slipway completion bash > "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/slipway"
937
+
938
+ # zsh: put _slipway in a directory on your fpath (fpath+=~/.zfunc before compinit), or source it directly
939
+ mkdir -p ~/.zfunc
940
+ slipway completion zsh > ~/.zfunc/_slipway
941
+ source <(slipway completion zsh)
942
+
943
+ # fish
944
+ mkdir -p ~/.config/fish/completions
945
+ slipway completion fish > ~/.config/fish/completions/slipway.fish
946
+ ```
947
+
948
+ Completion covers commands, flags, flag values (`-o js<TAB>` gives `json`), resource types, the
949
+ names of your projects and groups and the field paths of `explain`, asked from the program
950
+ itself each time you press TAB.
951
+
952
+ ## Manual pages
953
+
954
+ The pages are installed with slipway. `slipway man` opens `slipway(1)` and `slipway man get`
955
+ opens `slipway-get(1)` with `man(1)`, colored like the help page when color is on; if any of
956
+ `MANPAGER`, `MANROFFOPT`, `LESS_TERMCAP_md` or `GROFF_NO_SGR` is non-empty, your pager
957
+ settings win and slipway passes nothing of its own.
958
+
959
+ `slipway man --install` copies the pages to `${XDG_DATA_HOME:-~/.local/share}/man/man1` (a
960
+ relative `XDG_DATA_HOME` is ignored), and `slipway man --install=DIR` copies them into DIR,
961
+ which must be named `man1`: `man` finds section 1 pages in the `man1` directory under each
962
+ `MANPATH` entry, so any other DIR is refused with exit status 2. DIR is optional, so it must
963
+ follow the `=`; `slipway man --install DIR` is refused as well. When the pages land in
964
+ `~/.local/share/man/man1`, man-db looks there on its own as long as `~/.local/bin` is on
965
+ `PATH`, and the command ends by saying so. Anywhere else, such as under a custom
966
+ `XDG_DATA_HOME`, it ends with the `MANPATH` line for the parent directory, which makes `man`
967
+ find the pages. To read them without installing:
968
+
969
+ ```sh
970
+ export MANPATH="$(dirname "$(slipway man --path)"):$MANPATH"
971
+ man slipway-get
972
+ ```
973
+
974
+ `slipway help COMMAND` and `slipway COMMAND --help` print the same content in the terminal.
975
+
976
+ ## Exit status
977
+
978
+ | Status | Meaning |
979
+ | --- | --- |
980
+ | `0` | Success. |
981
+ | `1` | Runtime error, such as a missing resource or an unreadable manifest. |
982
+ | `2` | Usage error: unknown command, unknown flag or invalid argument. |
983
+ | `130` | Interrupted by SIGINT. |
984
+
985
+ `slipway diff` also exits with 3 when a project differs from its manifest. The man pages of
986
+ `diff`, `sync` and `rollout undo` list their statuses.
987
+
988
+ ## Development
989
+
990
+ ```sh
991
+ git clone https://github.com/hvpaiva/slipway.git
992
+ cd slipway
993
+ bin/setup
994
+ bundle exec rake # tests and RuboCop
995
+ bundle exec rake check # what CI runs
996
+ ```
997
+
998
+ `bin/setup` installs the development dependencies and reports the tools it found.
999
+ `bin/sandbox` opens a shell whose `slipway` is the checkout, against an empty registry and
1000
+ configuration in a temporary directory, to [try a change by
1001
+ hand](CONTRIBUTING.md#trying-a-change-by-hand).
1002
+
1003
+ ## Other documents
1004
+
1005
+ - [CONTRIBUTING.md](CONTRIBUTING.md) for the rake tasks, the conventions, the generated files and the release process.
1006
+ - [ARCHITECTURE.md](ARCHITECTURE.md) for a map of the code.
1007
+ - [CHANGELOG.md](CHANGELOG.md) for what changed in each version.
1008
+ - [SECURITY.md](SECURITY.md) for what slipway promises about the repositories it touches and how to report a vulnerability.
1009
+ - [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for the rules of the community.
1010
+
1011
+ ## License
1012
+
1013
+ MIT. See [LICENSE.txt](LICENSE.txt).