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,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Slipway
|
|
6
|
+
module Commands
|
|
7
|
+
module Rollout
|
|
8
|
+
# Writes one spec field of each named project and prints what changed. No git command runs:
|
|
9
|
+
# the next fetch or sync reads the field.
|
|
10
|
+
class SpecChange < Base
|
|
11
|
+
def kinds = [Resources::PROJECTS]
|
|
12
|
+
|
|
13
|
+
def run(runtime, context, args, opts)
|
|
14
|
+
scope = scope(runtime, context, opts)
|
|
15
|
+
names = scope.project_targets(args, verb: self.class::VERB)
|
|
16
|
+
scope.select(Resources::PROJECTS, names) { |projects| projects.each { write(runtime, context, it) } }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def write(runtime, context, project)
|
|
22
|
+
changed = change(project)
|
|
23
|
+
runtime.store.save(changed) if changed
|
|
24
|
+
word, role = changed ? [self.class::DONE, :result_changed] : [self.class::NOTHING, :result_unchanged]
|
|
25
|
+
result_line(context, Resources::PROJECTS, project.name, word, role)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Unpin < SpecChange
|
|
30
|
+
DESCRIPTION = "Stop holding projects at a revision.\n\n" \
|
|
31
|
+
'Removes spec.revision from the manifest of each named project, so the next slipway sync ' \
|
|
32
|
+
'fast-forwards its branch onto its upstream again. The repository is left as it is. A ' \
|
|
33
|
+
'project without spec.revision prints not pinned.'
|
|
34
|
+
VERB = 'unpin'
|
|
35
|
+
DONE = 'unpinned'
|
|
36
|
+
NOTHING = 'not pinned'
|
|
37
|
+
|
|
38
|
+
def self.command(factory)
|
|
39
|
+
CLI::Command.new(
|
|
40
|
+
name: 'unpin', summary: 'Stop holding a project at a revision', description: DESCRIPTION,
|
|
41
|
+
usage: MANY_USAGE,
|
|
42
|
+
examples: [CLI::Example.new(comment: 'Let project hldr follow its upstream again',
|
|
43
|
+
command: 'rollout unpin hldr')],
|
|
44
|
+
positionals: [Options.project_positional(factory, required: true)], handler: new(factory)
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def change(project) = project.revision && project.with(revision: nil)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Pause < SpecChange
|
|
54
|
+
DESCRIPTION = "Mark the provided projects as paused.\n\n" \
|
|
55
|
+
'Sets spec.paused in the manifest of each named project. Paused projects are left alone by ' \
|
|
56
|
+
'slipway fetch and slipway sync, which run no git command in them; slipway rollout undo still ' \
|
|
57
|
+
'acts on them. Use slipway rollout resume to resume a paused project.'
|
|
58
|
+
VERB = 'pause'
|
|
59
|
+
DONE = 'paused'
|
|
60
|
+
NOTHING = 'already paused'
|
|
61
|
+
|
|
62
|
+
def self.command(factory)
|
|
63
|
+
CLI::Command.new(
|
|
64
|
+
name: 'pause', summary: 'Mark the provided project as paused', description: DESCRIPTION,
|
|
65
|
+
usage: MANY_USAGE,
|
|
66
|
+
examples: [CLI::Example.new(comment: 'Keep fetch and sync away from project dots',
|
|
67
|
+
command: 'rollout pause dots')],
|
|
68
|
+
positionals: [Options.project_positional(factory, required: true)], handler: new(factory)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def change(project) = project.paused ? nil : project.with(paused: true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class Resume < SpecChange
|
|
78
|
+
DESCRIPTION = "Resume paused projects.\n\n" \
|
|
79
|
+
'Removes spec.paused from the manifest of each named project, so slipway fetch and slipway ' \
|
|
80
|
+
'sync act on it again. A project that is not paused prints not paused.'
|
|
81
|
+
VERB = 'resume'
|
|
82
|
+
DONE = 'resumed'
|
|
83
|
+
NOTHING = 'not paused'
|
|
84
|
+
|
|
85
|
+
def self.command(factory)
|
|
86
|
+
CLI::Command.new(
|
|
87
|
+
name: 'resume', summary: 'Resume a paused project', description: DESCRIPTION, usage: MANY_USAGE,
|
|
88
|
+
examples: [CLI::Example.new(comment: 'Resume the paused project dots', command: 'rollout resume dots')],
|
|
89
|
+
positionals: [Options.project_positional(factory, required: true)], handler: new(factory)
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def change(project) = project.paused ? project.with(paused: false) : nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'manual'
|
|
5
|
+
require_relative 'results'
|
|
6
|
+
require_relative '../outcome'
|
|
7
|
+
require_relative '../rollback'
|
|
8
|
+
|
|
9
|
+
module Slipway
|
|
10
|
+
module Commands
|
|
11
|
+
module Rollout
|
|
12
|
+
class Undo < Base
|
|
13
|
+
# Raised once the line has printed: it already says why, so this adds only exit status 1.
|
|
14
|
+
class Failed < Error
|
|
15
|
+
def initialize = super(problems: [])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
DESCRIPTION = "Roll back a project to a previous revision.\n\n" \
|
|
19
|
+
'Moves the checked-out branch to the revision before the current one, or to the one ' \
|
|
20
|
+
'--to-revision names from slipway rollout history, and then writes that commit to ' \
|
|
21
|
+
'spec.revision, so sync holds the project there until slipway rollout unpin. The branch moves ' \
|
|
22
|
+
'back with git reset --keep, only when its upstream holds every commit the move drops, and ' \
|
|
23
|
+
'forward, to undo an undo, with git merge --ff-only. Untracked files and unstaged changes to ' \
|
|
24
|
+
'files the move leaves alone are kept; git refuses a move that would overwrite a local change ' \
|
|
25
|
+
'or an untracked file, and slipway refuses one that would overwrite an ignored file. A move ' \
|
|
26
|
+
'back skips a project with staged changes, and a move forward one with staged or unstaged ' \
|
|
27
|
+
"changes.\n\n" \
|
|
28
|
+
'A project whose HEAD is detached, whose branch has no commits or no upstream that exists, ' \
|
|
29
|
+
'that is in the middle of a merge, rebase or other operation, or whose branch has commits its ' \
|
|
30
|
+
'upstream lacks, is skipped with the reason and nothing is moved or written. A paused project ' \
|
|
31
|
+
"can be rolled back: pausing only keeps fetch and sync away.\n\n" \
|
|
32
|
+
'The project prints one line, one of the results listed below, with a reason in ' \
|
|
33
|
+
"parentheses and the details under it.\n\n" \
|
|
34
|
+
'With --dry-run nothing is moved or written, and the line ends in (dry run).'
|
|
35
|
+
EXIT_STATUSES = Manual::EXIT_STATUSES.merge(
|
|
36
|
+
'0' => 'The project was rolled back, or already stood at the revision and was held there.',
|
|
37
|
+
'1' => 'Runtime error, such as a missing resource or an unreadable manifest, or a project that was ' \
|
|
38
|
+
'skipped or whose move was denied or failed.'
|
|
39
|
+
).freeze
|
|
40
|
+
TO_REVISION = CLI::Option.new(long: 'to-revision', argument: 'N',
|
|
41
|
+
description: 'The revision to roll back to, as slipway rollout history ' \
|
|
42
|
+
'numbers it. 0, the default, is the revision before the ' \
|
|
43
|
+
'current one.')
|
|
44
|
+
REVISION_INVALID = 'invalid argument %p for --to-revision: must be a revision number, or 0'
|
|
45
|
+
RESULTS = CLI::Glossary.new(
|
|
46
|
+
title: 'Results',
|
|
47
|
+
entries: {
|
|
48
|
+
Rollback::ROLLED_BACK => 'The branch moved, or it already stood at the revision and only spec.revision ' \
|
|
49
|
+
'changed. The detail names the commits it crossed and the command that lets ' \
|
|
50
|
+
'sync follow the upstream again.',
|
|
51
|
+
Outcome::UNCHANGED => 'The branch already stood at the revision and spec.revision already held it.',
|
|
52
|
+
"#{Outcome::SKIPPED} (Reason)" => 'Nothing moved and nothing was written. git could not read the ' \
|
|
53
|
+
'repository (Missing, NotARepo, Unsafe, Unknown); the branch cannot ' \
|
|
54
|
+
'move (Conflicted, Detached, Unborn, NoUpstream, Gone, InProgress); ' \
|
|
55
|
+
'the history has no such revision (NoHistory, NoPrevious, ' \
|
|
56
|
+
'UnknownRevision) or the repository no such commit ' \
|
|
57
|
+
'(RevisionNotFound); a move back would drop commits the upstream ' \
|
|
58
|
+
'lacks (LocalCommits) or staged changes (Dirty), or the revision is ' \
|
|
59
|
+
"off the branch's history (Diverged); a move forward needs a tree " \
|
|
60
|
+
'without staged or unstaged changes (Dirty) and a revision on the ' \
|
|
61
|
+
'upstream (OffUpstream); or git refused the move (WouldLoseChanges, ' \
|
|
62
|
+
'WouldOverwrite for untracked or ignored files in the way, Busy, ' \
|
|
63
|
+
'NotFastForward for a branch that moved since the check).',
|
|
64
|
+
"#{Outcome::DENIED} (Reason)" => 'A partial clone had to fetch the files the move writes and the remote ' \
|
|
65
|
+
'asked for a password, a passphrase or a host key (AuthRequired), as ' \
|
|
66
|
+
'in slipway fetch.',
|
|
67
|
+
"#{Outcome::FAILED} (Reason)" => 'The move ran past networkTimeout (Timeout) or git failed for another ' \
|
|
68
|
+
'reason (Unknown), and spec.revision was not written, though a move ' \
|
|
69
|
+
'stopped at the deadline keeps the files git had already written, as ' \
|
|
70
|
+
'in slipway sync; or the branch moved but spec.revision could not be ' \
|
|
71
|
+
"written (#{Rollback::NOT_PINNED}), and the detail names the " \
|
|
72
|
+
'--to-revision command that writes it without moving the branch again.'
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
ROLES = { Rollback::ROLLED_BACK => :result_changed, Outcome::UNCHANGED => :result_unchanged,
|
|
76
|
+
Outcome::SKIPPED => :result_skipped, Outcome::DENIED => :result_denied,
|
|
77
|
+
Outcome::FAILED => :result_failed }.freeze
|
|
78
|
+
|
|
79
|
+
def self.command(factory)
|
|
80
|
+
CLI::Command.new(
|
|
81
|
+
name: 'undo', summary: 'Undo a previous rollout', description: DESCRIPTION, usage: SINGLE_USAGE,
|
|
82
|
+
examples:, exit_statuses: EXIT_STATUSES, glossaries: [RESULTS],
|
|
83
|
+
positionals: [Options.project_positional(factory, variadic: false, required: true)],
|
|
84
|
+
options: [TO_REVISION, Options::DRY_RUN],
|
|
85
|
+
handler: new(factory)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.examples
|
|
90
|
+
[
|
|
91
|
+
CLI::Example.new(comment: 'Roll back project hldr to the previous revision', command: 'rollout undo hldr'),
|
|
92
|
+
CLI::Example.new(comment: 'Roll back project hldr to revision 3',
|
|
93
|
+
command: 'rollout undo hldr --to-revision=3'),
|
|
94
|
+
CLI::Example.new(comment: 'Show where undo would move project hldr, without moving or writing anything',
|
|
95
|
+
command: 'rollout undo hldr --dry-run')
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
private_class_method :examples
|
|
99
|
+
|
|
100
|
+
def kinds = [Resources::PROJECTS]
|
|
101
|
+
|
|
102
|
+
def run(runtime, context, args, opts)
|
|
103
|
+
scope = scope(runtime, context, opts)
|
|
104
|
+
names = scope.project_targets(args, verb: 'roll back')
|
|
105
|
+
number = revision(opts[:to_revision])
|
|
106
|
+
dry_run = opts[:dry_run] == true
|
|
107
|
+
rollback = Rollback.new(runtime, dry_run:, group: opts[:group] ? nil : runtime.settings.group)
|
|
108
|
+
results = Results.new(context, ROLES, dry_run:)
|
|
109
|
+
scope.select(Resources::PROJECTS, names) do |projects|
|
|
110
|
+
projects.each { results.report(rollback.undo(it, number:)) }
|
|
111
|
+
end
|
|
112
|
+
raise Failed if results.any?(Outcome::SKIPPED, Outcome::DENIED, Outcome::FAILED)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def revision(value)
|
|
118
|
+
return if value.nil?
|
|
119
|
+
|
|
120
|
+
number = Integer(value, 10, exception: false)
|
|
121
|
+
number && !number.negative? ? number : raise(CLI::UsageError, format(REVISION_INVALID, value))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../cli/errors'
|
|
4
|
+
require_relative '../field_selector'
|
|
5
|
+
require_relative '../names'
|
|
6
|
+
require_relative '../output'
|
|
7
|
+
require_relative '../resources'
|
|
8
|
+
require_relative '../selector'
|
|
9
|
+
require_relative '../store'
|
|
10
|
+
require_relative '../views'
|
|
11
|
+
|
|
12
|
+
module Slipway
|
|
13
|
+
module Commands
|
|
14
|
+
# Names typed on the command line are checked here, so a bad one is a usage error rather
|
|
15
|
+
# than a failure deep in the store.
|
|
16
|
+
class Scope
|
|
17
|
+
NONE = 'No resources found.'
|
|
18
|
+
NONE_IN = 'No resources found in '
|
|
19
|
+
GROUP_SUFFIX = ' group.'
|
|
20
|
+
NAMES_WITH_SELECTOR = 'name cannot be provided when a selector is specified'
|
|
21
|
+
NAMES_WITH_ALL_GROUPS = 'a resource cannot be retrieved by name across all groups'
|
|
22
|
+
MIXED_TYPES = 'all resources must share one type'
|
|
23
|
+
MIXED_FORMS = 'a name in TYPE/NAME form cannot be combined with a bare name'
|
|
24
|
+
NAME_MISSING = 'missing required argument "NAME"'
|
|
25
|
+
|
|
26
|
+
def initialize(runtime, context, opts)
|
|
27
|
+
@runtime = runtime
|
|
28
|
+
@context = context
|
|
29
|
+
@opts = opts
|
|
30
|
+
@skipped = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def kind(word) = Resources.resolve(word)
|
|
34
|
+
|
|
35
|
+
# `TYPE/NAME...` is the form `get -o name` prints.
|
|
36
|
+
def targets(words)
|
|
37
|
+
type, *names = words
|
|
38
|
+
kind, names = type.include?('/') ? slashed(words) : [kind(type), names]
|
|
39
|
+
names.each { check_name(kind, it) }
|
|
40
|
+
[kind, names]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A verb that acts on repositories names projects alone, bare or in the TYPE/NAME form
|
|
44
|
+
# `get projects -o name` prints, since a group holds no repository of its own.
|
|
45
|
+
def project_targets(words, verb:)
|
|
46
|
+
kind, names = words.any? { it.include?('/') } ? slashed(words) : [Resources::PROJECTS, words]
|
|
47
|
+
raise CLI::UsageError, "cannot #{verb} a group" unless kind == Resources::PROJECTS
|
|
48
|
+
|
|
49
|
+
names.each { check_name(kind, it) }
|
|
50
|
+
check_names(kind, across: "cannot #{verb} a project by name across all groups") unless names.empty?
|
|
51
|
+
names
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def target(words)
|
|
55
|
+
kind, names = targets(words)
|
|
56
|
+
raise CLI::UsageError, NAME_MISSING if names.empty?
|
|
57
|
+
raise CLI::UsageError, "unexpected argument #{names[1].inspect}" if names.size > 1
|
|
58
|
+
|
|
59
|
+
[kind, names.first]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def group
|
|
63
|
+
flag = @opts[:group]
|
|
64
|
+
as_usage_error { Names.validate!(flag, what: 'group name') } unless flag.nil?
|
|
65
|
+
@runtime.group_for(@opts)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def all_groups? = @opts[:all_groups] == true
|
|
69
|
+
|
|
70
|
+
def selector = Selector.parse(@opts[:selector])
|
|
71
|
+
|
|
72
|
+
def field_selector(kind, names)
|
|
73
|
+
check_names(kind) unless names.empty?
|
|
74
|
+
FieldSelector.parse(@opts[:field_selector], fields: kind.namespaced? ? Views::Project::FIELDS : Views::Group::FIELDS)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A verb whose exit status vouches for every project must not report success when a listing
|
|
78
|
+
# left a manifest out.
|
|
79
|
+
def skipped? = @skipped
|
|
80
|
+
|
|
81
|
+
# As in kubectl, the resources that exist are shown before the names that do not are
|
|
82
|
+
# reported, one `error:` line each.
|
|
83
|
+
def select(kind, names)
|
|
84
|
+
return yield(listed(kind)) if names.empty?
|
|
85
|
+
|
|
86
|
+
check_names(kind)
|
|
87
|
+
found, missing = resolve(kind, names)
|
|
88
|
+
yield found unless found.empty?
|
|
89
|
+
raise Error.new(problems: missing) unless missing.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def report_none(kind)
|
|
93
|
+
return @context.warn(@context.paint_err(:muted, NONE)) if all_groups? || !kind.namespaced?
|
|
94
|
+
|
|
95
|
+
@context.warn(@context.paint_err(:muted, NONE_IN) + @context.paint_err(:string, group) +
|
|
96
|
+
@context.paint_err(:muted, GROUP_SUFFIX))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def slashed(words)
|
|
102
|
+
pairs = words.map do |word|
|
|
103
|
+
type, name = word.split('/', 2)
|
|
104
|
+
raise CLI::UsageError, MIXED_FORMS if name.nil?
|
|
105
|
+
|
|
106
|
+
[kind(type), name]
|
|
107
|
+
end
|
|
108
|
+
kinds = pairs.map(&:first).uniq
|
|
109
|
+
raise CLI::UsageError, MIXED_TYPES if kinds.size > 1
|
|
110
|
+
|
|
111
|
+
[kinds.first, pairs.map(&:last)]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def check_name(kind, name)
|
|
115
|
+
as_usage_error { Names.validate!(name, what: "#{kind.singular} name") }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def as_usage_error
|
|
119
|
+
yield
|
|
120
|
+
rescue Names::Invalid => e
|
|
121
|
+
raise CLI::UsageError, e.message
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A manifest that cannot be read is reported and skipped, so one bad file does not
|
|
125
|
+
# hide the rest of the registry.
|
|
126
|
+
def listed(kind)
|
|
127
|
+
matcher = selector
|
|
128
|
+
scope_group = kind.namespaced? && !all_groups? ? group : nil
|
|
129
|
+
resources = @runtime.store.list(kind, group: scope_group) do |problem|
|
|
130
|
+
@skipped = true
|
|
131
|
+
Output.warning(@context, problem.message)
|
|
132
|
+
end
|
|
133
|
+
resources.select { matcher.match?(it.labels) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def resolve(kind, names)
|
|
137
|
+
found = []
|
|
138
|
+
missing = []
|
|
139
|
+
names.each do |name|
|
|
140
|
+
found << @runtime.store.find(kind, name, group:)
|
|
141
|
+
rescue Store::NotFound => e
|
|
142
|
+
missing << e.message
|
|
143
|
+
end
|
|
144
|
+
[found, missing]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# kubectl refuses a label or field selector or --all-namespaces next to explicit names.
|
|
148
|
+
# `across` replaces kubectl's "retrieved" for a verb that acts on the projects it names.
|
|
149
|
+
def check_names(kind, across: NAMES_WITH_ALL_GROUPS)
|
|
150
|
+
selectors = @opts.values_at(:selector, :field_selector)
|
|
151
|
+
raise CLI::UsageError, NAMES_WITH_SELECTOR unless selectors.all? { it.to_s.strip.empty? }
|
|
152
|
+
raise CLI::UsageError, across if kind.namespaced? && all_groups?
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'manual'
|
|
5
|
+
require_relative 'fetch'
|
|
6
|
+
require_relative 'results'
|
|
7
|
+
require_relative '../fetcher'
|
|
8
|
+
require_relative '../outcome'
|
|
9
|
+
require_relative '../syncer'
|
|
10
|
+
|
|
11
|
+
module Slipway
|
|
12
|
+
module Commands
|
|
13
|
+
class Sync < Base
|
|
14
|
+
# Raised once every line has printed: the lines already say what went wrong, so it adds no
|
|
15
|
+
# error line, only exit status 1.
|
|
16
|
+
class Failed < Error
|
|
17
|
+
def initialize = super(problems: [])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
DESCRIPTION = "Fetch each selected project and fast-forward its branch.\n\n" \
|
|
21
|
+
'Runs in every project of the current group, in the projects named, in the ones a label ' \
|
|
22
|
+
'selector matches, or with --all-groups in every project. Each project is fetched as slipway ' \
|
|
23
|
+
'fetch fetches it and compared with its manifest as slipway diff compares it, and its ' \
|
|
24
|
+
'checked-out branch is fast-forwarded onto its upstream when the branch tracks an upstream ' \
|
|
25
|
+
'that still exists, is behind it without commits of its own, has no staged, unstaged or ' \
|
|
26
|
+
'conflicted changes and no merge, rebase or other operation in progress. Untracked files do ' \
|
|
27
|
+
'not stop it, and git refuses a fast-forward that would overwrite one. Sync never merges, ' \
|
|
28
|
+
"rebases, stashes, resets, switches a branch or changes a remote.\n\n" \
|
|
29
|
+
'A project whose spec.syncPolicy is FetchOnly is only fetched, and one whose spec.paused is ' \
|
|
30
|
+
'true is left alone: no git command runs in it. A project pinned by spec.revision is ' \
|
|
31
|
+
'fast-forwarded up to that commit instead of its upstream, only when its upstream holds the ' \
|
|
32
|
+
"commit, never past it, and never moved back to it.\n\n" \
|
|
33
|
+
'Each project prints one line, one of the results listed below, with a reason in parentheses ' \
|
|
34
|
+
'and the details under it. The parallel setting caps how many projects fetch at once, ' \
|
|
35
|
+
'fast-forwards run one at a time, and the results print in the order the projects are ' \
|
|
36
|
+
"listed.\n\n" \
|
|
37
|
+
'With --dry-run nothing is fetched or written: the plan is made from the last fetch.'
|
|
38
|
+
USAGE = '[NAME... | project/NAME...]'
|
|
39
|
+
EXIT_STATUSES = Manual::EXIT_STATUSES.merge(
|
|
40
|
+
'0' => 'Every selected project was fast-forwarded, fetched, left unchanged, skipped or paused.',
|
|
41
|
+
'1' => 'Runtime error, such as a missing resource or an unreadable manifest, or a project whose fetch or ' \
|
|
42
|
+
'fast-forward was denied or failed.'
|
|
43
|
+
).freeze
|
|
44
|
+
RESULTS = CLI::Glossary.new(
|
|
45
|
+
title: 'Results',
|
|
46
|
+
intro: 'When more than one project ran, a count of the results closes the run on stderr. With ' \
|
|
47
|
+
'--dry-run every line and the count end in (dry run). Ctrl-C stops the git processes slipway ' \
|
|
48
|
+
'started and exits with status 130; a fast-forward it stops ends as one stopped at the deadline.',
|
|
49
|
+
entries: {
|
|
50
|
+
Syncer::FAST_FORWARDED => 'The branch moved. For a move onto the upstream, the detail names the commits ' \
|
|
51
|
+
'it gained, as main a1b2c3d..e4f5a6b (3 commits), and the command that undoes ' \
|
|
52
|
+
"the move; a move up to a spec.revision pin ends in #{Syncer::TO_PIN}.",
|
|
53
|
+
Outcome::FETCHED => 'The fetch of a FetchOnly project moved refs; the refs follow as in slipway fetch.',
|
|
54
|
+
Outcome::UNCHANGED => 'The branch stayed where it was and nothing blocked it; the fetch may still have ' \
|
|
55
|
+
'moved remote-tracking refs.',
|
|
56
|
+
"#{Outcome::SKIPPED} (Reason)" => 'The branch stayed where it was: a blocker stopped it, under the name ' \
|
|
57
|
+
'slipway diff gives it; git refused the fast-forward (WouldOverwrite ' \
|
|
58
|
+
'for untracked files in the way, WouldLoseChanges for local changes git ' \
|
|
59
|
+
'status does not show, Busy for a lock another git process holds on ' \
|
|
60
|
+
'the index, HEAD or the branch, NotFastForward for a branch that ' \
|
|
61
|
+
'gained a commit since the check); or the project was skipped before ' \
|
|
62
|
+
'its fetch, as in slipway fetch (Missing, NotARepo, Unsafe, Unknown, ' \
|
|
63
|
+
'NoRemote, LocalUpstream).',
|
|
64
|
+
Outcome::PAUSED => 'spec.paused is true, so no git command ran in the project.',
|
|
65
|
+
"#{Outcome::DENIED} (Reason)" => 'The fetch or the fast-forward needed a password, a passphrase or a ' \
|
|
66
|
+
'host key (AuthRequired), as in slipway fetch.',
|
|
67
|
+
"#{Outcome::FAILED} (Reason)" => 'The fetch or the fast-forward ran past networkTimeout (Timeout), used ' \
|
|
68
|
+
'a transport protocols leaves out (ProtocolNotAllowed), or git failed ' \
|
|
69
|
+
'for another reason (Unknown). A fast-forward stopped at the deadline ' \
|
|
70
|
+
'leaves the branch where it was, but the files git had already written ' \
|
|
71
|
+
'stay in the working tree, and the detail names the command that lists ' \
|
|
72
|
+
'them.'
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
# In the order the closing summary counts them.
|
|
76
|
+
ROLES = { Syncer::FAST_FORWARDED => :result_changed, Outcome::FETCHED => :result_changed,
|
|
77
|
+
Outcome::UNCHANGED => :result_unchanged, Outcome::SKIPPED => :result_skipped,
|
|
78
|
+
Outcome::PAUSED => :result_paused, Outcome::DENIED => :result_denied,
|
|
79
|
+
Outcome::FAILED => :result_failed }.freeze
|
|
80
|
+
ALL_GROUPS = Options::ALL_GROUPS.with(description: 'If present, sync every project across all groups. The ' \
|
|
81
|
+
'group in the current configuration is ignored even if ' \
|
|
82
|
+
'specified with --group.')
|
|
83
|
+
|
|
84
|
+
def self.command(factory)
|
|
85
|
+
CLI::Command.new(
|
|
86
|
+
name: 'sync', summary: 'Fetch projects and fast-forward their branches', section: 'Repository Commands',
|
|
87
|
+
description: DESCRIPTION, examples:, usage: USAGE, exit_statuses: EXIT_STATUSES, glossaries: [RESULTS],
|
|
88
|
+
positionals: [Options.project_positional(factory)],
|
|
89
|
+
options: [Options::SELECTOR, ALL_GROUPS, Fetch::PRUNE, Options::DRY_RUN],
|
|
90
|
+
handler: new(factory)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.examples
|
|
95
|
+
[
|
|
96
|
+
CLI::Example.new(comment: 'Sync every project in the current group', command: 'sync'),
|
|
97
|
+
CLI::Example.new(comment: 'Sync every project in every group', command: 'sync -A'),
|
|
98
|
+
CLI::Example.new(comment: 'Sync two projects of the work group', command: 'sync api web -n work'),
|
|
99
|
+
CLI::Example.new(comment: 'Show what sync would do from the last fetch, without contacting any remote',
|
|
100
|
+
command: 'sync -A --dry-run')
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
private_class_method :examples
|
|
104
|
+
|
|
105
|
+
def kinds = [Resources::PROJECTS]
|
|
106
|
+
|
|
107
|
+
def run(runtime, context, args, opts)
|
|
108
|
+
scope = scope(runtime, context, opts)
|
|
109
|
+
names = scope.project_targets(args, verb: 'sync')
|
|
110
|
+
dry_run = opts[:dry_run] == true
|
|
111
|
+
fetcher = Fetcher.new(runtime, prune: opts[:prune] == true, dry_run:)
|
|
112
|
+
syncer = Syncer.new(runtime, fetcher, dry_run:, group: configured_group(runtime, opts))
|
|
113
|
+
results = Results.new(context, ROLES, dry_run:)
|
|
114
|
+
scope.select(Resources::PROJECTS, names) do |projects|
|
|
115
|
+
next scope.report_none(Resources::PROJECTS) if projects.empty?
|
|
116
|
+
|
|
117
|
+
results.stream(projects, workers: runtime.settings.parallel, work: syncer.method(:observe)) do |step|
|
|
118
|
+
syncer.settle(step)
|
|
119
|
+
end
|
|
120
|
+
unfetched(context, syncer.unfetched)
|
|
121
|
+
results.summarize
|
|
122
|
+
end
|
|
123
|
+
raise Failed if results.any?(Outcome::DENIED, Outcome::FAILED)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
# settings.group already holds a typed -n, and the undo line is run later without it.
|
|
129
|
+
def configured_group(runtime, opts) = opts[:group] ? nil : runtime.settings.group
|
|
130
|
+
|
|
131
|
+
def unfetched(context, count)
|
|
132
|
+
return if count.zero?
|
|
133
|
+
|
|
134
|
+
subject = count == 1 ? '1 project was' : "#{count} projects were"
|
|
135
|
+
Output.warning(context, "#{subject} never fetched and a dry run does not fetch; run 'slipway fetch' " \
|
|
136
|
+
'first for an up-to-date plan')
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'cli'
|
|
5
|
+
require_relative 'settings'
|
|
6
|
+
require_relative 'paths'
|
|
7
|
+
require_relative 'commands/manual'
|
|
8
|
+
require_relative 'commands/options'
|
|
9
|
+
require_relative 'commands/base'
|
|
10
|
+
require_relative 'commands/scope'
|
|
11
|
+
require_relative 'commands/get'
|
|
12
|
+
require_relative 'commands/describe'
|
|
13
|
+
require_relative 'commands/create'
|
|
14
|
+
require_relative 'commands/apply'
|
|
15
|
+
require_relative 'commands/delete'
|
|
16
|
+
require_relative 'commands/edit'
|
|
17
|
+
require_relative 'commands/label'
|
|
18
|
+
require_relative 'commands/explain'
|
|
19
|
+
require_relative 'commands/fetch'
|
|
20
|
+
require_relative 'commands/diff'
|
|
21
|
+
require_relative 'commands/sync'
|
|
22
|
+
require_relative 'commands/rollout'
|
|
23
|
+
require_relative 'commands/config'
|
|
24
|
+
require_relative 'commands/api_resources'
|
|
25
|
+
|
|
26
|
+
module Slipway
|
|
27
|
+
module Commands
|
|
28
|
+
PROGRAM = 'slipway'
|
|
29
|
+
DESCRIPTION = 'A kubectl-style registry of the git repositories on your machine'
|
|
30
|
+
# The space after each newline indents the paragraphs, as kubectl's root help does.
|
|
31
|
+
LONG_DESCRIPTION = "#{DESCRIPTION}.\n\n " \
|
|
32
|
+
"A project is a registered git repository. get and describe show where each one\n " \
|
|
33
|
+
"stands, and fetch fetches them all without prompting. Its manifest can also\n " \
|
|
34
|
+
"declare where the repository should be: the remote, the branch and a commit to\n " \
|
|
35
|
+
"hold it at. diff shows where a repository differs from that, sync fast-forwards\n " \
|
|
36
|
+
"the branches that can move without losing work, and rollout undo takes such a\n " \
|
|
37
|
+
"move back.\n\n " \
|
|
38
|
+
"Projects live in groups the way pods live in namespaces. The group is\n " \
|
|
39
|
+
"\"#{Resources::DEFAULT_GROUP}\", or the one SLIPWAY_GROUP or the group key sets, unless -n\n " \
|
|
40
|
+
"is given, and -A covers every group.\n\n " \
|
|
41
|
+
"#{Options::TYPES_SENTENCE}".freeze
|
|
42
|
+
# Help lists the verbs in this order within their sections.
|
|
43
|
+
VERBS = [Get, Describe, Create, Apply, Delete, Edit, Label, Explain, Fetch, Diff, Sync, Rollout, Config,
|
|
44
|
+
APIResources].freeze
|
|
45
|
+
|
|
46
|
+
def self.registry(factory)
|
|
47
|
+
CLI::Registry.new(program: PROGRAM, version: VERSION, description: DESCRIPTION,
|
|
48
|
+
long_description: LONG_DESCRIPTION,
|
|
49
|
+
globals: CLI::Globals.all(group_completer: Options.group_completer(factory)),
|
|
50
|
+
commands: VERBS.map { it.command(factory) },
|
|
51
|
+
builtins: { paths: Paths.method(:new) })
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'git/url'
|
|
4
|
+
|
|
5
|
+
module Slipway
|
|
6
|
+
# The ways a project can differ from its manifest, and the blockers that keep sync from
|
|
7
|
+
# resolving them. Plan decides which apply; this module holds the words and their sentences.
|
|
8
|
+
module Drift
|
|
9
|
+
# `command` is a git command line that shows or resolves the item; slipway prints it and never
|
|
10
|
+
# runs it. Either can quote a URL git answered with, so both are redacted here, once for every
|
|
11
|
+
# reader.
|
|
12
|
+
Item = Data.define(:type, :message, :command, :blocker) do
|
|
13
|
+
def initialize(type:, message:, command: nil, blocker: false)
|
|
14
|
+
super(type:, message: Git::Url.redact(message), command: command && Git::Url.redact(command), blocker:)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
MISSING = 'Missing'
|
|
19
|
+
REMOTE = 'Remote'
|
|
20
|
+
BRANCH = 'Branch'
|
|
21
|
+
REVISION = 'Revision'
|
|
22
|
+
BEHIND = 'Behind'
|
|
23
|
+
# In the order they are listed.
|
|
24
|
+
TYPES = [MISSING, REMOTE, BRANCH, REVISION, BEHIND].freeze
|
|
25
|
+
# When each type is reported, in the same order. The help of diff prints these, and the README
|
|
26
|
+
# table says the same with its code spans marked.
|
|
27
|
+
TYPE_MEANINGS = {
|
|
28
|
+
MISSING => 'The registered path is relative or is not a directory. For a path that is not a directory, a ' \
|
|
29
|
+
'project with spec.remote shows the git clone command that recreates it.',
|
|
30
|
+
REMOTE => 'origin is absent or differs from spec.remote. Sync never changes a remote.',
|
|
31
|
+
BRANCH => 'HEAD is detached or on another branch than spec.branch. Sync never switches branches.',
|
|
32
|
+
REVISION => 'HEAD is not the commit spec.revision pins. The pin replaces the upstream, so a pinned project is ' \
|
|
33
|
+
'never Behind; under FastForward sync will fast-forward a branch behind the pin to it unless a ' \
|
|
34
|
+
'blocker stops it, and never moves a branch back.',
|
|
35
|
+
BEHIND => 'The checked-out branch is behind its upstream. Under FastForward sync will fast-forward it unless a ' \
|
|
36
|
+
'blocker stops it; under FetchOnly, or while spec.paused is true, it is only reported.'
|
|
37
|
+
}.freeze
|
|
38
|
+
# The drift sync resolves by fast-forwarding the checked-out branch.
|
|
39
|
+
MOVES = [REVISION, BEHIND].freeze
|
|
40
|
+
|
|
41
|
+
# In the order Plan checks them. Each sentence says what holds sync back, so the line reads
|
|
42
|
+
# the same in diff, describe and sync. Rollback refuses with Conflicted and InProgress too,
|
|
43
|
+
# so those two name no verb.
|
|
44
|
+
BLOCKERS = {
|
|
45
|
+
'NotARepo' => '%<path>s holds files but no repository; sync clones only into an absent directory',
|
|
46
|
+
'Unsafe' => '%<reason>s',
|
|
47
|
+
'Unknown' => '%<reason>s',
|
|
48
|
+
'Detached' => 'HEAD is detached at %<head>s; sync never moves a detached HEAD',
|
|
49
|
+
'Unborn' => 'no commits yet; nothing to fast-forward',
|
|
50
|
+
'Gone' => 'upstream %<upstream>s no longer exists; sync never retargets a branch',
|
|
51
|
+
'NoUpstream' => '%<branch>s tracks no upstream; sync fast-forwards only a tracking branch',
|
|
52
|
+
'RevisionNotFound' => 'spec.revision %<revision>s is not in this repository; fetch it or unpin',
|
|
53
|
+
'PastRevision' => '%<branch>s is past the pinned revision; sync never moves a branch back',
|
|
54
|
+
'OffUpstream' => 'spec.revision %<revision>s is not on %<upstream>s; sync moves a branch only along its upstream',
|
|
55
|
+
'Conflicted' => '%<paths>s; finish or abort the merge first',
|
|
56
|
+
'Dirty' => '%<changes>s; sync fast-forwards only a tree without staged or unstaged changes',
|
|
57
|
+
'Diverged' => '%<ahead>d ahead, %<behind>d behind %<upstream>s; sync never merges or rebases',
|
|
58
|
+
'InProgress' => 'a %<operation>s is in progress'
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# What each blocker means, in the same order, for the help of diff and the README table.
|
|
62
|
+
BLOCKER_MEANINGS = {
|
|
63
|
+
'NotARepo' => 'The directory exists but holds no repository.',
|
|
64
|
+
'Unsafe' => 'git refused the repository because another user owns it (safe.directory); the git command that ' \
|
|
65
|
+
'trusts it follows.',
|
|
66
|
+
'Unknown' => 'git could not answer; the reason is also printed once on stderr.',
|
|
67
|
+
'Detached' => 'HEAD points at a commit rather than a branch.',
|
|
68
|
+
'Unborn' => 'The branch has no commits yet.',
|
|
69
|
+
'Gone' => 'The upstream is configured but its ref no longer exists.',
|
|
70
|
+
'NoUpstream' => 'The branch tracks no upstream.',
|
|
71
|
+
'RevisionNotFound' => 'The repository has no commit by the name spec.revision pins.',
|
|
72
|
+
'PastRevision' => 'HEAD is past the pinned commit or on another line of history, so reaching the pin would ' \
|
|
73
|
+
'move the branch back.',
|
|
74
|
+
'OffUpstream' => 'The upstream does not hold the pinned commit, which may be on another branch or a fork; sync ' \
|
|
75
|
+
'moves a branch only along its upstream.',
|
|
76
|
+
'Conflicted' => 'The branch is behind and the working tree has unmerged paths.',
|
|
77
|
+
'Dirty' => 'The branch is behind and has staged or unstaged changes; untracked files do not block.',
|
|
78
|
+
'Diverged' => 'The branch is behind and has commits of its own.',
|
|
79
|
+
'InProgress' => 'The branch would be fast-forwarded, but a merge, rebase, cherry-pick, revert, bisect or ' \
|
|
80
|
+
'git am is in progress.'
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
def self.blocker(word, command: nil, **values)
|
|
84
|
+
Item.new(type: word, message: format(BLOCKERS.fetch(word), **values), command:, blocker: true)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|