kettle-dev 1.1.1 → 1.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a5adcf9f057ec385ca20f8441e9cf7cf45a3c28916146dc828342cc280a7275
4
- data.tar.gz: 9c85ede6e630a006c83d26db02f2938be131fb227b3aa76e1e8f531f616edf3a
3
+ metadata.gz: 53a29ab92d63cca882cbb5034661c07b06e782c59fdad69f95fc5d19954f1b7f
4
+ data.tar.gz: 6d5e46fea1827a9fac76181c8070237a03503136f966991df5698cbf31653fbd
5
5
  SHA512:
6
- metadata.gz: ae3e344c567670c9817d9724bddbe08f926564e906843ae02bb95110bb2fce3ae3769511a5106e73bcb0abf93c2a1a64cf876c5d82bd6052adaa6046cd5eeecb
7
- data.tar.gz: 9ccad75834eb783f912660a214416e3fd53d8229955a69026c013fbcb4ddfd6b43bc211ace28e7a0e7dec08f41a2c3463e862515f7fae0e9059111c49c478d74
6
+ metadata.gz: df2b7fa0b2a766b6ab6e42b1ac5414a84472e1f21295cf9b717e598d407c0d01183ce703bbe5957b33343c2d2ffe178cdc9688546548747ed2fc7f0f18d76f9a
7
+ data.tar.gz: 286cc3ae10d0da550de8d298c96752681ad77a6a63c36733c1123a3f1f1bf15e30e680573775349d190e1ca3ea414a529d84bb19b592d0a8f220c53279dbeaa3
checksums.yaml.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- �kj��~�Y��/���
2
- E���RO7VO$����<a>q��o_o�LM�h|���$Z%��� ���H���{��/��?,�z��
3
- "�}1����m�B�$��1�W��Eہ�li�D6��̛0��;&m��]�D�તO��� ��B=s�_E)�)�Ro�� I��VtaaĪ�Y���ӝT�jR;]`��4Ю��(y��h"\8sØ�۸�{;��a
1
+ /9����O�M��W{��;�88�[�U����T@���\`�Lg��.���N�R���Һ�0�Ȗ0*
2
+ ���`�A<�Q�:��$�Q2(!�9���!��
data/.gitignore CHANGED
@@ -16,6 +16,7 @@ Appraisal.*.gemfile.lock
16
16
  /coverage/
17
17
  /spec/reports/
18
18
  /results/
19
+ .output.txt
19
20
 
20
21
  # Documentation
21
22
  /.yardoc/
@@ -1,3 +1,17 @@
1
+ # You can override the included template(s) by including variable overrides
2
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
3
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
4
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
5
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
6
+ # Note that environment variables can be set in several places
7
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
8
+ #stages:
9
+ # - test
10
+ #sast:
11
+ # stage: test
12
+ #include:
13
+ # - template: Security/SAST.gitlab-ci.yml
14
+
1
15
  default:
2
16
  image: ruby
3
17
 
data/.rubocop.yml CHANGED
@@ -14,3 +14,6 @@ RSpec/ExampleLength:
14
14
 
15
15
  RSpec/MultipleExpectations:
16
16
  Enabled: false
17
+
18
+ RSpec/NestedGroups:
19
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -24,6 +24,25 @@ Please file a bug if you notice a violation of semantic versioning.
24
24
  ### Fixed
25
25
  ### Security
26
26
 
27
+ ## [1.1.2] - 2025-09-02
28
+ - TAG: [v1.1.2][1.1.2t]
29
+ - COVERAGE: 97.14% -- 2858/2942 lines in 22 files
30
+ - BRANCH COVERAGE: 82.29% -- 1194/1451 branches in 22 files
31
+ - 76.76% documented
32
+ ### Added
33
+ - .gitlab-ci.yml documentation (in example)
34
+ - kettle-dvcs script for setting up DVCS, and checking status of remotes
35
+ - https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
36
+ - kettle-dvcs --status: prefix "ahead by N" with ✅️ when N==0, and 🔴 when N>0
37
+ - kettle-dvcs --status: also prints a Local status section comparing local HEAD to origin/<branch>, and keeps origin visible via that section
38
+ - Document kettle-dvcs CLI in README (usage, options, examples)
39
+ - RBS types for Kettle::Dev::DvcsCLI and inline YARD docs on CLI
40
+ - Specs for DvcsCLI covering remote normalization, fetch outcomes, and README updates
41
+ ### Changed
42
+ - major spec refactoring
43
+ ### Fixed
44
+ - (linting) rspec-pending_for 0.0.17+ (example gemspec)
45
+
27
46
  ## [1.1.1] - 2025-09-02
28
47
  - TAG: [v1.1.1][1.1.1t]
29
48
  - COVERAGE: 97.04% -- 2655/2736 lines in 21 files
@@ -428,7 +447,9 @@ Please file a bug if you notice a violation of semantic versioning.
428
447
  - Selecting will run the selected workflow via `act`
429
448
  - This may move to its own gem in the future.
430
449
 
431
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.1...HEAD
450
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.2...HEAD
451
+ [1.1.2]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.1...v1.1.2
452
+ [1.1.2t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.2
432
453
  [1.1.1]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.0...v1.1.1
433
454
  [1.1.1t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.1
434
455
  [1.1.0]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.27...v1.1.0
data/CONTRIBUTING.md CHANGED
@@ -89,9 +89,10 @@ bundle exec rake test
89
89
 
90
90
  ### Spec organization (required)
91
91
 
92
- - For each class or module under `lib/`, keep all of its unit tests in a single spec file under `spec/` that mirrors the path and file name (e.g., specs for `lib/kettle/dev/release_cli.rb` live in `spec/kettle/dev/release_cli_spec.rb`).
93
- - Do not create ad-hoc "_more" or split spec files for the same class/module. Consolidate all unit tests into the main spec file for that class/module.
94
- - Only integration scenarios that intentionally span multiple classes belong in `spec/integration/`.
92
+ - One spec file per class/module. For each class or module under `lib/`, keep all of its unit tests in a single spec file under `spec/` that mirrors the path and file name exactly: `lib/kettle/dev/release_cli.rb` -> `spec/kettle/dev/release_cli_spec.rb`.
93
+ - Never add a second spec file for the same class/module. Examples of disallowed names: `*_more_spec.rb`, `*_extra_spec.rb`, `*_status_spec.rb`, or any other suffix that still targets the same class. If you find yourself wanting a second file, merge those examples into the canonical spec file for that class/module.
94
+ - Exception: Integration specs that intentionally span multiple classes. Place these under `spec/integration/` (or a clearly named integration folder), and do not directly mirror a single class. Name them after the scenario, not a class.
95
+ - Migration note: If a duplicate spec file exists, move all examples into the canonical file and delete the duplicate. Do not leave stubs or empty files behind.
95
96
 
96
97
  ## Lint It
97
98
 
data/README.md CHANGED
@@ -77,6 +77,51 @@ Compatible with MRI Ruby 2.3+, and concordant releases of JRuby, and TruffleRuby
77
77
 
78
78
  ### Federated DVCS
79
79
 
80
+ #### kettle-dvcs (normalize multi-forge remotes)
81
+
82
+ - Script: `exe/kettle-dvcs` (install binstubs for convenience: `bundle binstubs kettle-dev --path bin`)
83
+ - Purpose: Normalize git remotes across GitHub, GitLab, and Codeberg, and create an `all` remote that pushes to all and fetches only from your chosen origin.
84
+ - Assumptions: org and repo names are identical across forges.
85
+
86
+ Usage:
87
+
88
+ ```console
89
+ kettle-dvcs [options] [ORG] [REPO]
90
+ ```
91
+
92
+ Options:
93
+ - `--origin [github|gitlab|codeberg]` Which forge to use as `origin` (default: github)
94
+ - `--protocol [ssh|https]` URL style (default: ssh)
95
+ - `--github-name NAME` Remote name for GitHub when not origin (default: gh)
96
+ - `--gitlab-name NAME` Remote name for GitLab (default: gl)
97
+ - `--codeberg-name NAME` Remote name for Codeberg (default: cb)
98
+ - `--force` Non-interactive; accept defaults, and do not prompt for ORG/REPO
99
+
100
+ Examples:
101
+ - Default, interactive (infers ORG/REPO from an existing remote when possible):
102
+ ```console
103
+ kettle-dvcs
104
+ ```
105
+ - Non-interactive with explicit org/repo:
106
+ ```console
107
+ kettle-dvcs --force my-org my-repo
108
+ ```
109
+ - Use GitLab as origin and HTTPS URLs:
110
+ ```console
111
+ kettle-dvcs --origin gitlab --protocol https my-org my-repo
112
+ ```
113
+
114
+ What it does:
115
+ - Ensures remotes exist and have consistent URLs for each forge.
116
+ - Renames existing remotes when their URL already matches the desired target but their name does not (e.g., `gitlab` -> `gl`).
117
+ - Creates/refreshes an `all` remote that:
118
+ - fetches only from your chosen `origin` forge.
119
+ - has pushurls configured for all three forges so `git push all <branch>` updates all mirrors.
120
+ - Prints `git remote -v` at the end.
121
+ - Attempts to `git fetch` each forge remote to check availability:
122
+ - If all succeed, the README’s federated DVCS summary line has “(Coming soon!)” removed.
123
+ - If any fail, the script prints import links to help you create a mirror on that forge.
124
+
80
125
  <details>
81
126
  <summary>Find this repo on other forges (Coming soon!)</summary>
82
127
 
@@ -750,7 +795,7 @@ Thanks for RTFM. ☺️
750
795
  [📌gitmoji]:https://gitmoji.dev
751
796
  [📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20😜%20😍-34495e.svg?style=flat-square
752
797
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
753
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.736-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
798
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.942-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
754
799
  [🔐security]: SECURITY.md
755
800
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
756
801
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/README.md.example CHANGED
@@ -505,7 +505,7 @@ Thanks for RTFM. ☺️
505
505
  [📌gitmoji]:https://gitmoji.dev
506
506
  [📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20😜%20😍-34495e.svg?style=flat-square
507
507
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
508
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.736-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
508
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.942-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
509
509
  [🔐security]: SECURITY.md
510
510
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
511
511
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/Rakefile.example CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # kettle-dev Rakefile v1.1.1 - 2025-09-02
3
+ # kettle-dev Rakefile v1.1.2 - 2025-09-02
4
4
  # Ruby 2.3 (Safe Navigation) or higher required
5
5
  #
6
6
  # MIT License (see License.txt)
data/exe/kettle-dvcs ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # vim: set syntax=ruby
5
+
6
+ # kettle-dvcs: Normalize git remotes across GitHub, GitLab, and Codeberg
7
+ # - Aligns/creates remotes for the three forges and an 'all' remote
8
+ # - Attempts fetch from each forge and updates README federation summary
9
+
10
+ # Immediate, unbuffered output
11
+ $stdout.sync = true
12
+ $stderr.sync = true
13
+
14
+ # Depending library or project must be using bundler
15
+ require "bundler/setup"
16
+
17
+ script_basename = File.basename(__FILE__)
18
+
19
+ begin
20
+ require "kettle/dev"
21
+ puts "== #{script_basename} v#{Kettle::Dev::Version::VERSION} =="
22
+ rescue LoadError => e
23
+ warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
24
+ warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
25
+ exit(1)
26
+ end
27
+
28
+ # Always execute when this file is loaded (e.g., via a Bundler binstub).
29
+ # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
30
+ if ARGV.include?("-h") || ARGV.include?("--help")
31
+ puts <<~USAGE
32
+ Usage: kettle-dvcs [options] [ORG] [REPO]
33
+
34
+ Normalizes git remotes across GitHub, GitLab, and Codeberg.
35
+
36
+ Options:
37
+ --origin [github|gitlab|codeberg] Choose origin forge (default: github)
38
+ --protocol [ssh|https] URL scheme (default: ssh)
39
+ --github-name NAME Remote name for GitHub when not origin (default: gh)
40
+ --gitlab-name NAME Remote name for GitLab (default: gl)
41
+ --codeberg-name NAME Remote name for Codeberg (default: cb)
42
+ --status Fetch remotes; show ahead/behind vs origin/main
43
+ --force Non-interactive; accept defaults
44
+
45
+ Behavior:
46
+ - Ensures remotes exist and have consistent URLs
47
+ - Creates an 'all' remote that fetches from origin only and pushes to all three
48
+ - Prints `git remote -v`
49
+ - Fetches each forge to detect availability and updates README accordingly
50
+
51
+ Environment:
52
+ KETTLE_DEV_DISABLE_GIT_GEM=true # force CLI git backend even if 'git' gem present
53
+ DEBUG=true # print backtraces on errors
54
+ USAGE
55
+ exit 0
56
+ end
57
+
58
+ begin
59
+ # Pass ARGV through; DvcsCLI does option parsing
60
+ exit(Kettle::Dev::DvcsCLI.new(ARGV).run!)
61
+ rescue LoadError => e
62
+ warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
63
+ warn(e.backtrace.join("\n")) if ENV["DEBUG"]
64
+ exit(1)
65
+ rescue SystemExit => e
66
+ warn("#{script_basename}: exited (status=#{e.status}, msg=#{e.message})") if e.status != 0
67
+ raise
68
+ rescue StandardError => e
69
+ warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
70
+ warn(e.backtrace.join("\n"))
71
+ exit(1)
72
+ end
@@ -130,7 +130,7 @@ Gem::Specification.new do |spec|
130
130
  # Testing
131
131
  spec.add_development_dependency("appraisal2", "~> 3.0") # ruby >= 1.8.7, for testing against multiple versions of dependencies
132
132
  spec.add_development_dependency("kettle-test", "~> 1.0") # ruby >= 2.3
133
- spec.add_development_dependency("rspec-pending_for") # ruby >= 2.3, used to skip specs on incompatible Rubies
133
+ spec.add_development_dependency("rspec-pending_for", "~> 0.0", ">= 0.0.17") # ruby >= 2.3, used to skip specs on incompatible Rubies
134
134
 
135
135
  # Releasing
136
136
  spec.add_development_dependency("ruby-progressbar", "~> 1.13") # ruby >= 0
@@ -0,0 +1,396 @@
1
+ require "optparse"
2
+
3
+ module Kettle
4
+ module Dev
5
+ # CLI to normalize git remotes across GitHub, GitLab, and Codeberg.
6
+ # - Defaults: origin=github, protocol=ssh, gitlab remote name=gl, codeberg remote name=cb
7
+ # - Creates/aligns remotes and an 'all' remote that pulls only from origin, pushes to all
8
+ #
9
+ # Usage:
10
+ # kettle-dvcs [options] [ORG] [REPO]
11
+ #
12
+ # Options:
13
+ # --origin [github|gitlab|codeberg] Choose which forge is origin (default: github)
14
+ # --protocol [ssh|https] Use git+ssh or HTTPS URLs (default: ssh)
15
+ # --gitlab-name NAME Remote name for GitLab (default: gl)
16
+ # --codeberg-name NAME Remote name for Codeberg (default: cb)
17
+ # --force Accept defaults; non-interactive
18
+ #
19
+ # Behavior:
20
+ # - Aligns or creates remotes for github, gitlab, and codeberg with consistent org/repo and protocol
21
+ # - Renames existing remotes to match chosen naming scheme when URLs already match
22
+ # - Creates an "all" remote that fetches from origin only and pushes to all three forges
23
+ # - Attempts to fetch from each forge to determine availability and updates README federation summary
24
+ #
25
+ # @example Non-interactive run with defaults (origin: github, protocol: ssh)
26
+ # kettle-dvcs --force my-org my-repo
27
+ #
28
+ # @example Use GitLab as origin and HTTPS URLs
29
+ # kettle-dvcs --origin gitlab --protocol https my-org my-repo
30
+ class DvcsCLI
31
+ DEFAULTS = {
32
+ origin: "github",
33
+ protocol: "ssh",
34
+ gh_name: "gh",
35
+ gl_name: "gl",
36
+ cb_name: "cb",
37
+ force: false,
38
+ status: false,
39
+ }.freeze
40
+
41
+ # Create the CLI with argv-like arguments
42
+ # @param argv [Array<String>] the command-line arguments (without program name)
43
+ def initialize(argv)
44
+ @argv = argv
45
+ @opts = DEFAULTS.dup
46
+ end
47
+
48
+ # Execute the CLI command.
49
+ # Aligns remotes, configures the `all` remote, prints remotes, attempts fetches,
50
+ # and updates README federation status accordingly.
51
+ # @return [Integer] exit status code (0 on success; may abort with non-zero)
52
+ def run!
53
+ parse!
54
+ git = ensure_git_adapter!
55
+
56
+ if @opts[:status]
57
+ # Status mode: no working tree mutation beyond fetch. Don't require clean tree.
58
+ _, _ = resolve_org_repo(git)
59
+ names = remote_names
60
+ branch = detect_default_branch!(git)
61
+ say("Fetching all remotes for status...")
62
+ # Fetch origin first to ensure origin/<branch> is up to date
63
+ git.fetch(names[:origin]) if names[:origin]
64
+ %i[github gitlab codeberg].each do |forge|
65
+ r = names[forge]
66
+ next unless r && r != names[:origin]
67
+ git.fetch(r)
68
+ end
69
+ show_status!(git, names, branch)
70
+ show_local_vs_origin!(git, branch)
71
+ return 0
72
+ end
73
+
74
+ abort!("Working tree is not clean; commit or stash changes before proceeding") unless git.clean?
75
+
76
+ org, repo = resolve_org_repo(git)
77
+
78
+ names = remote_names
79
+ urls = forge_urls(org, repo)
80
+
81
+ # Ensure remotes exist and have desired names/urls
82
+ ensure_remote_alignment!(git, names[:origin], urls[@opts[:origin].to_sym])
83
+ ensure_remote_alignment!(git, names[:github], urls[:github]) if names[:github] && names[:github] != names[:origin]
84
+ ensure_remote_alignment!(git, names[:gitlab], urls[:gitlab]) if names[:gitlab]
85
+ ensure_remote_alignment!(git, names[:codeberg], urls[:codeberg]) if names[:codeberg]
86
+
87
+ # Configure "all" remote: fetch only from origin, push to all three
88
+ configure_all_remote!(git, names, urls)
89
+
90
+ say("Remotes normalized. Origin: #{names[:origin]} (#{urls[@opts[:origin].to_sym]})")
91
+ show_remotes!(git)
92
+ fetch_results = attempt_fetches!(git, names)
93
+ update_readme_federation_status!(org, repo, fetch_results)
94
+ 0
95
+ end
96
+
97
+ private
98
+
99
+ # Determine default branch to compare against. Prefer 'main', fallback to 'master'.
100
+ # Uses origin to check existence.
101
+ def detect_default_branch!(git)
102
+ _out, ok = git.capture(["rev-parse", "--verify", "origin/main"])
103
+ return "main" if ok
104
+ _out2, ok2 = git.capture(["rev-parse", "--verify", "origin/master"])
105
+ return "master" if ok2
106
+ # Default to main if neither verifies
107
+ "main"
108
+ end
109
+
110
+ # Show ahead/behind status for each configured forge remote relative to origin/<branch>
111
+ def show_status!(git, names, branch)
112
+ base = "origin/#{branch}"
113
+ say("\nRemote status relative to #{base}:")
114
+ existing = Array(git.remotes)
115
+ {
116
+ github: names[:github],
117
+ gitlab: names[:gitlab],
118
+ codeberg: names[:codeberg],
119
+ }.each do |forge, remote|
120
+ next unless remote
121
+ next if remote == names[:origin]
122
+ next unless existing.include?(remote)
123
+ ref = "#{remote}/#{branch}"
124
+ out, ok = git.capture(["rev-list", "--left-right", "--count", "#{base}...#{ref}"])
125
+ if ok && !out.to_s.strip.empty?
126
+ parts = out.strip.split(/\s+/)
127
+ left = parts[0].to_i
128
+ right = parts[1].to_i
129
+ # left = commits only in base (origin) => remote is behind by left
130
+ # right = commits only in remote => remote is ahead by right
131
+ if left.zero? && right.zero?
132
+ say(" - #{forge} (#{remote}): in sync")
133
+ else
134
+ ahead_emoji = right.zero? ? "✅️" : "🔴"
135
+ say(" - #{forge} (#{remote}): #{ahead_emoji} ahead by #{right}, behind by #{left}")
136
+ end
137
+ else
138
+ say(" - #{forge} (#{remote}): no data (branch missing?)")
139
+ end
140
+ end
141
+ end
142
+
143
+ # Show local working copy status relative to origin/<branch>
144
+ def show_local_vs_origin!(git, branch)
145
+ base = "origin/#{branch}"
146
+ # Compare local HEAD to origin/<branch>
147
+ out, ok = git.capture(["rev-list", "--left-right", "--count", "HEAD...#{base}"])
148
+ say("\nLocal status relative to #{base}:")
149
+ if ok && !out.to_s.strip.empty?
150
+ parts = out.strip.split(/\s+/)
151
+ left = parts[0].to_i
152
+ right = parts[1].to_i
153
+ # left = commits only in HEAD => local ahead by left
154
+ # right = commits only in origin => local behind by right
155
+ if left.zero? && right.zero?
156
+ say(" - local (HEAD): in sync")
157
+ else
158
+ ahead_emoji = left.zero? ? "✅️" : "🔴"
159
+ say(" - local (HEAD): #{ahead_emoji} ahead by #{left}, behind by #{right}")
160
+ end
161
+ else
162
+ say(" - local (HEAD): no data (branch missing?)")
163
+ end
164
+ end
165
+
166
+ def parse!
167
+ parser = OptionParser.new do |o|
168
+ o.banner = "Usage: kettle-dvcs [options] [ORG] [REPO]"
169
+ o.on("--origin NAME", %w[github gitlab codeberg], "Choose origin forge (default: github)") { |v| @opts[:origin] = v }
170
+ o.on("--protocol NAME", %w[ssh https], "Protocol (default: ssh)") { |v| @opts[:protocol] = v }
171
+ o.on("--github-name NAME", "Remote name for GitHub when not origin (default: gh)") { |v| @opts[:gh_name] = v }
172
+ o.on("--gitlab-name NAME", "Remote name for GitLab (default: gl)") { |v| @opts[:gl_name] = v }
173
+ o.on("--codeberg-name NAME", "Remote name for Codeberg (default: cb)") { |v| @opts[:cb_name] = v }
174
+ o.on("--status", "Fetch remotes and show ahead/behind relative to origin/main") { @opts[:status] = true }
175
+ o.on("--force", "Accept defaults; non-interactive") { @opts[:force] = true }
176
+ o.on("-h", "--help", "Show help") {
177
+ puts o
178
+ Kettle::Dev::ExitAdapter.exit(0)
179
+ }
180
+ end
181
+ rest = parser.parse(@argv)
182
+ @opts[:org] = rest[0] if rest[0]
183
+ @opts[:repo] = rest[1] if rest[1]
184
+
185
+ unless %w[github gitlab codeberg].include?(@opts[:origin])
186
+ abort!("Invalid origin: #{@opts[:origin]}")
187
+ end
188
+ end
189
+
190
+ def ensure_git_adapter!
191
+ unless defined?(Kettle::Dev::GitAdapter)
192
+ abort!("Kettle::Dev::GitAdapter is required and not available")
193
+ end
194
+ Kettle::Dev::GitAdapter.new
195
+ end
196
+
197
+ def remote_names
198
+ {
199
+ origin: "origin",
200
+ github: (@opts[:origin] == "github") ? "origin" : @opts[:gh_name],
201
+ gitlab: (@opts[:origin] == "gitlab") ? "origin" : @opts[:gl_name],
202
+ codeberg: (@opts[:origin] == "codeberg") ? "origin" : @opts[:cb_name],
203
+ all: "all",
204
+ }
205
+ end
206
+
207
+ def forge_urls(org, repo)
208
+ case @opts[:protocol]
209
+ when "ssh"
210
+ {
211
+ github: "git@github.com:#{org}/#{repo}.git",
212
+ gitlab: "git@gitlab.com:#{org}/#{repo}.git",
213
+ codeberg: "git@codeberg.org:#{org}/#{repo}.git",
214
+ }
215
+ else # https
216
+ {
217
+ github: "https://github.com/#{org}/#{repo}.git",
218
+ gitlab: "https://gitlab.com/#{org}/#{repo}.git",
219
+ codeberg: "https://codeberg.org/#{org}/#{repo}.git",
220
+ }
221
+ end
222
+ end
223
+
224
+ def resolve_org_repo(git)
225
+ org = @opts[:org]
226
+ repo = @opts[:repo]
227
+ if org && repo
228
+ return [org, repo]
229
+ end
230
+ # Try to infer from any existing remote url
231
+ urls = git.remotes_with_urls
232
+ sample = urls["origin"] || urls.values.first
233
+ if sample && sample =~ %r{[:/](?<org>[^/]+)/(?<repo>[^/]+?)(?:\.git)?$}
234
+ org ||= Regexp.last_match(:org)
235
+ repo ||= Regexp.last_match(:repo)
236
+ end
237
+ if !org || !repo
238
+ if @opts[:force]
239
+ abort!("ORG and REPO could not be inferred; supply them or ensure an existing remote URL")
240
+ else
241
+ org = prompt("Organization name", default: org)
242
+ repo = prompt("Repository name", default: repo)
243
+ end
244
+ end
245
+ [org, repo]
246
+ end
247
+
248
+ def prompt(label, default: nil)
249
+ return default if @opts[:force]
250
+ print("#{label}#{default ? " [#{default}]" : ""}: ")
251
+ ans = $stdin.gets&.strip
252
+ ans = nil if ans == ""
253
+ ans || default || abort!("#{label} is required")
254
+ end
255
+
256
+ def ensure_remote_alignment!(git, name, url)
257
+ # Validate URL presence to avoid passing nil to Open3
258
+ abort!("Internal error: URL for remote '#{name}' is empty") if url.nil? || url.to_s.strip.empty?
259
+ # We need remote management capabilities via capture to avoid adding adapter methods right now.
260
+ # Fails if GitAdapter is not present as required.
261
+ existing = git.remotes
262
+ if existing.include?(name)
263
+ current = git.remote_url(name)
264
+ if current != url
265
+ sh_git!(git, ["remote", "set-url", name, url])
266
+ end
267
+ else
268
+ # Check if any remote already points to this URL under a different name; rename it
269
+ urls = git.remotes_with_urls
270
+ if (pair = urls.find { |_n, u| u == url })
271
+ old = pair[0]
272
+ sh_git!(git, ["remote", "rename", old, name]) unless old == name
273
+ else
274
+ sh_git!(git, ["remote", "add", name, url])
275
+ end
276
+ end
277
+ end
278
+
279
+ def configure_all_remote!(git, names, urls)
280
+ all = names[:all]
281
+ # Remove existing 'all' to recreate cleanly
282
+ if git.remotes.include?(all)
283
+ sh_git!(git, ["remote", "remove", all])
284
+ end
285
+ # Create with origin fetch URL; we will add multiple pushurls
286
+ origin_url = urls[@opts[:origin].to_sym]
287
+ sh_git!(git, ["remote", "add", all, origin_url])
288
+ # Ensure fetch only from origin (set fetch refspec to match origin's default)
289
+ # We'll reset fetch to +refs/heads/*:refs/remotes/all/* from origin remote
290
+ # Simpler: disable fetch by clearing fetch then add one matching origin
291
+ sh_git!(git, ["config", "--unset-all", "remote.#{all}.fetch"]) # ignore failure
292
+ # Emulate origin default fetch
293
+ sh_git!(git, ["config", "--add", "remote.#{all}.fetch", "+refs/heads/*:refs/remotes/#{all}/*"])
294
+ # Configure push to all forges
295
+ %i[github gitlab codeberg].each do |forge|
296
+ sh_git!(git, ["config", "--add", "remote.#{all}.pushurl", forge_urls_entry(forge, urls)])
297
+ end
298
+ end
299
+
300
+ def forge_urls_entry(forge, urls)
301
+ urls[forge]
302
+ end
303
+
304
+ def sh_git!(git, args)
305
+ # Ensure no nil sneaks into the argv to Open3 (TypeError avoidance)
306
+ if args.any? { |a| a.nil? || (a.respond_to?(:strip) && a.strip.empty?) }
307
+ abort!("Internal error: Attempted to run 'git #{args.inspect}' with an empty argument")
308
+ end
309
+ out, ok = git.capture(args)
310
+ unless ok
311
+ abort!("git #{args.join(" ")} failed: #{out}")
312
+ end
313
+ out
314
+ end
315
+
316
+ def show_remotes!(git)
317
+ out, ok = git.capture(["remote", "-v"])
318
+ if ok && !out.to_s.strip.empty?
319
+ say("\nCurrent remotes (git remote -v):")
320
+ puts out
321
+ else
322
+ # Fallback: print the fetch URLs mapping
323
+ say("\nCurrent remotes (name => fetch URL):")
324
+ git.remotes_with_urls.each do |name, url|
325
+ puts " #{name}\t#{url} (fetch)"
326
+ end
327
+ end
328
+ end
329
+
330
+ # Try fetching from each configured forge remote. Returns a hash of forge=>boolean
331
+ def attempt_fetches!(git, names)
332
+ results = {}
333
+ {
334
+ github: names[:github],
335
+ gitlab: names[:gitlab],
336
+ codeberg: names[:codeberg],
337
+ }.each do |forge, remote_name|
338
+ next unless remote_name
339
+ ok = git.fetch(remote_name)
340
+ results[forge] = !!ok
341
+ say("Fetched from #{forge} (remote: #{remote_name}) => #{ok ? "OK" : "FAILED"}")
342
+ end
343
+ results
344
+ end
345
+
346
+ # Update README federation disclosure based on fetch results
347
+ def update_readme_federation_status!(org, repo, results)
348
+ readme_path = File.join(Dir.pwd, "README.md")
349
+ return unless File.exist?(readme_path)
350
+ content = File.read(readme_path)
351
+ # Determine if all succeeded
352
+ forges = [:github, :gitlab, :codeberg]
353
+ all_ok = forges.all? { |f| results[f] }
354
+ new_content = content.dup
355
+ summary_line_with_cs = /<summary>Find this repo on other forges \(Coming soon!\)<\/summary>/
356
+ summary_line_no_cs = "<summary>Find this repo on other forges</summary>"
357
+ if all_ok
358
+ new_content.gsub!(summary_line_with_cs, summary_line_no_cs)
359
+ else
360
+ # Ensure the line contains (Coming soon!) so readers know it's partial
361
+ unless content =~ summary_line_with_cs
362
+ new_content.gsub!("<summary>Find this repo on other forges</summary>", "<summary>Find this repo on other forges (Coming soon!)</summary>")
363
+ end
364
+ end
365
+ if new_content != content
366
+ File.write(readme_path, new_content)
367
+ say("Updated README federation summary to reflect current forge status")
368
+ end
369
+ # Print import links for any failed forge
370
+ unless all_ok
371
+ say("\nSome forges are not yet available. Use these import links to create mirrors:")
372
+ import_links = {
373
+ github: "https://github.com/new/import",
374
+ gitlab: "https://gitlab.com/projects/new#import_project",
375
+ codeberg: "https://codeberg.org/repo/create?scm=git&name=#{repo}&migration=true",
376
+ }
377
+ [:github, :gitlab, :codeberg].each do |forge|
378
+ next if results[forge]
379
+ say(" - #{forge.capitalize} import: #{import_links[forge]}")
380
+ end
381
+ end
382
+ rescue StandardError => e
383
+ warn("Failed to update README federation status: #{e.message}")
384
+ end
385
+
386
+ def say(msg)
387
+ puts msg
388
+ end
389
+
390
+ def abort!(msg)
391
+ warn(msg)
392
+ Kettle::Dev::ExitAdapter.exit(1)
393
+ end
394
+ end
395
+ end
396
+ end
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.1.1"
9
+ VERSION = "1.1.2"
10
10
 
11
11
  module_function
12
12
 
data/lib/kettle/dev.rb CHANGED
@@ -15,6 +15,7 @@ module Kettle
15
15
  autoload :CIHelpers, "kettle/dev/ci_helpers"
16
16
  autoload :CIMonitor, "kettle/dev/ci_monitor"
17
17
  autoload :CommitMsg, "kettle/dev/commit_msg"
18
+ autoload :DvcsCLI, "kettle/dev/dvcs_cli"
18
19
  autoload :ExitAdapter, "kettle/dev/exit_adapter"
19
20
  autoload :GemSpecReader, "kettle/dev/gem_spec_reader"
20
21
  autoload :GitAdapter, "kettle/dev/git_adapter"
@@ -0,0 +1,8 @@
1
+ module Kettle
2
+ module Dev
3
+ class DvcsCLI
4
+ def initialize: (Array[String] argv) -> void
5
+ def run!: () -> Integer
6
+ end
7
+ end
8
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -198,6 +198,7 @@ executables:
198
198
  - kettle-changelog
199
199
  - kettle-commit-msg
200
200
  - kettle-dev-setup
201
+ - kettle-dvcs
201
202
  - kettle-readme-backers
202
203
  - kettle-release
203
204
  extensions: []
@@ -277,6 +278,7 @@ files:
277
278
  - exe/kettle-changelog
278
279
  - exe/kettle-commit-msg
279
280
  - exe/kettle-dev-setup
281
+ - exe/kettle-dvcs
280
282
  - exe/kettle-readme-backers
281
283
  - exe/kettle-release
282
284
  - gemfiles/modular/coverage.gemfile
@@ -293,6 +295,7 @@ files:
293
295
  - lib/kettle/dev/ci_helpers.rb
294
296
  - lib/kettle/dev/ci_monitor.rb
295
297
  - lib/kettle/dev/commit_msg.rb
298
+ - lib/kettle/dev/dvcs_cli.rb
296
299
  - lib/kettle/dev/exit_adapter.rb
297
300
  - lib/kettle/dev/gem_spec_reader.rb
298
301
  - lib/kettle/dev/git_adapter.rb
@@ -324,6 +327,7 @@ files:
324
327
  - sig/kettle/dev/ci_helpers.rbs
325
328
  - sig/kettle/dev/ci_monitor.rbs
326
329
  - sig/kettle/dev/commit_msg.rbs
330
+ - sig/kettle/dev/dvcscli.rbs
327
331
  - sig/kettle/dev/exit_adapter.rbs
328
332
  - sig/kettle/dev/gem_spec_reader.rbs
329
333
  - sig/kettle/dev/git_adapter.rbs
@@ -343,10 +347,10 @@ licenses:
343
347
  - MIT
344
348
  metadata:
345
349
  homepage_uri: https://kettle-dev.galtzo.com/
346
- source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.1.1
347
- changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.1/CHANGELOG.md
350
+ source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.1.2
351
+ changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.2/CHANGELOG.md
348
352
  bug_tracker_uri: https://github.com/kettle-rb/kettle-dev/issues
349
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.1.1
353
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.1.2
350
354
  funding_uri: https://github.com/sponsors/pboling
351
355
  wiki_uri: https://github.com/kettle-rb/kettle-dev/wiki
352
356
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file