repo-tender 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +53 -5
- data/exe/repo-tender +13 -0
- data/exe/src +23 -0
- data/lib/repo_tender/cli/clone.rb +54 -0
- data/lib/repo_tender/cli/config.rb +0 -1
- data/lib/repo_tender/cli/daemon.rb +24 -9
- data/lib/repo_tender/cli/org.rb +22 -9
- data/lib/repo_tender/cli/repo.rb +0 -1
- data/lib/repo_tender/cli/shell.rb +121 -0
- data/lib/repo_tender/cli/status.rb +0 -1
- data/lib/repo_tender/cli/sync.rb +23 -11
- data/lib/repo_tender/cli.rb +26 -3
- data/lib/repo_tender/cloner.rb +77 -0
- data/lib/repo_tender/config/contract.rb +1 -0
- data/lib/repo_tender/config/model.rb +1 -0
- data/lib/repo_tender/config/store.rb +32 -10
- data/lib/repo_tender/forge/github.rb +2 -0
- data/lib/repo_tender/migration.rb +46 -0
- data/lib/repo_tender/nav.rb +99 -0
- data/lib/repo_tender/paths.rb +3 -3
- data/lib/repo_tender/shell_integration.rb +320 -0
- data/lib/repo_tender/sync/engine.rb +46 -7
- data/lib/repo_tender/sync/report.rb +18 -0
- data/lib/repo_tender/version.rb +1 -1
- data/lib/repo_tender.rb +1 -0
- data/repo-tender.gemspec +9 -4
- metadata +56 -17
- data/bin/repo-tender +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cdef875cd3f2076b9c58047f9b59844846957ac24a5deee9c615e2cd30a8d7e
|
|
4
|
+
data.tar.gz: d810d8e25f1ce4262a63994ca7f3c0c752b876f549b33c870e9557736175ac70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9fe1a7e54338a7806dd7b6c7686a8008a045b28c5ef9afb622548adc0e9e98872c6571752923aa47fb7ce94caf1a529f3f9b72557aefacece14ecd620a48ca54
|
|
7
|
+
data.tar.gz: e62fd711352b672fa0ab5fb7fc7f76184f21952ade51a88fb585ad4a1bb7253e93332baf037991b43454ea6f4b062a49a6450b6352d29e249f301b25ed6f398d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ·
|
|
6
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Restored
|
|
11
|
+
|
|
12
|
+
- **Standalone again.** The evergreen engine was absorbed into
|
|
13
|
+
[space-architect] v8.0.0 under that gem's namespace (`Space::Src`) and
|
|
14
|
+
binary name (`src`); this release restores the `repo-tender` identity —
|
|
15
|
+
same engine, home repo [jetpks/repo-tender] again.
|
|
16
|
+
Lineage: 0.x repo-tender → absorbed as space-src in space-architect 8.0.0 →
|
|
17
|
+
restored standalone 1.0.0.
|
|
18
|
+
|
|
19
|
+
[space-architect]: https://github.com/jetpks/space-architect
|
|
20
|
+
[jetpks/repo-tender]: https://github.com/jetpks/repo-tender
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Binaries:** `repo-tender` is the primary binary again. `src` ships as a
|
|
25
|
+
deprecation shim — it still works, but prints
|
|
26
|
+
`src is deprecated; use repo-tender instead` on stderr on every invocation,
|
|
27
|
+
then forwards argv/stdout/exit codes to the primary binary unchanged.
|
|
28
|
+
- **State/config locations:** `repo-tender` is the app name for
|
|
29
|
+
`$XDG_STATE_HOME` / `$XDG_CONFIG_HOME` dirs and the launchd label is
|
|
30
|
+
`io.github.jetpks.repo-tender.sync` again.
|
|
31
|
+
- **Identity migration inverted:** a one-shot data-preserving migration moves
|
|
32
|
+
`space-src`-named config/state dirs to their `repo-tender` locations and
|
|
33
|
+
flags the stale `io.github.jetpks.space-src.sync` launchd agent. Moves only,
|
|
34
|
+
never deletes; pre-existing `repo-tender` dirs are a clean no-op.
|
|
35
|
+
- **Module namespace:** `Space::Src` → `RepoTender` throughout.
|
|
36
|
+
- **Ruby floor:** `>= 4.0.5` (was `>= 3.3` in the 0.x gem).
|
|
37
|
+
- **Default clone home:** `~/src/evergreen` (was the absorbed identity's
|
|
38
|
+
`~/architect/src`).
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Fish shell integration and completions, installed as
|
|
43
|
+
`~/.config/fish/functions/repo-tender.fish` /
|
|
44
|
+
`~/.config/fish/completions/repo-tender.fish` via
|
|
45
|
+
`repo-tender shell fish install`.
|
data/README.md
CHANGED
|
@@ -74,7 +74,7 @@ git clone git@github.com:jetpks/repo-tender.git
|
|
|
74
74
|
cd repo-tender
|
|
75
75
|
mise install # installs Ruby 4.0.5 per mise.toml
|
|
76
76
|
bundle install
|
|
77
|
-
|
|
77
|
+
exe/repo-tender --help
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
Make sure `gh` is logged in (otherwise org listing drops to an anonymous
|
|
@@ -120,6 +120,17 @@ repo-tender org add socketry # host defaults to github.com
|
|
|
120
120
|
repo-tender org add github.com/socketry # equivalent, explicit host
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
Got a few truly enormous repos in an org you never want cloned? Exclude them
|
|
124
|
+
with `--ignored-repos` (bare `name` or `owner/name`, comma-separated):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
repo-tender org add bigco --ignored-repos monorepo,huge
|
|
128
|
+
# => added: github.com/bigco (… ignored_repos=["monorepo", "huge"])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The ignore list is authoritative at expansion time — an ignored repo never
|
|
132
|
+
enters a sync sweep. ✋
|
|
133
|
+
|
|
123
134
|
### Sync everything now ⚡
|
|
124
135
|
|
|
125
136
|
```bash
|
|
@@ -145,6 +156,23 @@ github.com/ruby/ruby dirty trunk 2026-06-14T20:01:36Z 20
|
|
|
145
156
|
`clean` is the happy path. Anything else is repo-tender telling you a repo needs
|
|
146
157
|
*your* attention — it won't touch it for you. 🔒
|
|
147
158
|
|
|
159
|
+
### Grab an instant copy ⚡🐄
|
|
160
|
+
|
|
161
|
+
Need a working copy of an evergreen repo? `clone` makes a near-instant
|
|
162
|
+
copy-on-write clone (macOS APFS `cp -Rc`) from your local mirror — no network,
|
|
163
|
+
barely any disk:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
repo-tender clone ruby # resolves the single ruby/ruby mirror
|
|
167
|
+
repo-tender clone ruby async --into ~/wip # copy several into a parent dir
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Name a repo bare (`ruby`), `owner/name`, or `host/owner/name` — bare names that
|
|
171
|
+
match more than one mirror error out and list the candidates so you can qualify.
|
|
172
|
+
`--into` is the destination **parent** (default `.`); each repo lands at
|
|
173
|
+
`<into>/<name>`. And it **never clobbers** — an existing destination is left
|
|
174
|
+
byte-for-byte alone. 🔒
|
|
175
|
+
|
|
148
176
|
### Schedule it & forget it 🤖
|
|
149
177
|
|
|
150
178
|
Install a per-user launchd agent that runs `sync` every `refresh_interval`:
|
|
@@ -189,11 +217,13 @@ repo-tender status --json
|
|
|
189
217
|
|
|
190
218
|
**Track repos & orgs:**
|
|
191
219
|
- `repo add|remove|list REF` — manage individual repos (`host/owner/name`)
|
|
192
|
-
- `org add|remove|list NAME` — manage whole orgs (`name` or `host/name`)
|
|
220
|
+
- `org add|remove|list NAME` — manage whole orgs (`name` or `host/name`);
|
|
221
|
+
`org add` takes `--include-archived`, `--include-forks`, `--ignored-repos a,b`
|
|
193
222
|
|
|
194
223
|
**Run & inspect:**
|
|
195
224
|
- `sync [--repo REF]` — run one sync pass (optionally scoped to one repo)
|
|
196
225
|
- `status` — print the per-repo evergreen status table (reads state, no network)
|
|
226
|
+
- `clone NAME... [--into DIR]` — instant COW copy of mirror(s) into a parent dir
|
|
197
227
|
- `config path|show` — show the config path, or the effective config
|
|
198
228
|
|
|
199
229
|
**Schedule (launchd):**
|
|
@@ -225,11 +255,29 @@ The bits worth knowing the *why* of:
|
|
|
225
255
|
org expansions). Splitting them keeps machine rewrites away from the file you
|
|
226
256
|
actually wrote.
|
|
227
257
|
|
|
258
|
+
## The `src` shim & lineage 🧬
|
|
259
|
+
|
|
260
|
+
repo-tender's story has a detour in it: the engine was absorbed into
|
|
261
|
+
[space-architect] v8.0.0 under that gem's namespace and binary name (`src`,
|
|
262
|
+
state under `~/.local/state/space-src`), and has now been restored standalone
|
|
263
|
+
here as repo-tender 1.0.0 — same engine, own identity again.
|
|
264
|
+
|
|
265
|
+
[space-architect]: https://github.com/jetpks/space-architect
|
|
266
|
+
|
|
267
|
+
Two things happen for anyone arriving from the absorbed identity:
|
|
268
|
+
|
|
269
|
+
- **`src` still works** — it's shipped as a deprecation shim. Every invocation
|
|
270
|
+
prints `src is deprecated; use repo-tender instead` on stderr, then forwards
|
|
271
|
+
argv, stdout, and exit codes to the primary binary unchanged.
|
|
272
|
+
- **State migrates automatically** — on first run of any `repo-tender`
|
|
273
|
+
command, a one-shot data-preserving migration moves your `space-src` config
|
|
274
|
+
and state dirs to their `repo-tender` locations (a no-clobber move; existing
|
|
275
|
+
`repo-tender` dirs are never touched) and flags a stale launchd agent with
|
|
276
|
+
the `repo-tender daemon install` instruction.
|
|
277
|
+
|
|
228
278
|
## Documentation 📖
|
|
229
279
|
|
|
230
|
-
- **[
|
|
231
|
-
status value, file location, and exit code
|
|
232
|
-
- **[Design (PRD)](docs/prd/repo-tender.md)** 🏗️ — the full design & decisions
|
|
280
|
+
- **[Changelog](CHANGELOG.md)** 📜 — what's new (1.0.0: standalone again)
|
|
233
281
|
- **[Builder context](AGENTS.md)** 🤝 — toolchain & conventions
|
|
234
282
|
|
|
235
283
|
## Development 🧪
|
data/exe/repo-tender
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
source_gemfile = File.expand_path("../Gemfile", __dir__)
|
|
5
|
+
if File.exist?(source_gemfile)
|
|
6
|
+
ENV["BUNDLE_GEMFILE"] ||= source_gemfile
|
|
7
|
+
require "bundler/setup"
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require "repo_tender"
|
|
12
|
+
|
|
13
|
+
RepoTender::CLI.run(ARGV, $stdout, $stderr)
|
data/exe/src
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Deprecation shim for the `src` binary name (the engine lived briefly under
|
|
5
|
+
# the absorbed identity). Warns on EVERY invocation, then forwards
|
|
6
|
+
# argv/stdout/exit codes to the primary binary unchanged — including the
|
|
7
|
+
# usage output, which renders $PROGRAM_NAME's basename (so the shim presents
|
|
8
|
+
# the primary binary's name for byte-identical --help parity).
|
|
9
|
+
|
|
10
|
+
warn "src is deprecated; use repo-tender instead"
|
|
11
|
+
|
|
12
|
+
source_gemfile = File.expand_path("../Gemfile", __dir__)
|
|
13
|
+
if File.exist?(source_gemfile)
|
|
14
|
+
ENV["BUNDLE_GEMFILE"] ||= source_gemfile
|
|
15
|
+
require "bundler/setup"
|
|
16
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
$PROGRAM_NAME = File.join(File.expand_path(__dir__), "repo-tender")
|
|
20
|
+
|
|
21
|
+
require "repo_tender"
|
|
22
|
+
|
|
23
|
+
RepoTender::CLI.run(ARGV, $stdout, $stderr)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
require "dry/monads"
|
|
5
|
+
require "repo_tender/cli/options"
|
|
6
|
+
require "repo_tender/cloner"
|
|
7
|
+
|
|
8
|
+
module RepoTender
|
|
9
|
+
module CLI
|
|
10
|
+
# `clone` command: APFS COW copy of an evergreen repo into a working dir.
|
|
11
|
+
# Resolves each NAME against config.base_dir and copies via cp -Rc.
|
|
12
|
+
# Multiple names are processed independently; a per-name failure is
|
|
13
|
+
# reported on err and does not abort the others. Exit code is 1 if any
|
|
14
|
+
# name failed, else 0.
|
|
15
|
+
class Clone < Dry::CLI::Command
|
|
16
|
+
include GlobalOptions
|
|
17
|
+
|
|
18
|
+
desc "Clone evergreen repo(s) into a working directory (APFS COW copy)"
|
|
19
|
+
argument :names, type: :array, required: true,
|
|
20
|
+
desc: "Repo name(s): bare name, owner/name, or host/owner/name"
|
|
21
|
+
option :into, default: ".",
|
|
22
|
+
desc: "Destination parent directory (default: current working directory)"
|
|
23
|
+
|
|
24
|
+
def call(names:, into: ".", plain: nil, json: nil, no_color: nil, quiet: nil, **)
|
|
25
|
+
mode = UI::Mode.resolve(
|
|
26
|
+
flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
|
|
27
|
+
env: CLI.env,
|
|
28
|
+
out: out
|
|
29
|
+
)
|
|
30
|
+
pastel = Pastel.new(enabled: mode.color)
|
|
31
|
+
|
|
32
|
+
paths = CLI.make_paths
|
|
33
|
+
config = Config::Store.load(paths.config_file).success
|
|
34
|
+
cloner = Cloner.new(base_dir: config.base_dir)
|
|
35
|
+
|
|
36
|
+
any_failure = false
|
|
37
|
+
names.each do |name|
|
|
38
|
+
result = cloner.call(name: name, into: into)
|
|
39
|
+
if result.success?
|
|
40
|
+
dest = result.success
|
|
41
|
+
out.puts pastel.green("cloned: #{name} → #{dest}")
|
|
42
|
+
else
|
|
43
|
+
err.puts result.failure
|
|
44
|
+
any_failure = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
CLI.record_outcome(Outcome.new(exit_code: any_failure ? 1 : 0))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
RepoTender::CLI::Registry.register "clone", RepoTender::CLI::Clone
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require "pastel"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "dry/monads"
|
|
6
|
-
require "repo_tender/cli"
|
|
7
6
|
require "repo_tender/ui/mode"
|
|
8
7
|
require "repo_tender/cli/options"
|
|
9
8
|
require "repo_tender/launchd/agent"
|
|
10
9
|
require "repo_tender/launchd/plist"
|
|
10
|
+
require "repo_tender/migration"
|
|
11
11
|
|
|
12
12
|
module RepoTender
|
|
13
13
|
module CLI
|
|
@@ -57,7 +57,7 @@ module RepoTender
|
|
|
57
57
|
# toolchain-resolved ruby; pinned via mise.toml).
|
|
58
58
|
# * `bin_path` — `RbConfig.ruby` + the script path
|
|
59
59
|
# (we use `__dir__` of this file's caller; for the
|
|
60
|
-
# gem install, this is `<gem>/
|
|
60
|
+
# gem install, this is `<gem>/exe/repo-tender`).
|
|
61
61
|
#
|
|
62
62
|
# In tests, we inject these via the `Resolve` object
|
|
63
63
|
# (see below) — never call out to the shell.
|
|
@@ -113,6 +113,15 @@ module RepoTender
|
|
|
113
113
|
label = Launchd::Agent::DEFAULT_LABEL
|
|
114
114
|
pp = plist_path(paths, label)
|
|
115
115
|
|
|
116
|
+
# Relabel: if old-identity plist exists, bootout (benign-failure-tolerant)
|
|
117
|
+
# and remove it before bootstrapping the new-label plist.
|
|
118
|
+
old_pp = plist_path(paths, Migration::OLD_LABEL)
|
|
119
|
+
if File.exist?(old_pp)
|
|
120
|
+
old_agent = make_agent(label: Migration::OLD_LABEL)
|
|
121
|
+
old_agent.uninstall
|
|
122
|
+
File.delete(old_pp) if File.exist?(old_pp)
|
|
123
|
+
end
|
|
124
|
+
|
|
116
125
|
resolve = Resolve.detect(repo_root: Dir.pwd)
|
|
117
126
|
xml = build_plist(resolve: resolve, config: config, paths: paths, label: label)
|
|
118
127
|
FileUtils.mkdir_p(File.dirname(pp))
|
|
@@ -264,6 +273,12 @@ module RepoTender
|
|
|
264
273
|
)
|
|
265
274
|
pastel = Pastel.new(enabled: mode.color)
|
|
266
275
|
|
|
276
|
+
paths = CLI.make_paths
|
|
277
|
+
old_pp = plist_path(paths, Migration::OLD_LABEL)
|
|
278
|
+
if File.exist?(old_pp)
|
|
279
|
+
err.puts pastel.yellow("warning: stale agent #{Migration::OLD_LABEL} detected; run `repo-tender daemon install` to upgrade")
|
|
280
|
+
end
|
|
281
|
+
|
|
267
282
|
label = Launchd::Agent::DEFAULT_LABEL
|
|
268
283
|
agent = make_agent
|
|
269
284
|
result = agent.status
|
|
@@ -283,11 +298,11 @@ module RepoTender
|
|
|
283
298
|
end
|
|
284
299
|
end
|
|
285
300
|
|
|
286
|
-
# Detect the runtime paths the plist needs. The repo-tender
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
301
|
+
# Detect the runtime paths the plist needs. The repo-tender install path
|
|
302
|
+
# matters because the plist stores an absolute `bin_path` — that
|
|
303
|
+
# is the script launchd invokes. We resolve `bin_path` from the
|
|
304
|
+
# on-disk gem layout if we can, else fall back to the directory
|
|
305
|
+
# the daemon command was run from.
|
|
291
306
|
class RepoTender::CLI::Daemon::Helpers::Resolve
|
|
292
307
|
# @param repo_root [String] absolute path of the working directory (where mise.toml is expected)
|
|
293
308
|
# @return [Resolve]
|
|
@@ -323,10 +338,10 @@ class RepoTender::CLI::Daemon::Helpers::Resolve
|
|
|
323
338
|
def self.detect_bin_path(repo_root)
|
|
324
339
|
path = ENV["REPO_TENDER_BIN_PATH"]
|
|
325
340
|
return path if path && !path.empty?
|
|
326
|
-
# Prefer the on-disk dev bin at `<repo_root>/
|
|
341
|
+
# Prefer the on-disk dev bin at `<repo_root>/exe/repo-tender`
|
|
327
342
|
# — it's what the human runs during testing, and the gem is
|
|
328
343
|
# typically not `gem install`ed in a source checkout.
|
|
329
|
-
dev = File.join(repo_root, "
|
|
344
|
+
dev = File.join(repo_root, "exe", "repo-tender")
|
|
330
345
|
return dev if File.exist?(dev)
|
|
331
346
|
# Next, an installed binary on PATH.
|
|
332
347
|
require "open3"
|
data/lib/repo_tender/cli/org.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "pastel"
|
|
4
4
|
require "dry/monads"
|
|
5
|
-
require "repo_tender/cli"
|
|
6
5
|
require "repo_tender/ui/mode"
|
|
7
6
|
require "repo_tender/cli/options"
|
|
8
7
|
|
|
@@ -20,7 +19,7 @@ module RepoTender
|
|
|
20
19
|
# Parse an org ref string. Accepts "github.com/socketry" or
|
|
21
20
|
# bare "socketry" (defaults host to github.com). Anything
|
|
22
21
|
# with 3+ parts is rejected (org has only host + name).
|
|
23
|
-
def parse_ref(name, include_archived: false, include_forks: false)
|
|
22
|
+
def parse_ref(name, include_archived: false, include_forks: false, ignored_repos: [])
|
|
24
23
|
parts = name.to_s.split("/")
|
|
25
24
|
case parts.length
|
|
26
25
|
when 1
|
|
@@ -30,7 +29,8 @@ module RepoTender
|
|
|
30
29
|
host: Config::DEFAULT_HOST,
|
|
31
30
|
name: org,
|
|
32
31
|
include_archived: include_archived,
|
|
33
|
-
include_forks: include_forks
|
|
32
|
+
include_forks: include_forks,
|
|
33
|
+
ignored_repos: ignored_repos
|
|
34
34
|
))
|
|
35
35
|
when 2
|
|
36
36
|
host, n = parts
|
|
@@ -40,7 +40,8 @@ module RepoTender
|
|
|
40
40
|
host: host,
|
|
41
41
|
name: n,
|
|
42
42
|
include_archived: include_archived,
|
|
43
|
-
include_forks: include_forks
|
|
43
|
+
include_forks: include_forks,
|
|
44
|
+
ignored_repos: ignored_repos
|
|
44
45
|
))
|
|
45
46
|
else
|
|
46
47
|
Dry::Monads::Failure("invalid org reference: #{name.inspect} (expected \"<name>\" or \"<host>/<name>\")")
|
|
@@ -51,6 +52,11 @@ module RepoTender
|
|
|
51
52
|
def format_ref(o) = "#{o.host}/#{o.name}"
|
|
52
53
|
def format_failure(f) = f.is_a?(Hash) ? f.inspect : f.to_s
|
|
53
54
|
|
|
55
|
+
def format_ignored(o)
|
|
56
|
+
return "" if o.ignored_repos.empty?
|
|
57
|
+
" ignored_repos=#{o.ignored_repos.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
54
60
|
def fail_with(cmd, msg)
|
|
55
61
|
cmd.send(:err).puts msg
|
|
56
62
|
RepoTender::CLI.record_outcome(Outcome.new(exit_code: 1, message: msg))
|
|
@@ -68,8 +74,10 @@ module RepoTender
|
|
|
68
74
|
desc: "Include archived repos when expanding the org"
|
|
69
75
|
option :include_forks, type: :boolean, default: false,
|
|
70
76
|
desc: "Include forks when expanding the org"
|
|
77
|
+
option :ignored_repos, type: :array, default: [],
|
|
78
|
+
desc: "Repos to exclude from expansion (bare name or owner/name)"
|
|
71
79
|
|
|
72
|
-
def call(name:, include_archived: false, include_forks: false, plain: nil, json: nil, no_color: nil, quiet: nil, **)
|
|
80
|
+
def call(name:, include_archived: false, include_forks: false, ignored_repos: [], plain: nil, json: nil, no_color: nil, quiet: nil, **)
|
|
73
81
|
mode = UI::Mode.resolve(
|
|
74
82
|
flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
|
|
75
83
|
env: CLI.env,
|
|
@@ -79,7 +87,8 @@ module RepoTender
|
|
|
79
87
|
|
|
80
88
|
parsed = parse_ref(name,
|
|
81
89
|
include_archived: include_archived,
|
|
82
|
-
include_forks: include_forks
|
|
90
|
+
include_forks: include_forks,
|
|
91
|
+
ignored_repos: ignored_repos)
|
|
83
92
|
return fail_with(self, parsed.failure) if parsed.failure?
|
|
84
93
|
|
|
85
94
|
new_ref = parsed.success
|
|
@@ -88,7 +97,8 @@ module RepoTender
|
|
|
88
97
|
|
|
89
98
|
if config.orgs.any? { |o| same_org?(o, new_ref) }
|
|
90
99
|
out.puts pastel.yellow("already tracked: #{format_ref(new_ref)}" \
|
|
91
|
-
" (include_archived=#{new_ref.include_archived}, include_forks=#{new_ref.include_forks})"
|
|
100
|
+
" (include_archived=#{new_ref.include_archived}, include_forks=#{new_ref.include_forks})" \
|
|
101
|
+
"#{format_ignored(new_ref)}")
|
|
92
102
|
return CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
93
103
|
end
|
|
94
104
|
|
|
@@ -100,7 +110,8 @@ module RepoTender
|
|
|
100
110
|
end
|
|
101
111
|
|
|
102
112
|
out.puts pastel.green("added: #{format_ref(new_ref)}" \
|
|
103
|
-
" (include_archived=#{new_ref.include_archived}, include_forks=#{new_ref.include_forks})"
|
|
113
|
+
" (include_archived=#{new_ref.include_archived}, include_forks=#{new_ref.include_forks})" \
|
|
114
|
+
"#{format_ignored(new_ref)}")
|
|
104
115
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
105
116
|
end
|
|
106
117
|
end
|
|
@@ -150,6 +161,7 @@ module RepoTender
|
|
|
150
161
|
end
|
|
151
162
|
|
|
152
163
|
class List < Dry::CLI::Command
|
|
164
|
+
include Helpers
|
|
153
165
|
include GlobalOptions
|
|
154
166
|
|
|
155
167
|
desc "List tracked orgs"
|
|
@@ -169,7 +181,8 @@ module RepoTender
|
|
|
169
181
|
else
|
|
170
182
|
config.orgs.each do |o|
|
|
171
183
|
out.puts pastel.cyan("#{o.host}/#{o.name}" \
|
|
172
|
-
" (include_archived=#{o.include_archived}, include_forks=#{o.include_forks})"
|
|
184
|
+
" (include_archived=#{o.include_archived}, include_forks=#{o.include_forks})" \
|
|
185
|
+
"#{format_ignored(o)}")
|
|
173
186
|
end
|
|
174
187
|
end
|
|
175
188
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
data/lib/repo_tender/cli/repo.rb
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "repo_tender/shell_integration"
|
|
4
|
+
|
|
5
|
+
module RepoTender
|
|
6
|
+
module CLI
|
|
7
|
+
module Shell
|
|
8
|
+
class Init < Dry::CLI::Command
|
|
9
|
+
desc "Print shell integration script"
|
|
10
|
+
argument :shell_name, required: true, desc: "Shell name (e.g. fish)"
|
|
11
|
+
|
|
12
|
+
def call(shell_name:, **)
|
|
13
|
+
out.puts ShellIntegration.for(shell_name)
|
|
14
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
15
|
+
rescue => e
|
|
16
|
+
err.puts "repo-tender shell init: #{e.message}"
|
|
17
|
+
CLI.record_outcome(Outcome.new(exit_code: 1))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Fish < Dry::CLI::Command
|
|
22
|
+
desc "Manage fish shell integration: install, uninstall, path"
|
|
23
|
+
argument :subcommand, required: false, desc: "install, uninstall, or path (default: install)"
|
|
24
|
+
option :force, type: :boolean, default: false, desc: "Overwrite or remove existing shell files"
|
|
25
|
+
|
|
26
|
+
def call(subcommand: "install", force: false, **)
|
|
27
|
+
case subcommand
|
|
28
|
+
when "install"
|
|
29
|
+
result = ShellIntegration.install("fish", env: CLI.env, force: force)
|
|
30
|
+
out.puts fish_install_message(result.fetch(:action), result.fetch(:path))
|
|
31
|
+
out.puts fish_completions_install_message(result.fetch(:completions_action), result.fetch(:completions_path))
|
|
32
|
+
out.puts "Restart fish to load the integration in this terminal: exec fish"
|
|
33
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
34
|
+
when "uninstall"
|
|
35
|
+
result = ShellIntegration.uninstall("fish", env: CLI.env, force: force)
|
|
36
|
+
out.puts fish_uninstall_message(result.fetch(:action), result.fetch(:path))
|
|
37
|
+
out.puts fish_completions_uninstall_message(result.fetch(:completions_action), result.fetch(:completions_path))
|
|
38
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
39
|
+
when "path"
|
|
40
|
+
out.puts "Function: #{ShellIntegration.path_for("fish", env: CLI.env)}"
|
|
41
|
+
out.puts "Completions: #{ShellIntegration.completions_path_for("fish", env: CLI.env)}"
|
|
42
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
43
|
+
else
|
|
44
|
+
err.puts "Usage: repo-tender shell fish [install|uninstall|path]"
|
|
45
|
+
CLI.record_outcome(Outcome.new(exit_code: 1))
|
|
46
|
+
end
|
|
47
|
+
rescue => e
|
|
48
|
+
err.puts "repo-tender shell fish: #{e.message}"
|
|
49
|
+
CLI.record_outcome(Outcome.new(exit_code: 1))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def fish_install_message(action, path)
|
|
55
|
+
case action
|
|
56
|
+
when :unchanged then "Fish integration already installed: #{path}"
|
|
57
|
+
when :updated then "Updated fish integration: #{path}"
|
|
58
|
+
else "Installed fish integration: #{path}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fish_uninstall_message(action, path)
|
|
63
|
+
case action
|
|
64
|
+
when :missing then "Fish integration was not installed: #{path}"
|
|
65
|
+
else "Removed fish integration: #{path}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def fish_completions_install_message(action, path)
|
|
70
|
+
case action
|
|
71
|
+
when :unchanged then "Fish completions already installed: #{path}"
|
|
72
|
+
when :updated then "Updated fish completions: #{path}"
|
|
73
|
+
else "Installed fish completions: #{path}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fish_completions_uninstall_message(action, path)
|
|
78
|
+
case action
|
|
79
|
+
when :missing then "Fish completions were not installed: #{path}"
|
|
80
|
+
else "Removed fish completions: #{path}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Complete < Dry::CLI::Command
|
|
86
|
+
desc "Print completion candidates"
|
|
87
|
+
argument :kind, required: true, desc: "Completion kind"
|
|
88
|
+
argument :extra, type: :array, required: false, desc: "Extra args for completion"
|
|
89
|
+
|
|
90
|
+
def call(kind:, extra: [], **)
|
|
91
|
+
case kind
|
|
92
|
+
when "checkouts"
|
|
93
|
+
begin
|
|
94
|
+
paths = CLI.make_paths
|
|
95
|
+
config = Config::Store.load(paths.config_file).success
|
|
96
|
+
Nav.scan(config.base_dir).each { |e| out.puts e[:target] }
|
|
97
|
+
rescue
|
|
98
|
+
# Never raise on a completion call — missing/unparseable config is fine.
|
|
99
|
+
end
|
|
100
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
101
|
+
when "shells"
|
|
102
|
+
out.puts "fish"
|
|
103
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
104
|
+
else
|
|
105
|
+
err.puts "Usage: repo-tender shell complete checkouts|shells"
|
|
106
|
+
CLI.record_outcome(Outcome.new(exit_code: 1))
|
|
107
|
+
end
|
|
108
|
+
rescue => e
|
|
109
|
+
err.puts "repo-tender shell complete: #{e.message}"
|
|
110
|
+
CLI.record_outcome(Outcome.new(exit_code: 1))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
RepoTender::CLI::Registry.register "shell" do |prefix|
|
|
118
|
+
prefix.register "init", RepoTender::CLI::Shell::Init
|
|
119
|
+
prefix.register "fish", RepoTender::CLI::Shell::Fish
|
|
120
|
+
prefix.register "complete", RepoTender::CLI::Shell::Complete
|
|
121
|
+
end
|
data/lib/repo_tender/cli/sync.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/monads"
|
|
4
|
-
require "repo_tender/cli"
|
|
5
4
|
require "repo_tender/cli/repo" # for Repo::Helpers.parse_ref
|
|
6
5
|
require "repo_tender/cli/options"
|
|
7
6
|
require "repo_tender/ui/mode"
|
|
@@ -44,6 +43,19 @@ module RepoTender
|
|
|
44
43
|
# the next spawn. No-op when the log is missing or
|
|
45
44
|
# under-threshold (sync tests in G4 stay green).
|
|
46
45
|
rotate_plist_logs(paths)
|
|
46
|
+
|
|
47
|
+
# Resolved BEFORE the scoping block: the "scoping sync to:"
|
|
48
|
+
# notice below is prose, and must be suppressed in JSON mode
|
|
49
|
+
# for the same reason the summary line is. Resolution depends
|
|
50
|
+
# only on flags/env/out, so it is safe this early.
|
|
51
|
+
mode = UI::Mode.resolve(
|
|
52
|
+
flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
|
|
53
|
+
env: CLI.env,
|
|
54
|
+
out: out
|
|
55
|
+
)
|
|
56
|
+
# Every line of --json output must parse as one JSON object.
|
|
57
|
+
human = mode.format != :json
|
|
58
|
+
|
|
47
59
|
if repo
|
|
48
60
|
target = scope_target(repo)
|
|
49
61
|
return fail_with(self, "invalid repo reference: #{repo.inspect} (expected host/owner/name)") if target.failure?
|
|
@@ -58,14 +70,9 @@ module RepoTender
|
|
|
58
70
|
# exactly the G4 "other repo gets no state row" test
|
|
59
71
|
# path, so we explicitly empty orgs here).
|
|
60
72
|
config = Config::Store.with(config, repos: [found], orgs: [])
|
|
61
|
-
out.puts "scoping sync to: #{Repo::Helpers.format_ref(found)}"
|
|
73
|
+
out.puts "scoping sync to: #{Repo::Helpers.format_ref(found)}" if human
|
|
62
74
|
end
|
|
63
75
|
|
|
64
|
-
mode = UI::Mode.resolve(
|
|
65
|
-
flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
|
|
66
|
-
env: CLI.env,
|
|
67
|
-
out: out
|
|
68
|
-
)
|
|
69
76
|
reporter = if mode.format == :json
|
|
70
77
|
UI::JsonReporter.new(out)
|
|
71
78
|
elsif mode.animate
|
|
@@ -74,14 +81,19 @@ module RepoTender
|
|
|
74
81
|
UI::PlainReporter.new(out, mode: mode)
|
|
75
82
|
end
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
# prune only on a full sweep. With --repo the config above was
|
|
85
|
+
# narrowed to one repo, so pruning would delete the state rows
|
|
86
|
+
# for every OTHER tracked repo.
|
|
87
|
+
result = RepoTender::Sync::Engine.new(reporter: reporter)
|
|
88
|
+
.call(config: config, paths: paths, prune: repo.nil?)
|
|
78
89
|
if result.failure?
|
|
79
90
|
return fail_with(self, "sync failed: #{format_failure(result.failure)}")
|
|
80
91
|
end
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
report = result.success
|
|
94
|
+
# JsonReporter already emits a run_finished event carrying the
|
|
95
|
+
# summary, so suppressing the prose line loses nothing.
|
|
96
|
+
out.puts "synced #{report.processed} repo(s)" if human
|
|
85
97
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
86
98
|
end
|
|
87
99
|
|