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.
- checksums.yaml +7 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +1013 -0
- data/exe/slipway +10 -0
- data/lib/slipway/cli/builtins.rb +241 -0
- data/lib/slipway/cli/completer.rb +158 -0
- data/lib/slipway/cli/completion_scripts.rb +163 -0
- data/lib/slipway/cli/context.rb +67 -0
- data/lib/slipway/cli/errors.rb +19 -0
- data/lib/slipway/cli/globals.rb +27 -0
- data/lib/slipway/cli/help_renderer.rb +135 -0
- data/lib/slipway/cli/manpage.rb +226 -0
- data/lib/slipway/cli/parser.rb +45 -0
- data/lib/slipway/cli/registry.rb +191 -0
- data/lib/slipway/cli/runner.rb +186 -0
- data/lib/slipway/cli/style.rb +82 -0
- data/lib/slipway/cli/theme.rb +85 -0
- data/lib/slipway/cli/validator.rb +61 -0
- data/lib/slipway/cli.rb +22 -0
- data/lib/slipway/command_line.rb +22 -0
- data/lib/slipway/commands/api_resources.rb +82 -0
- data/lib/slipway/commands/apply.rb +172 -0
- data/lib/slipway/commands/base.rb +50 -0
- data/lib/slipway/commands/config.rb +73 -0
- data/lib/slipway/commands/create.rb +218 -0
- data/lib/slipway/commands/delete.rb +82 -0
- data/lib/slipway/commands/describe.rb +74 -0
- data/lib/slipway/commands/diff.rb +122 -0
- data/lib/slipway/commands/edit.rb +130 -0
- data/lib/slipway/commands/explain.rb +97 -0
- data/lib/slipway/commands/fetch.rb +112 -0
- data/lib/slipway/commands/from_dir.rb +141 -0
- data/lib/slipway/commands/get.rb +167 -0
- data/lib/slipway/commands/label.rb +114 -0
- data/lib/slipway/commands/manual.rb +67 -0
- data/lib/slipway/commands/options.rb +73 -0
- data/lib/slipway/commands/results.rb +57 -0
- data/lib/slipway/commands/rollout.rb +114 -0
- data/lib/slipway/commands/rollout_spec.rb +99 -0
- data/lib/slipway/commands/rollout_undo.rb +126 -0
- data/lib/slipway/commands/scope.rb +156 -0
- data/lib/slipway/commands/sync.rb +140 -0
- data/lib/slipway/commands.rb +54 -0
- data/lib/slipway/drift.rb +87 -0
- data/lib/slipway/editor.rb +71 -0
- data/lib/slipway/error.rb +27 -0
- data/lib/slipway/fetcher.rb +99 -0
- data/lib/slipway/field_selector.rb +86 -0
- data/lib/slipway/git/branch_name.rb +32 -0
- data/lib/slipway/git/commit.rb +13 -0
- data/lib/slipway/git/distance.rb +13 -0
- data/lib/slipway/git/errors.rb +125 -0
- data/lib/slipway/git/fake.rb +147 -0
- data/lib/slipway/git/fast_forward.rb +12 -0
- data/lib/slipway/git/fast_forwarding.rb +148 -0
- data/lib/slipway/git/fetch_result.rb +22 -0
- data/lib/slipway/git/move_back.rb +12 -0
- data/lib/slipway/git/reflog.rb +25 -0
- data/lib/slipway/git/repository.rb +288 -0
- data/lib/slipway/git/rolling_back.rb +98 -0
- data/lib/slipway/git/runner.rb +175 -0
- data/lib/slipway/git/status.rb +110 -0
- data/lib/slipway/git/url.rb +95 -0
- data/lib/slipway/git.rb +20 -0
- data/lib/slipway/inspector.rb +103 -0
- data/lib/slipway/labels.rb +126 -0
- data/lib/slipway/manifest.rb +265 -0
- data/lib/slipway/names.rb +22 -0
- data/lib/slipway/outcome.rb +45 -0
- data/lib/slipway/output/age.rb +70 -0
- data/lib/slipway/output/describe.rb +71 -0
- data/lib/slipway/output/explain.rb +75 -0
- data/lib/slipway/output/serializer.rb +35 -0
- data/lib/slipway/output/table.rb +67 -0
- data/lib/slipway/output.rb +28 -0
- data/lib/slipway/paths.rb +65 -0
- data/lib/slipway/plan.rb +227 -0
- data/lib/slipway/pool.rb +94 -0
- data/lib/slipway/resources.rb +91 -0
- data/lib/slipway/rollback.rb +236 -0
- data/lib/slipway/rollout_history.rb +69 -0
- data/lib/slipway/runtime.rb +65 -0
- data/lib/slipway/scanner.rb +54 -0
- data/lib/slipway/schema.rb +128 -0
- data/lib/slipway/selector.rb +146 -0
- data/lib/slipway/settings.rb +174 -0
- data/lib/slipway/state.rb +82 -0
- data/lib/slipway/store.rb +170 -0
- data/lib/slipway/syncer.rb +139 -0
- data/lib/slipway/version.rb +5 -0
- data/lib/slipway/views/group.rb +35 -0
- data/lib/slipway/views/project.rb +148 -0
- data/lib/slipway/views.rb +10 -0
- data/lib/slipway/yaml.rb +14 -0
- data/lib/slipway.rb +32 -0
- data/man/man1/slipway-api-resources.1 +53 -0
- data/man/man1/slipway-apply.1 +45 -0
- data/man/man1/slipway-completion.1 +29 -0
- data/man/man1/slipway-config-path.1 +20 -0
- data/man/man1/slipway-config-view.1 +25 -0
- data/man/man1/slipway-config.1 +22 -0
- data/man/man1/slipway-create.1 +89 -0
- data/man/man1/slipway-delete.1 +46 -0
- data/man/man1/slipway-describe.1 +95 -0
- data/man/man1/slipway-diff.1 +120 -0
- data/man/man1/slipway-edit.1 +34 -0
- data/man/man1/slipway-explain.1 +36 -0
- data/man/man1/slipway-fetch.1 +77 -0
- data/man/man1/slipway-get.1 +155 -0
- data/man/man1/slipway-help.1 +19 -0
- data/man/man1/slipway-label.1 +53 -0
- data/man/man1/slipway-man.1 +36 -0
- data/man/man1/slipway-rollout-history.1 +28 -0
- data/man/man1/slipway-rollout-pause.1 +21 -0
- data/man/man1/slipway-rollout-resume.1 +21 -0
- data/man/man1/slipway-rollout-undo.1 +73 -0
- data/man/man1/slipway-rollout-unpin.1 +21 -0
- data/man/man1/slipway-rollout.1 +36 -0
- data/man/man1/slipway-sync.1 +90 -0
- data/man/man1/slipway-version.1 +17 -0
- data/man/man1/slipway.1 +243 -0
- metadata +173 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'command_line'
|
|
4
|
+
require_relative 'drift'
|
|
5
|
+
require_relative 'git'
|
|
6
|
+
require_relative 'outcome'
|
|
7
|
+
require_relative 'plan'
|
|
8
|
+
require_relative 'rollout_history'
|
|
9
|
+
|
|
10
|
+
module Slipway
|
|
11
|
+
# Brings projects to their manifests by the one move that cannot lose work: each is fetched,
|
|
12
|
+
# planned by Plan.for, and its checked-out branch fast-forwarded when the plan says so. It
|
|
13
|
+
# observes and plans on the workers and moves branches on the calling thread, one project at a
|
|
14
|
+
# time, so no two of its writes run at once.
|
|
15
|
+
class Syncer
|
|
16
|
+
# The plan for a project whose fetch went through, made from what git answered after it.
|
|
17
|
+
Observed = Data.define(:project, :inspection, :fetched, :plan)
|
|
18
|
+
|
|
19
|
+
FAST_FORWARDED = 'fast-forwarded'
|
|
20
|
+
REACHED = [Outcome::FETCHED, Outcome::UNCHANGED].freeze
|
|
21
|
+
ABBREV = Git::Porcelain::ABBREVIATION
|
|
22
|
+
TO_PIN = '(to the pinned revision)'
|
|
23
|
+
# Git leaves the branch where it was on each refusal. A lock may belong to a git that is
|
|
24
|
+
# still running, so none is ever removed.
|
|
25
|
+
ADVICE = { 'Busy' => 'sync never removes a lock', 'WouldOverwrite' => 'move them and run sync again',
|
|
26
|
+
'WouldLoseChanges' => 'commit or move them and run sync again',
|
|
27
|
+
'NotFastForward' => 'sync never merges or rebases' }.freeze
|
|
28
|
+
|
|
29
|
+
# Dry-run projects that no fetch has reached, counted as they settle.
|
|
30
|
+
attr_reader :unfetched
|
|
31
|
+
|
|
32
|
+
# `group` is the one a command without -n selects, so the undo command can leave it out; nil
|
|
33
|
+
# when -n was typed, so every undo command names its group.
|
|
34
|
+
def initialize(runtime, fetcher, dry_run:, group:)
|
|
35
|
+
@runtime = runtime
|
|
36
|
+
@fetcher = fetcher
|
|
37
|
+
@dry_run = dry_run
|
|
38
|
+
@group = group
|
|
39
|
+
@unfetched = 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Runs on a worker thread: an Outcome when nothing is left to do, else an Observed.
|
|
43
|
+
def observe(project)
|
|
44
|
+
return Outcome.new(project:, word: Outcome::PAUSED) if project.paused
|
|
45
|
+
|
|
46
|
+
inspection = @runtime.inspector.examine(project)
|
|
47
|
+
return unreadable(inspection) if inspection.error
|
|
48
|
+
|
|
49
|
+
fetched = @fetcher.fetch(project, inspection)
|
|
50
|
+
return fetched unless REACHED.include?(fetched.word)
|
|
51
|
+
|
|
52
|
+
inspection = @runtime.inspector.examine(project) unless @dry_run
|
|
53
|
+
return unreadable(inspection) if inspection.error
|
|
54
|
+
|
|
55
|
+
Observed.new(project:, inspection:, fetched:, plan: Plan.for(inspection))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Runs on the calling thread, in the order the projects are listed.
|
|
59
|
+
def settle(step)
|
|
60
|
+
return step unless step.is_a?(Observed)
|
|
61
|
+
|
|
62
|
+
@unfetched += 1 if @dry_run && step.inspection.fetched_at.nil?
|
|
63
|
+
plan = step.plan
|
|
64
|
+
return skipped(step) unless plan.skips.empty?
|
|
65
|
+
return move(step) if plan.fast_forward?
|
|
66
|
+
|
|
67
|
+
still(step)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# The checks inside the fast-forward read the repository again right before git merges,
|
|
73
|
+
# and --ff-only is git's own last word.
|
|
74
|
+
def move(step)
|
|
75
|
+
project = step.project
|
|
76
|
+
plan = step.plan
|
|
77
|
+
return outcome(project, FAST_FORWARDED, drift(plan.items)) if @dry_run
|
|
78
|
+
|
|
79
|
+
onto = plan.to_revision? ? project.revision : Git::Repository::UPSTREAM
|
|
80
|
+
forward = @fetcher.exclusively(project) { @runtime.git.fast_forward(@fetcher.path(project), onto:) }
|
|
81
|
+
return still(step) unless forward.moved?
|
|
82
|
+
|
|
83
|
+
outcome(project, FAST_FORWARDED, [moved(step, forward), *declared(plan)])
|
|
84
|
+
rescue Git::Blocked => e
|
|
85
|
+
refused(step, e)
|
|
86
|
+
rescue Git::Error => e
|
|
87
|
+
Outcome.failure(project, e)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def moved(step, forward)
|
|
91
|
+
range = "#{step.inspection.status.branch} #{forward.from[0, ABBREV]}..#{forward.to[0, ABBREV]}"
|
|
92
|
+
return "#{range} #{TO_PIN}" if step.plan.to_revision?
|
|
93
|
+
|
|
94
|
+
gained = forward.count
|
|
95
|
+
undo = RolloutHistory.command('undo', step.project, @group)
|
|
96
|
+
"#{range} (#{gained} commit#{'s' unless gained == 1}); undo with '#{undo}'"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def refused(step, error)
|
|
100
|
+
reason = error.reason
|
|
101
|
+
detail = [error.message.delete_prefix("#{error.path}: "), ADVICE[reason]].compact.join('; ')
|
|
102
|
+
command = CommandLine.git(step.project, 'status') unless reason == 'Busy'
|
|
103
|
+
outcome(step.project, Outcome::SKIPPED, [detail, command, *declared(step.plan)], reason:)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def skipped(step)
|
|
107
|
+
blocker = step.plan.skips.first
|
|
108
|
+
outcome(step.project, Outcome::SKIPPED, [blocker.message, blocker.command, *declared(step.plan)],
|
|
109
|
+
reason: blocker.type)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Nothing moved, so the plan's drift says why. Only a FetchOnly project reports what its fetch
|
|
113
|
+
# brought; for any other, sync reports the branch.
|
|
114
|
+
def still(step)
|
|
115
|
+
fetched = step.fetched if step.project.sync_policy == SyncPolicy::FETCH_ONLY
|
|
116
|
+
outcome(step.project, fetched&.word || Outcome::UNCHANGED,
|
|
117
|
+
[*fetched&.details, held(step), *drift(step.plan.reports)])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def held(step)
|
|
121
|
+
"held at #{step.project.revision[0, ABBREV]} by spec.revision" if step.inspection.at_pin?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Worded as diff words it, with the command that shows or resolves it.
|
|
125
|
+
def unreadable(inspection)
|
|
126
|
+
item = Plan.for(inspection).items.first
|
|
127
|
+
outcome(inspection.project, Outcome::SKIPPED, [item.message, item.command], reason: item.type)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A move or a blocker already says where the branch stands against its upstream or its pin.
|
|
131
|
+
def declared(plan) = drift(plan.reports.reject { Drift::MOVES.include?(it.type) })
|
|
132
|
+
|
|
133
|
+
def drift(items) = items.map { "#{it.type}: #{it.message}" }
|
|
134
|
+
|
|
135
|
+
def outcome(project, word, details, reason: nil)
|
|
136
|
+
Outcome.new(project:, word:, reason:, details: details.compact)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../labels'
|
|
4
|
+
require_relative '../output'
|
|
5
|
+
|
|
6
|
+
module Slipway
|
|
7
|
+
module Views
|
|
8
|
+
module Group
|
|
9
|
+
# See Views::Project::FIELDS.
|
|
10
|
+
FIELDS = { 'metadata.name' => '' }.freeze
|
|
11
|
+
|
|
12
|
+
def self.headers(wide: false, labels: false)
|
|
13
|
+
columns = %w[NAME PROJECTS AGE]
|
|
14
|
+
columns << 'DESCRIPTION' if wide
|
|
15
|
+
columns << 'LABELS' if labels
|
|
16
|
+
columns
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.row(group, count:, now:, wide: false, labels: false)
|
|
20
|
+
cells = [group.name, count.to_s, Output::Age.humanize(group.created_at, now)]
|
|
21
|
+
cells << group.description if wide
|
|
22
|
+
cells << Labels.format(group.labels) if labels
|
|
23
|
+
cells
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.describe(group, count:, now:)
|
|
27
|
+
[['Name', group.name], ['Labels', group.labels], ['Created', group.created_at],
|
|
28
|
+
['Age', Output::Age.humanize(group.created_at, now)], ['Description', group.description],
|
|
29
|
+
['Projects', count]]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.object(group, count:) = group.to_manifest.merge('status' => { 'projects' => count })
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../git/status'
|
|
4
|
+
require_relative '../git/url'
|
|
5
|
+
require_relative '../labels'
|
|
6
|
+
require_relative '../plan'
|
|
7
|
+
require_relative '../resources'
|
|
8
|
+
require_relative '../state'
|
|
9
|
+
require_relative '../output'
|
|
10
|
+
|
|
11
|
+
module Slipway
|
|
12
|
+
module Views
|
|
13
|
+
module Project
|
|
14
|
+
DETACHED = '(detached)'
|
|
15
|
+
# A repository git could not read shows <none> in FETCHED, like its other cells, so
|
|
16
|
+
# <never> always means git answered and FETCH_HEAD records no fetch.
|
|
17
|
+
NEVER = '<never>'
|
|
18
|
+
ROLES = lambda do |header, value|
|
|
19
|
+
case header
|
|
20
|
+
when 'STATUS' then State.role(value)
|
|
21
|
+
when 'FETCHED' then :muted if value == NEVER
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
# The paths of #object a field selector may name, each with the value it compares as when
|
|
25
|
+
# the object leaves it out: kubectl compares an unset field as the empty string, while a
|
|
26
|
+
# missing lastFetch means no fetch is on record, which FETCHED shows as <never>.
|
|
27
|
+
FIELDS = { 'metadata.name' => '', 'metadata.group' => '', 'spec.path' => '', 'status.state' => '',
|
|
28
|
+
'status.branch' => '', 'status.lastFetch' => 'never' }.freeze
|
|
29
|
+
|
|
30
|
+
def self.headers(wide: false, group: false, labels: false)
|
|
31
|
+
columns = [*(['GROUP'] if group), 'NAME', 'BRANCH', 'STATUS', 'FETCHED', 'AGE']
|
|
32
|
+
columns.push('PATH', 'HEAD', 'LAST-COMMIT', 'DRIFT') if wide
|
|
33
|
+
columns << 'LABELS' if labels
|
|
34
|
+
columns
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# nil cells are fine: Table prints them as <none>.
|
|
38
|
+
def self.row(inspection, now:, wide: false, group: false, labels: false)
|
|
39
|
+
project = inspection.project
|
|
40
|
+
cells = [*([project.group] if group), project.name, branch(inspection.status), inspection.state,
|
|
41
|
+
fetched(inspection, now), Output::Age.humanize(project.created_at, now)]
|
|
42
|
+
if wide
|
|
43
|
+
cells.push(project.path, inspection.status&.head, last_commit_age(inspection.commit, now),
|
|
44
|
+
drift_types(inspection))
|
|
45
|
+
end
|
|
46
|
+
cells << Labels.format(project.labels) if labels
|
|
47
|
+
cells
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.describe(inspection, now:)
|
|
51
|
+
project = inspection.project
|
|
52
|
+
[['Name', project.name], ['Group', project.group], ['Labels', project.labels],
|
|
53
|
+
['Created', project.created_at], ['Age', Output::Age.humanize(project.created_at, now)],
|
|
54
|
+
['Path', project.path], ['Description', project.description], *desired(project),
|
|
55
|
+
['Status', Output::Painted.new(role: State.role(inspection.state), text: inspection.state)],
|
|
56
|
+
['Repository', repository(inspection)], ['Last Commit', last_commit(inspection.commit)],
|
|
57
|
+
['Drift', drift(inspection).map { [it.type, it.message] }]]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Fields git could not answer are left out, as kubectl leaves out unset fields.
|
|
61
|
+
def self.object(inspection)
|
|
62
|
+
status = inspection.status
|
|
63
|
+
inspection.project.to_manifest.merge(
|
|
64
|
+
'status' => plain(
|
|
65
|
+
'branch' => status&.branch, 'head' => status&.head, 'upstream' => status&.upstream,
|
|
66
|
+
'ahead' => status&.ahead, 'behind' => status&.behind, 'staged' => status&.staged,
|
|
67
|
+
'unstaged' => status&.unstaged, 'untracked' => status&.untracked,
|
|
68
|
+
'conflicted' => status&.conflicted, 'stashes' => status&.stashes,
|
|
69
|
+
'state' => inspection.state, 'lastFetch' => Resources.timestamp(inspection.fetched_at),
|
|
70
|
+
'drift' => drift_object(inspection),
|
|
71
|
+
'lastCommit' => commit_object(inspection.commit)
|
|
72
|
+
).compact
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.desired(project)
|
|
77
|
+
[['Remote', project.remote&.then { Git::Url.redact(it) }], ['Branch', project.branch],
|
|
78
|
+
['Revision', project.revision&.then { "#{it[0, Git::Porcelain::ABBREVIATION]} (pinned)" }],
|
|
79
|
+
['Sync Policy', project.sync_policy], ['Paused', project.paused]]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.branch(status)
|
|
83
|
+
return nil if status.nil?
|
|
84
|
+
|
|
85
|
+
status.detached? ? DETACHED : status.branch
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.fetched(inspection, now)
|
|
89
|
+
return nil if inspection.status.nil?
|
|
90
|
+
|
|
91
|
+
inspection.fetched_at ? Output::Age.humanize(inspection.fetched_at, now) : NEVER
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.last_commit_age(commit, now) = commit && Output::Age.humanize(commit.time, now)
|
|
95
|
+
|
|
96
|
+
def self.drift(inspection) = Plan.for(inspection).items
|
|
97
|
+
|
|
98
|
+
def self.drift_types(inspection)
|
|
99
|
+
types = drift(inspection).map(&:type)
|
|
100
|
+
types.join(',') unless types.empty?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.drift_object(inspection)
|
|
104
|
+
drift(inspection).map { plain('type' => it.type, 'message' => it.message, 'blocker' => it.blocker) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.repository(inspection)
|
|
108
|
+
status = inspection.status
|
|
109
|
+
return failure(inspection.error) if status.nil?
|
|
110
|
+
|
|
111
|
+
[['Branch', branch(status)], ['Head', status.head], ['Upstream', status.upstream],
|
|
112
|
+
['Ahead', status.ahead], ['Behind', status.behind], ['Staged', status.staged],
|
|
113
|
+
['Unstaged', status.unstaged], ['Untracked', status.untracked],
|
|
114
|
+
['Conflicted', status.conflicted], ['Stashes', status.stashes],
|
|
115
|
+
['Remote', inspection.remote&.then { Git::Url.redact(it) }], ['Last Fetch', last_fetch(inspection)]]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.last_fetch(inspection) = inspection.fetched_at || Output::Painted.new(role: :muted, text: NEVER)
|
|
119
|
+
|
|
120
|
+
# The Path line already shows the path, so it is cut from git's message.
|
|
121
|
+
def self.failure(error)
|
|
122
|
+
reason = error.message.delete_prefix("#{error.path}: ")
|
|
123
|
+
error.respond_to?(:hint) && error.hint ? [reason, error.hint] : reason
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.last_commit(commit)
|
|
127
|
+
return nil if commit.nil?
|
|
128
|
+
|
|
129
|
+
[['Hash', commit.sha], ['Author', "#{commit.author} <#{commit.email}>"],
|
|
130
|
+
['Date', Resources.timestamp(commit.time)], ['Subject', commit.subject]]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.commit_object(commit)
|
|
134
|
+
return nil if commit.nil?
|
|
135
|
+
|
|
136
|
+
plain('hash' => commit.sha, 'author' => commit.author, 'email' => commit.email,
|
|
137
|
+
'date' => Resources.timestamp(commit.time), 'subject' => commit.subject)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# json and yaml escape what their syntax needs, not what a terminal obeys, and `jq -r`
|
|
141
|
+
# prints a field as it is, so git's text is made plain here as it is in a table.
|
|
142
|
+
def self.plain(fields) = fields.transform_values { it.is_a?(String) ? Output.plain(it) : it }
|
|
143
|
+
|
|
144
|
+
private_class_method :desired, :branch, :fetched, :last_commit_age, :drift, :drift_types, :drift_object,
|
|
145
|
+
:repository, :last_fetch, :failure, :last_commit, :commit_object, :plain
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/slipway/yaml.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
|
|
5
|
+
module Slipway
|
|
6
|
+
# The one YAML writer: manifests, `get -o yaml` and `config view` print the same dialect.
|
|
7
|
+
module Yaml
|
|
8
|
+
DOCUMENT_START = "---\n"
|
|
9
|
+
|
|
10
|
+
# line_width: -1 turns off Psych's line folding. The document marker is dropped, the way
|
|
11
|
+
# kubectl prints objects.
|
|
12
|
+
def self.dump(hash) = Psych.safe_dump(hash, line_width: -1).delete_prefix(DOCUMENT_START)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/slipway.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'slipway/version'
|
|
4
|
+
require_relative 'slipway/cli'
|
|
5
|
+
require_relative 'slipway/paths'
|
|
6
|
+
require_relative 'slipway/settings'
|
|
7
|
+
require_relative 'slipway/editor'
|
|
8
|
+
require_relative 'slipway/names'
|
|
9
|
+
require_relative 'slipway/labels'
|
|
10
|
+
require_relative 'slipway/selector'
|
|
11
|
+
require_relative 'slipway/resources'
|
|
12
|
+
require_relative 'slipway/schema'
|
|
13
|
+
require_relative 'slipway/manifest'
|
|
14
|
+
require_relative 'slipway/store'
|
|
15
|
+
require_relative 'slipway/git'
|
|
16
|
+
require_relative 'slipway/state'
|
|
17
|
+
require_relative 'slipway/output'
|
|
18
|
+
require_relative 'slipway/runtime'
|
|
19
|
+
require_relative 'slipway/inspector'
|
|
20
|
+
require_relative 'slipway/views'
|
|
21
|
+
require_relative 'slipway/commands'
|
|
22
|
+
|
|
23
|
+
# A kubectl-style registry of the git repositories on your machine.
|
|
24
|
+
module Slipway
|
|
25
|
+
# Shared by exe/slipway and the tests, so it returns the exit status instead of exiting.
|
|
26
|
+
# `runtime_factory` is called only once a verb runs, with the context and the parsed options.
|
|
27
|
+
def self.run(argv, context: CLI::Context.system, runtime_factory: Runtime.method(:build))
|
|
28
|
+
registry = Commands.registry(runtime_factory)
|
|
29
|
+
color, theme = Runtime.color_defaults(context, argv)
|
|
30
|
+
CLI::Runner.new(registry, context, color:, theme:).run(argv)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-API-RESOURCES" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-api\-resources \- Print the supported resource types
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway api\-resources"
|
|
7
|
+
.RI [ flags ]
|
|
8
|
+
.YS
|
|
9
|
+
.SH DESCRIPTION
|
|
10
|
+
Print the supported resource types.
|
|
11
|
+
.PP
|
|
12
|
+
Prints a table of the resource types slipway knows, sorted by name. A command accepts a type by its NAME, its singular or one of its SHORTNAMES. Type words are case\-insensitive. Use \-o wide to add the verbs that act on each type, and \-o name to print only the names.
|
|
13
|
+
.SH OPTIONS
|
|
14
|
+
.TP
|
|
15
|
+
\fB\-o\fR, \fB\-\-output\fR \fIFORMAT\fR
|
|
16
|
+
Output format. One of: table, wide, name. (default "table")
|
|
17
|
+
.TP
|
|
18
|
+
\fB\-\-no\-headers\fR
|
|
19
|
+
When using the default output format, don't print headers.
|
|
20
|
+
.SH COLUMNS
|
|
21
|
+
Every type shows NAME, SHORTNAMES, KIND and GROUPED, and \-o wide adds VERBS. A type without aliases reads <none> in SHORTNAMES.
|
|
22
|
+
.TP
|
|
23
|
+
\fBNAME\fR
|
|
24
|
+
The name of the type, in the plural.
|
|
25
|
+
.TP
|
|
26
|
+
\fBSHORTNAMES\fR
|
|
27
|
+
The aliases of the type, besides its singular.
|
|
28
|
+
.TP
|
|
29
|
+
\fBKIND\fR
|
|
30
|
+
The kind a manifest of the type declares.
|
|
31
|
+
.TP
|
|
32
|
+
\fBGROUPED\fR
|
|
33
|
+
Whether resources of the type live in a group, so that \-n and \-A scope them. kubectl's NAMESPACED column says the same about namespaces.
|
|
34
|
+
.TP
|
|
35
|
+
\fBVERBS\fR
|
|
36
|
+
The commands that act on resources of the type.
|
|
37
|
+
.SH EXAMPLES
|
|
38
|
+
.EX
|
|
39
|
+
# Print the supported resource types
|
|
40
|
+
slipway api\-resources
|
|
41
|
+
.EE
|
|
42
|
+
.PP
|
|
43
|
+
.EX
|
|
44
|
+
# Print the supported resource types with more information
|
|
45
|
+
slipway api\-resources \-o wide
|
|
46
|
+
.EE
|
|
47
|
+
.PP
|
|
48
|
+
.EX
|
|
49
|
+
# Print the names of the supported resource types
|
|
50
|
+
slipway api\-resources \-o name
|
|
51
|
+
.EE
|
|
52
|
+
.SH "SEE ALSO"
|
|
53
|
+
.BR slipway (1)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-APPLY" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-apply \- Apply a configuration to a resource by file name or stdin
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway apply"
|
|
7
|
+
.B \-f
|
|
8
|
+
.I FILE
|
|
9
|
+
.RI [ flags ]
|
|
10
|
+
.YS
|
|
11
|
+
.SH DESCRIPTION
|
|
12
|
+
Apply a configuration to a resource by file name or stdin.
|
|
13
|
+
.PP
|
|
14
|
+
The resource name must be specified in the manifest. A resource is created when it does not exist yet, configured when its manifest differs from the one stored, and reported unchanged otherwise. A directory applies every *.yaml and *.yml file it holds, sorted by name and without descending into subdirectories. A project manifest without metadata.group lands in the current group.
|
|
15
|
+
.PP
|
|
16
|
+
YAML is accepted, with several documents per file, and a document of kind List stands for each of its items. Documents are applied in order, so a group may be created by the document before the projects that use it.
|
|
17
|
+
.SH OPTIONS
|
|
18
|
+
.TP
|
|
19
|
+
\fB\-f\fR, \fB\-\-filename\fR \fIFILE\fR
|
|
20
|
+
The file that contains the manifests to apply; may be repeated. A directory reads its *.yaml and *.yml files, '\-' reads stdin. (required)
|
|
21
|
+
.TP
|
|
22
|
+
\fB\-\-dry\-run\fR
|
|
23
|
+
Print what would change and write nothing.
|
|
24
|
+
.SH EXAMPLES
|
|
25
|
+
.EX
|
|
26
|
+
# Apply the configuration in hldr.yaml to a project
|
|
27
|
+
slipway apply \-f hldr.yaml
|
|
28
|
+
.EE
|
|
29
|
+
.PP
|
|
30
|
+
.EX
|
|
31
|
+
# Apply every manifest in a directory
|
|
32
|
+
slipway apply \-f ./projects
|
|
33
|
+
.EE
|
|
34
|
+
.PP
|
|
35
|
+
.EX
|
|
36
|
+
# Apply the YAML passed into stdin
|
|
37
|
+
slipway apply \-f \- < hldr.yaml
|
|
38
|
+
.EE
|
|
39
|
+
.PP
|
|
40
|
+
.EX
|
|
41
|
+
# Show what would change without writing anything
|
|
42
|
+
slipway apply \-f hldr.yaml \-\-dry\-run
|
|
43
|
+
.EE
|
|
44
|
+
.SH "SEE ALSO"
|
|
45
|
+
.BR slipway (1)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-COMPLETION" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-completion \- Output shell completion code for the specified shell (bash, zsh, fish)
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway completion"
|
|
7
|
+
.I SHELL\&
|
|
8
|
+
.RI [ flags ]
|
|
9
|
+
.YS
|
|
10
|
+
.SH DESCRIPTION
|
|
11
|
+
Output shell completion code for the specified shell (bash, zsh, fish).
|
|
12
|
+
The shell code must be evaluated to provide interactive completion of commands, resource types and names.
|
|
13
|
+
.SH EXAMPLES
|
|
14
|
+
.EX
|
|
15
|
+
# Install bash completions where bash\-completion loads them
|
|
16
|
+
slipway completion bash > "${XDG_DATA_HOME:\-$HOME/.local/share}/bash\-completion/completions/slipway"
|
|
17
|
+
.EE
|
|
18
|
+
.PP
|
|
19
|
+
.EX
|
|
20
|
+
# Install zsh completions in a directory on your fpath
|
|
21
|
+
slipway completion zsh > ~/.zfunc/_slipway
|
|
22
|
+
.EE
|
|
23
|
+
.PP
|
|
24
|
+
.EX
|
|
25
|
+
# Install fish completions
|
|
26
|
+
slipway completion fish > ~/.config/fish/completions/slipway.fish
|
|
27
|
+
.EE
|
|
28
|
+
.SH "SEE ALSO"
|
|
29
|
+
.BR slipway (1)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-CONFIG-PATH" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-config\-path \- Display the path of the configuration file
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway config path"
|
|
7
|
+
.RI [ flags ]
|
|
8
|
+
.YS
|
|
9
|
+
.SH DESCRIPTION
|
|
10
|
+
Display the path of the configuration file.
|
|
11
|
+
.PP
|
|
12
|
+
Prints the file that slipway reads: \-\-config, then SLIPWAY_CONFIG, then $XDG_CONFIG_HOME/slipway/config.yaml. When the default file does not exist, its path is still printed and a note on stderr says so; a file named by \-\-config or SLIPWAY_CONFIG must exist.
|
|
13
|
+
.SH EXAMPLES
|
|
14
|
+
.EX
|
|
15
|
+
# Print the path of the configuration file
|
|
16
|
+
slipway config path
|
|
17
|
+
.EE
|
|
18
|
+
.SH "SEE ALSO"
|
|
19
|
+
.BR slipway (1),
|
|
20
|
+
.BR slipway\-config (1)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-CONFIG-VIEW" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-config\-view \- Display the configuration in effect
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway config view"
|
|
7
|
+
.RI [ flags ]
|
|
8
|
+
.YS
|
|
9
|
+
.SH DESCRIPTION
|
|
10
|
+
Display the configuration in effect.
|
|
11
|
+
.PP
|
|
12
|
+
Prints every setting as YAML after applying the precedence flag, then SLIPWAY_* environment variable, then configuration file, then built\-in default. The first line names the configuration file that was consulted, and says (not found) when the default file does not exist. A file named by \-\-config or SLIPWAY_CONFIG must exist.
|
|
13
|
+
.SH EXAMPLES
|
|
14
|
+
.EX
|
|
15
|
+
# Show the settings in effect
|
|
16
|
+
slipway config view
|
|
17
|
+
.EE
|
|
18
|
+
.PP
|
|
19
|
+
.EX
|
|
20
|
+
# Show the settings another file would give
|
|
21
|
+
slipway config view \-\-config ~/work/slipway.yaml
|
|
22
|
+
.EE
|
|
23
|
+
.SH "SEE ALSO"
|
|
24
|
+
.BR slipway (1),
|
|
25
|
+
.BR slipway\-config (1)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-CONFIG" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-config \- Inspect the configuration in effect
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway config"
|
|
7
|
+
.I COMMAND
|
|
8
|
+
.RI [ flags ]
|
|
9
|
+
.YS
|
|
10
|
+
.SH DESCRIPTION
|
|
11
|
+
Inspect the configuration that slipway resolved from flags, environment variables and the configuration file.
|
|
12
|
+
.SH COMMANDS
|
|
13
|
+
.TP 6
|
|
14
|
+
\fBview\fR
|
|
15
|
+
Display the configuration in effect
|
|
16
|
+
.TP 6
|
|
17
|
+
\fBpath\fR
|
|
18
|
+
Display the path of the configuration file
|
|
19
|
+
.SH "SEE ALSO"
|
|
20
|
+
.BR slipway (1),
|
|
21
|
+
.BR slipway\-config\-view (1),
|
|
22
|
+
.BR slipway\-config\-path (1)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
.\" Generated by slipway 0.1.0. Do not edit.
|
|
2
|
+
.TH "SLIPWAY-CREATE" "1" "2026-09-30" "slipway 0.1.0" "Slipway Manual"
|
|
3
|
+
.SH NAME
|
|
4
|
+
slipway\-create \- Create a resource by name
|
|
5
|
+
.SH SYNOPSIS
|
|
6
|
+
.SY "slipway create"
|
|
7
|
+
.I "(TYPE NAME | project \-\-from\-dir DIR)"\&
|
|
8
|
+
.RI [ flags ]
|
|
9
|
+
.YS
|
|
10
|
+
.SH DESCRIPTION
|
|
11
|
+
Create a resource by name.
|
|
12
|
+
.PP
|
|
13
|
+
A project registers the git repository at \-\-path in the current group, which must exist unless it is the default group. A group is created empty and holds the projects you register with \-\-group later.
|
|
14
|
+
.PP
|
|
15
|
+
With \-\-from\-dir DIR in place of NAME, create project registers every git repository at DIR or under it, down to \-\-depth levels. Each project is named after its directory, lowercased with each run of characters other than ASCII letters, digits and dashes made one dash, and records its path and the URL of its origin without any credentials; the checked\-out branch is not recorded. A path the group already holds is reported unchanged, so running it again adds only new clones.
|
|
16
|
+
.PP
|
|
17
|
+
Use \-\-dry\-run to check the arguments without writing anything, and \-o yaml with it to print the manifest instead, ready for apply \-f.
|
|
18
|
+
.PP
|
|
19
|
+
Resource types: projects (project, proj) and groups (group). Type words are case\-insensitive.
|
|
20
|
+
.SH OPTIONS
|
|
21
|
+
.TP
|
|
22
|
+
\fB\-\-path\fR \fIDIR\fR
|
|
23
|
+
Directory of the git repository to register; required for a project given by NAME. A relative directory is stored resolved against the current directory, a ~ path as written.
|
|
24
|
+
.TP
|
|
25
|
+
\fB\-\-description\fR \fITEXT\fR
|
|
26
|
+
A short description of the resource.
|
|
27
|
+
.TP
|
|
28
|
+
\fB\-\-label\fR \fIKEY=VALUE\fR
|
|
29
|
+
A label to set on the new resource; may be repeated.
|
|
30
|
+
.TP
|
|
31
|
+
\fB\-\-remote\fR \fIURL\fR
|
|
32
|
+
The URL the origin remote is expected to have, written to spec.remote. A URL that embeds credentials is refused; use a credential helper.
|
|
33
|
+
.TP
|
|
34
|
+
\fB\-\-branch\fR \fINAME\fR
|
|
35
|
+
The branch the project is expected to have checked out, written to spec.branch.
|
|
36
|
+
.TP
|
|
37
|
+
\fB\-\-from\-dir\fR \fIDIR\fR
|
|
38
|
+
Register every git repository at or under DIR as a project, in place of NAME.
|
|
39
|
+
.TP
|
|
40
|
+
\fB\-\-depth\fR \fIN\fR
|
|
41
|
+
How many directory levels under \-\-from\-dir to search, from 1 to 8. The search stops at a repository and never follows a symbolic link under \-\-from\-dir. (default 1)
|
|
42
|
+
.TP
|
|
43
|
+
\fB\-\-dry\-run\fR
|
|
44
|
+
Print what would change and write nothing.
|
|
45
|
+
.TP
|
|
46
|
+
\fB\-o\fR, \fB\-\-output\fR \fIFORMAT\fR
|
|
47
|
+
Output format; without it, each resource prints a result line such as project/hldr created. One of: json, yaml, name.
|
|
48
|
+
.SH EXAMPLES
|
|
49
|
+
.EX
|
|
50
|
+
# Register the repository at ~/dev/hldr as a project in the current group
|
|
51
|
+
slipway create project hldr \-\-path '~/dev/hldr'
|
|
52
|
+
.EE
|
|
53
|
+
.PP
|
|
54
|
+
.EX
|
|
55
|
+
# Register a project in the work group with two labels
|
|
56
|
+
slipway create project api \-\-path '~/work/api' \-n work \-\-label lang=go \-\-label tier=api
|
|
57
|
+
.EE
|
|
58
|
+
.PP
|
|
59
|
+
.EX
|
|
60
|
+
# Register a project with the remote and the branch it is expected to have
|
|
61
|
+
slipway create project hldr \-\-path '~/dev/hldr' \-\-remote git@github.com:hvpaiva/hldr.git \-\-branch main
|
|
62
|
+
.EE
|
|
63
|
+
.PP
|
|
64
|
+
.EX
|
|
65
|
+
# Create a group with a description
|
|
66
|
+
slipway create group work \-\-description "Projects for the day job"
|
|
67
|
+
.EE
|
|
68
|
+
.PP
|
|
69
|
+
.EX
|
|
70
|
+
# Check the arguments without writing the project
|
|
71
|
+
slipway create project hldr \-\-path '~/dev/hldr' \-\-dry\-run
|
|
72
|
+
.EE
|
|
73
|
+
.PP
|
|
74
|
+
.EX
|
|
75
|
+
# Print the manifest of a project without registering it
|
|
76
|
+
slipway create project hldr \-\-path '~/dev/hldr' \-\-dry\-run \-o yaml
|
|
77
|
+
.EE
|
|
78
|
+
.PP
|
|
79
|
+
.EX
|
|
80
|
+
# Register every repository in ~/dev/personal in the personal group
|
|
81
|
+
slipway create project \-\-from\-dir ~/dev/personal \-n personal
|
|
82
|
+
.EE
|
|
83
|
+
.PP
|
|
84
|
+
.EX
|
|
85
|
+
# Write the manifests of the repositories two levels under ~/work to a file
|
|
86
|
+
slipway create project \-\-from\-dir ~/work \-\-depth 2 \-\-dry\-run \-o yaml > work.yaml
|
|
87
|
+
.EE
|
|
88
|
+
.SH "SEE ALSO"
|
|
89
|
+
.BR slipway (1)
|