cimas 0.1.4 → 0.3.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/.github/workflows/rake.yml +18 -0
- data/.github/workflows/release.yml +31 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +29 -0
- data/CLAUDE.md +56 -0
- data/README.adoc +467 -14
- data/bin/console +1 -1
- data/cimas.gemspec +4 -3
- data/exe/cimas +9 -201
- data/lib/cimas/cli/command.rb +827 -239
- data/lib/cimas/cli/error.rb +8 -0
- data/lib/cimas/cli/runner.rb +239 -0
- data/lib/cimas/cli.rb +7 -0
- data/lib/cimas/github.rb +47 -0
- data/lib/cimas/orphan_files.rb +37 -0
- data/lib/cimas/patch.rb +25 -0
- data/lib/cimas/release_preflight.rb +140 -0
- data/lib/cimas/repository.rb +25 -17
- data/lib/cimas/version.rb +1 -1
- data/lib/cimas/working_copy.rb +136 -0
- data/lib/cimas.rb +22 -3
- data/plans/cimas-revival-and-release-workflow-realignment.md +2158 -0
- metadata +38 -12
- data/Gemfile.lock +0 -86
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require "git"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
module Cimas
|
|
5
|
+
# Deep module over one repository's working copy: every git-gem call,
|
|
6
|
+
# exception rescue, and porcelain/CLI parsing the orchestrator needs
|
|
7
|
+
# lives behind ~10 domain verbs. Subcommands then read as wave prose
|
|
8
|
+
# instead of git incantations.
|
|
9
|
+
class WorkingCopy
|
|
10
|
+
def self.open(dir)
|
|
11
|
+
new(Git.open(dir))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clone(remote, name, path:)
|
|
15
|
+
Git.clone(remote, name, path: path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(git)
|
|
19
|
+
@git = git
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def head_sha
|
|
23
|
+
@git.object("HEAD").sha
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remote_name
|
|
27
|
+
@git.remotes.first.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# True when the working copy has any uncommitted change.
|
|
31
|
+
def drift?
|
|
32
|
+
out, = Open3.capture3("git", "-C", dir, "status", "--porcelain")
|
|
33
|
+
!out.strip.empty?
|
|
34
|
+
rescue StandardError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# checkout + reset_hard + clean: the "start from a pristine branch"
|
|
39
|
+
# preamble of sync and cleanup-orphan-files.
|
|
40
|
+
def reset_clean(branch, include_untracked: false)
|
|
41
|
+
checkout(branch)
|
|
42
|
+
@git.reset_hard(branch)
|
|
43
|
+
@git.clean(force: true, d: include_untracked)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stage(*paths)
|
|
47
|
+
paths.each { |path| @git.add(path) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def remove(*paths)
|
|
51
|
+
paths.each { |path| @git.remove(path) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# True when nothing is staged or modified — push skips the
|
|
55
|
+
# commit in this case.
|
|
56
|
+
def clean?
|
|
57
|
+
status = @git.status
|
|
58
|
+
status.changed.empty? && status.added.empty? && status.deleted.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def commit_all(message)
|
|
62
|
+
@git.commit_all(message)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def commit(message)
|
|
66
|
+
@git.commit(message)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# push's keep_changes=false preamble: stand on the base branch,
|
|
70
|
+
# soft-reset onto it, and discard an existing branch of the given
|
|
71
|
+
# name so it can be provisioned fresh.
|
|
72
|
+
def reset_onto(base, discard_branch: nil)
|
|
73
|
+
checkout(base)
|
|
74
|
+
@git.reset(base)
|
|
75
|
+
@git.branch(discard_branch).delete if discard_branch && @git.is_branch?(discard_branch)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Switch to a branch; :fresh discards an existing branch of the same
|
|
79
|
+
# name first (cleanup-orphan-files' shape).
|
|
80
|
+
def switch_branch(name, fresh: false)
|
|
81
|
+
@git.branch(name).delete if fresh && @git.is_branch?(name)
|
|
82
|
+
@git.branch(name).checkout
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# pull's dance: fetch origin, best-effort reset (a fresh clone may
|
|
86
|
+
# not hold the branch ref yet), checkout, pull.
|
|
87
|
+
def fetch_reset_pull(branch)
|
|
88
|
+
@git.remote("origin").fetch
|
|
89
|
+
begin
|
|
90
|
+
@git.reset_hard(branch)
|
|
91
|
+
rescue Git::Error
|
|
92
|
+
# reset can fail when the branch ref is absent on a fresh
|
|
93
|
+
# clone; checkout/pull below still applies.
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
checkout(branch)
|
|
97
|
+
@git.pull("origin", branch)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns :pushed, :behind_remote (non-force push behind the remote
|
|
101
|
+
# tip), or [:rejected, error]. The caller decides presentation.
|
|
102
|
+
# "Updates were rejected" covers both the classic git phrasing
|
|
103
|
+
# ("tip of your current branch is behind") and the modern one ("the
|
|
104
|
+
# remote contains work that you do not have locally").
|
|
105
|
+
def push(branch, force: false, remote: nil)
|
|
106
|
+
@git.push(remote || remote_name, branch, force: force)
|
|
107
|
+
:pushed
|
|
108
|
+
rescue Git::GitExecuteError, Git::FailedError => e
|
|
109
|
+
if e.message.match?(/Updates were rejected/)
|
|
110
|
+
:behind_remote
|
|
111
|
+
else
|
|
112
|
+
[:rejected, e]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def diff_patch
|
|
117
|
+
@git.diff.patch
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def each_staged_change
|
|
121
|
+
@git.status.changed.each do |file, status|
|
|
122
|
+
yield(file, status.blob(:index).contents)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def checkout(branch)
|
|
129
|
+
@git.checkout(branch)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def dir
|
|
133
|
+
@git.dir.to_s
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/cimas.rb
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "cimas/cli/command"
|
|
3
|
-
require "cimas/repository"
|
|
1
|
+
require "pathname"
|
|
4
2
|
|
|
5
3
|
module Cimas
|
|
4
|
+
# Ownership marker cimas writes into every file it generates and
|
|
5
|
+
# detects when deciding whether a file is cimas-managed (orphan
|
|
6
|
+
# cleanup). One definition — sync and cleanup must agree on it.
|
|
7
|
+
GENERATED_HEADER_MARKER = "Auto-generated by Cimas"
|
|
8
|
+
GENERATED_HEADER = [
|
|
9
|
+
"# #{GENERATED_HEADER_MARKER}: Do not edit it manually!",
|
|
10
|
+
"# See https://github.com/metanorma/cimas",
|
|
11
|
+
].join("\n").freeze
|
|
12
|
+
|
|
13
|
+
# Autoload paths are absolute (resolved from __dir__) so a plain
|
|
14
|
+
# `require_relative` boot from exe/cimas works from any cwd, without
|
|
15
|
+
# $LOAD_PATH setup.
|
|
16
|
+
autoload :VERSION, File.expand_path("cimas/version", __dir__)
|
|
17
|
+
autoload :Repository, File.expand_path("cimas/repository", __dir__)
|
|
18
|
+
autoload :Patch, File.expand_path("cimas/patch", __dir__)
|
|
19
|
+
autoload :OrphanFiles, File.expand_path("cimas/orphan_files", __dir__)
|
|
20
|
+
autoload :WorkingCopy, File.expand_path("cimas/working_copy", __dir__)
|
|
21
|
+
autoload :GitHub, File.expand_path("cimas/github", __dir__)
|
|
22
|
+
autoload :ReleasePreflight, File.expand_path("cimas/release_preflight", __dir__)
|
|
23
|
+
autoload :Cli, File.expand_path("cimas/cli", __dir__)
|
|
24
|
+
|
|
6
25
|
def self.root
|
|
7
26
|
File.dirname(__dir__)
|
|
8
27
|
end
|