kettle-dev 1.1.1 → 1.1.3

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: c3eb6a72bd85bbfde7317e4001bed71b6f06c23e8094ec3d7028135ea787cb8a
4
+ data.tar.gz: f8a6c719e95549c12739aead51912b68e49713cf1c12a2aa134a61404d82825c
5
5
  SHA512:
6
- metadata.gz: ae3e344c567670c9817d9724bddbe08f926564e906843ae02bb95110bb2fce3ae3769511a5106e73bcb0abf93c2a1a64cf876c5d82bd6052adaa6046cd5eeecb
7
- data.tar.gz: 9ccad75834eb783f912660a214416e3fd53d8229955a69026c013fbcb4ddfd6b43bc211ace28e7a0e7dec08f41a2c3463e862515f7fae0e9059111c49c478d74
6
+ metadata.gz: fe84cb073f601bc8b2c0e50979d229d81d5fa45b3f64455837d17b624fbd2ac3d75c2180b1d87df306e4d7b6efbda20a9d5c94926783fc06091b3b21f61aa368
7
+ data.tar.gz: 3a0b31c5be00d77c9b8c9073413caa80a9023f1291b5dd7c98c2a323e712e0756904bdb70fd00e319ca28021e44bdcf16f5506c9e25e18b8aa8c006f0102d254
checksums.yaml.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- �kj��~�Y��/���
2
- E���RO7VO$����<a>q��o_oLM�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
+ \ \o��ְ{vnq��Ѱ��ޙfl�P{���Q��J��Qj+`&3���=�Fw��7�Ӯ�(`���jMYl�q14����M�s�2�����)�9��|�<7S^[�D�Z�I,V�� z�� �ņ��d���[�H���h�������ҷﮀLo��_P�lj0��d!8!�Z�Bk�>�+�o���X�<4�!u�ֺx����<�:|��egoFx{��R���ԓ����a+�:�c�ӃoK�-8�+�T3��𑰤�s�8�yD�C����q�@p�X��2��Zq$eG��
2
+ ���� 8Q��Dk�Q ��T����k���0���p�:����ޜ}�Z��yi_|[�Х�*'
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,36 @@ Please file a bug if you notice a violation of semantic versioning.
24
24
  ### Fixed
25
25
  ### Security
26
26
 
27
+ ## [1.1.3] - 2025-09-02
28
+ - TAG: [v1.1.3][1.1.3t]
29
+ - COVERAGE: 97.14% -- 2857/2941 lines in 22 files
30
+ - BRANCH COVERAGE: 82.29% -- 1194/1451 branches in 22 files
31
+ - 76.22% documented
32
+ ### Changed
33
+ - URL for migrating repo to CodeBerg:
34
+ - https://codeberg.org/repo/migrate
35
+ ### Fixed
36
+ - Stop double defining DEBUGGING constant
37
+
38
+ ## [1.1.2] - 2025-09-02
39
+ - TAG: [v1.1.2][1.1.2t]
40
+ - COVERAGE: 97.14% -- 2858/2942 lines in 22 files
41
+ - BRANCH COVERAGE: 82.29% -- 1194/1451 branches in 22 files
42
+ - 76.76% documented
43
+ ### Added
44
+ - .gitlab-ci.yml documentation (in example)
45
+ - kettle-dvcs script for setting up DVCS, and checking status of remotes
46
+ - https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
47
+ - kettle-dvcs --status: prefix "ahead by N" with ✅️ when N==0, and 🔴 when N>0
48
+ - kettle-dvcs --status: also prints a Local status section comparing local HEAD to origin/<branch>, and keeps origin visible via that section
49
+ - Document kettle-dvcs CLI in README (usage, options, examples)
50
+ - RBS types for Kettle::Dev::DvcsCLI and inline YARD docs on CLI
51
+ - Specs for DvcsCLI covering remote normalization, fetch outcomes, and README updates
52
+ ### Changed
53
+ - major spec refactoring
54
+ ### Fixed
55
+ - (linting) rspec-pending_for 0.0.17+ (example gemspec)
56
+
27
57
  ## [1.1.1] - 2025-09-02
28
58
  - TAG: [v1.1.1][1.1.1t]
29
59
  - COVERAGE: 97.04% -- 2655/2736 lines in 21 files
@@ -428,7 +458,11 @@ Please file a bug if you notice a violation of semantic versioning.
428
458
  - Selecting will run the selected workflow via `act`
429
459
  - This may move to its own gem in the future.
430
460
 
431
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.1...HEAD
461
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.3...HEAD
462
+ [1.1.3]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.2...v1.1.3
463
+ [1.1.3t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.3
464
+ [1.1.2]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.1...v1.1.2
465
+ [1.1.2t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.2
432
466
  [1.1.1]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.0...v1.1.1
433
467
  [1.1.1t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.1
434
468
  [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.941-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.941-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.3 - 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
+ FORGE_MIGRATION_TOOLS = {
41
+ github: "https://github.com/new/import",
42
+ gitlab: "https://gitlab.com/projects/new#import_project",
43
+ codeberg: "https://codeberg.org/repo/migrate",
44
+ }.freeze
45
+
46
+ # Create the CLI with argv-like arguments
47
+ # @param argv [Array<String>] the command-line arguments (without program name)
48
+ def initialize(argv)
49
+ @argv = argv
50
+ @opts = DEFAULTS.dup
51
+ end
52
+
53
+ # Execute the CLI command.
54
+ # Aligns remotes, configures the `all` remote, prints remotes, attempts fetches,
55
+ # and updates README federation status accordingly.
56
+ # @return [Integer] exit status code (0 on success; may abort with non-zero)
57
+ def run!
58
+ parse!
59
+ git = ensure_git_adapter!
60
+
61
+ if @opts[:status]
62
+ # Status mode: no working tree mutation beyond fetch. Don't require clean tree.
63
+ _, _ = resolve_org_repo(git)
64
+ names = remote_names
65
+ branch = detect_default_branch!(git)
66
+ say("Fetching all remotes for status...")
67
+ # Fetch origin first to ensure origin/<branch> is up to date
68
+ git.fetch(names[:origin]) if names[:origin]
69
+ %i[github gitlab codeberg].each do |forge|
70
+ r = names[forge]
71
+ next unless r && r != names[:origin]
72
+ git.fetch(r)
73
+ end
74
+ show_status!(git, names, branch)
75
+ show_local_vs_origin!(git, branch)
76
+ return 0
77
+ end
78
+
79
+ abort!("Working tree is not clean; commit or stash changes before proceeding") unless git.clean?
80
+
81
+ org, repo = resolve_org_repo(git)
82
+
83
+ names = remote_names
84
+ urls = forge_urls(org, repo)
85
+
86
+ # Ensure remotes exist and have desired names/urls
87
+ ensure_remote_alignment!(git, names[:origin], urls[@opts[:origin].to_sym])
88
+ ensure_remote_alignment!(git, names[:github], urls[:github]) if names[:github] && names[:github] != names[:origin]
89
+ ensure_remote_alignment!(git, names[:gitlab], urls[:gitlab]) if names[:gitlab]
90
+ ensure_remote_alignment!(git, names[:codeberg], urls[:codeberg]) if names[:codeberg]
91
+
92
+ # Configure "all" remote: fetch only from origin, push to all three
93
+ configure_all_remote!(git, names, urls)
94
+
95
+ say("Remotes normalized. Origin: #{names[:origin]} (#{urls[@opts[:origin].to_sym]})")
96
+ show_remotes!(git)
97
+ fetch_results = attempt_fetches!(git, names)
98
+ update_readme_federation_status!(org, repo, fetch_results)
99
+ 0
100
+ end
101
+
102
+ private
103
+
104
+ # Determine default branch to compare against. Prefer 'main', fallback to 'master'.
105
+ # Uses origin to check existence.
106
+ def detect_default_branch!(git)
107
+ _out, ok = git.capture(["rev-parse", "--verify", "origin/main"])
108
+ return "main" if ok
109
+ _out2, ok2 = git.capture(["rev-parse", "--verify", "origin/master"])
110
+ return "master" if ok2
111
+ # Default to main if neither verifies
112
+ "main"
113
+ end
114
+
115
+ # Show ahead/behind status for each configured forge remote relative to origin/<branch>
116
+ def show_status!(git, names, branch)
117
+ base = "origin/#{branch}"
118
+ say("\nRemote status relative to #{base}:")
119
+ existing = Array(git.remotes)
120
+ {
121
+ github: names[:github],
122
+ gitlab: names[:gitlab],
123
+ codeberg: names[:codeberg],
124
+ }.each do |forge, remote|
125
+ next unless remote
126
+ next if remote == names[:origin]
127
+ next unless existing.include?(remote)
128
+ ref = "#{remote}/#{branch}"
129
+ out, ok = git.capture(["rev-list", "--left-right", "--count", "#{base}...#{ref}"])
130
+ if ok && !out.to_s.strip.empty?
131
+ parts = out.strip.split(/\s+/)
132
+ left = parts[0].to_i
133
+ right = parts[1].to_i
134
+ # left = commits only in base (origin) => remote is behind by left
135
+ # right = commits only in remote => remote is ahead by right
136
+ if left.zero? && right.zero?
137
+ say(" - #{forge} (#{remote}): in sync")
138
+ else
139
+ ahead_emoji = right.zero? ? "✅️" : "🔴"
140
+ say(" - #{forge} (#{remote}): #{ahead_emoji} ahead by #{right}, behind by #{left}")
141
+ end
142
+ else
143
+ say(" - #{forge} (#{remote}): no data (branch missing?)")
144
+ end
145
+ end
146
+ end
147
+
148
+ # Show local working copy status relative to origin/<branch>
149
+ def show_local_vs_origin!(git, branch)
150
+ base = "origin/#{branch}"
151
+ # Compare local HEAD to origin/<branch>
152
+ out, ok = git.capture(["rev-list", "--left-right", "--count", "HEAD...#{base}"])
153
+ say("\nLocal status relative to #{base}:")
154
+ if ok && !out.to_s.strip.empty?
155
+ parts = out.strip.split(/\s+/)
156
+ left = parts[0].to_i
157
+ right = parts[1].to_i
158
+ # left = commits only in HEAD => local ahead by left
159
+ # right = commits only in origin => local behind by right
160
+ if left.zero? && right.zero?
161
+ say(" - local (HEAD): in sync")
162
+ else
163
+ ahead_emoji = left.zero? ? "✅️" : "🔴"
164
+ say(" - local (HEAD): #{ahead_emoji} ahead by #{left}, behind by #{right}")
165
+ end
166
+ else
167
+ say(" - local (HEAD): no data (branch missing?)")
168
+ end
169
+ end
170
+
171
+ def parse!
172
+ parser = OptionParser.new do |o|
173
+ o.banner = "Usage: kettle-dvcs [options] [ORG] [REPO]"
174
+ o.on("--origin NAME", %w[github gitlab codeberg], "Choose origin forge (default: github)") { |v| @opts[:origin] = v }
175
+ o.on("--protocol NAME", %w[ssh https], "Protocol (default: ssh)") { |v| @opts[:protocol] = v }
176
+ o.on("--github-name NAME", "Remote name for GitHub when not origin (default: gh)") { |v| @opts[:gh_name] = v }
177
+ o.on("--gitlab-name NAME", "Remote name for GitLab (default: gl)") { |v| @opts[:gl_name] = v }
178
+ o.on("--codeberg-name NAME", "Remote name for Codeberg (default: cb)") { |v| @opts[:cb_name] = v }
179
+ o.on("--status", "Fetch remotes and show ahead/behind relative to origin/main") { @opts[:status] = true }
180
+ o.on("--force", "Accept defaults; non-interactive") { @opts[:force] = true }
181
+ o.on("-h", "--help", "Show help") {
182
+ puts o
183
+ Kettle::Dev::ExitAdapter.exit(0)
184
+ }
185
+ end
186
+ rest = parser.parse(@argv)
187
+ @opts[:org] = rest[0] if rest[0]
188
+ @opts[:repo] = rest[1] if rest[1]
189
+
190
+ unless %w[github gitlab codeberg].include?(@opts[:origin])
191
+ abort!("Invalid origin: #{@opts[:origin]}")
192
+ end
193
+ end
194
+
195
+ def ensure_git_adapter!
196
+ unless defined?(Kettle::Dev::GitAdapter)
197
+ abort!("Kettle::Dev::GitAdapter is required and not available")
198
+ end
199
+ Kettle::Dev::GitAdapter.new
200
+ end
201
+
202
+ def remote_names
203
+ {
204
+ origin: "origin",
205
+ github: (@opts[:origin] == "github") ? "origin" : @opts[:gh_name],
206
+ gitlab: (@opts[:origin] == "gitlab") ? "origin" : @opts[:gl_name],
207
+ codeberg: (@opts[:origin] == "codeberg") ? "origin" : @opts[:cb_name],
208
+ all: "all",
209
+ }
210
+ end
211
+
212
+ def forge_urls(org, repo)
213
+ case @opts[:protocol]
214
+ when "ssh"
215
+ {
216
+ github: "git@github.com:#{org}/#{repo}.git",
217
+ gitlab: "git@gitlab.com:#{org}/#{repo}.git",
218
+ codeberg: "git@codeberg.org:#{org}/#{repo}.git",
219
+ }
220
+ else # https
221
+ {
222
+ github: "https://github.com/#{org}/#{repo}.git",
223
+ gitlab: "https://gitlab.com/#{org}/#{repo}.git",
224
+ codeberg: "https://codeberg.org/#{org}/#{repo}.git",
225
+ }
226
+ end
227
+ end
228
+
229
+ def resolve_org_repo(git)
230
+ org = @opts[:org]
231
+ repo = @opts[:repo]
232
+ if org && repo
233
+ return [org, repo]
234
+ end
235
+ # Try to infer from any existing remote url
236
+ urls = git.remotes_with_urls
237
+ sample = urls["origin"] || urls.values.first
238
+ if sample && sample =~ %r{[:/](?<org>[^/]+)/(?<repo>[^/]+?)(?:\.git)?$}
239
+ org ||= Regexp.last_match(:org)
240
+ repo ||= Regexp.last_match(:repo)
241
+ end
242
+ if !org || !repo
243
+ if @opts[:force]
244
+ abort!("ORG and REPO could not be inferred; supply them or ensure an existing remote URL")
245
+ else
246
+ org = prompt("Organization name", default: org)
247
+ repo = prompt("Repository name", default: repo)
248
+ end
249
+ end
250
+ [org, repo]
251
+ end
252
+
253
+ def prompt(label, default: nil)
254
+ return default if @opts[:force]
255
+ print("#{label}#{default ? " [#{default}]" : ""}: ")
256
+ ans = $stdin.gets&.strip
257
+ ans = nil if ans == ""
258
+ ans || default || abort!("#{label} is required")
259
+ end
260
+
261
+ def ensure_remote_alignment!(git, name, url)
262
+ # Validate URL presence to avoid passing nil to Open3
263
+ abort!("Internal error: URL for remote '#{name}' is empty") if url.nil? || url.to_s.strip.empty?
264
+ # We need remote management capabilities via capture to avoid adding adapter methods right now.
265
+ # Fails if GitAdapter is not present as required.
266
+ existing = git.remotes
267
+ if existing.include?(name)
268
+ current = git.remote_url(name)
269
+ if current != url
270
+ sh_git!(git, ["remote", "set-url", name, url])
271
+ end
272
+ else
273
+ # Check if any remote already points to this URL under a different name; rename it
274
+ urls = git.remotes_with_urls
275
+ if (pair = urls.find { |_n, u| u == url })
276
+ old = pair[0]
277
+ sh_git!(git, ["remote", "rename", old, name]) unless old == name
278
+ else
279
+ sh_git!(git, ["remote", "add", name, url])
280
+ end
281
+ end
282
+ end
283
+
284
+ def configure_all_remote!(git, names, urls)
285
+ all = names[:all]
286
+ # Remove existing 'all' to recreate cleanly
287
+ if git.remotes.include?(all)
288
+ sh_git!(git, ["remote", "remove", all])
289
+ end
290
+ # Create with origin fetch URL; we will add multiple pushurls
291
+ origin_url = urls[@opts[:origin].to_sym]
292
+ sh_git!(git, ["remote", "add", all, origin_url])
293
+ # Ensure fetch only from origin (set fetch refspec to match origin's default)
294
+ # We'll reset fetch to +refs/heads/*:refs/remotes/all/* from origin remote
295
+ # Simpler: disable fetch by clearing fetch then add one matching origin
296
+ sh_git!(git, ["config", "--unset-all", "remote.#{all}.fetch"]) # ignore failure
297
+ # Emulate origin default fetch
298
+ sh_git!(git, ["config", "--add", "remote.#{all}.fetch", "+refs/heads/*:refs/remotes/#{all}/*"])
299
+ # Configure push to all forges
300
+ %i[github gitlab codeberg].each do |forge|
301
+ sh_git!(git, ["config", "--add", "remote.#{all}.pushurl", forge_urls_entry(forge, urls)])
302
+ end
303
+ end
304
+
305
+ def forge_urls_entry(forge, urls)
306
+ urls[forge]
307
+ end
308
+
309
+ def sh_git!(git, args)
310
+ # Ensure no nil sneaks into the argv to Open3 (TypeError avoidance)
311
+ if args.any? { |a| a.nil? || (a.respond_to?(:strip) && a.strip.empty?) }
312
+ abort!("Internal error: Attempted to run 'git #{args.inspect}' with an empty argument")
313
+ end
314
+ out, ok = git.capture(args)
315
+ unless ok
316
+ abort!("git #{args.join(" ")} failed: #{out}")
317
+ end
318
+ out
319
+ end
320
+
321
+ def show_remotes!(git)
322
+ out, ok = git.capture(["remote", "-v"])
323
+ if ok && !out.to_s.strip.empty?
324
+ say("\nCurrent remotes (git remote -v):")
325
+ puts out
326
+ else
327
+ # Fallback: print the fetch URLs mapping
328
+ say("\nCurrent remotes (name => fetch URL):")
329
+ git.remotes_with_urls.each do |name, url|
330
+ puts " #{name}\t#{url} (fetch)"
331
+ end
332
+ end
333
+ end
334
+
335
+ # Try fetching from each configured forge remote. Returns a hash of forge=>boolean
336
+ def attempt_fetches!(git, names)
337
+ results = {}
338
+ {
339
+ github: names[:github],
340
+ gitlab: names[:gitlab],
341
+ codeberg: names[:codeberg],
342
+ }.each do |forge, remote_name|
343
+ next unless remote_name
344
+ ok = git.fetch(remote_name)
345
+ results[forge] = !!ok
346
+ say("Fetched from #{forge} (remote: #{remote_name}) => #{ok ? "OK" : "FAILED"}")
347
+ end
348
+ results
349
+ end
350
+
351
+ # Update README federation disclosure based on fetch results
352
+ def update_readme_federation_status!(org, repo, results)
353
+ readme_path = File.join(Dir.pwd, "README.md")
354
+ return unless File.exist?(readme_path)
355
+ content = File.read(readme_path)
356
+ # Determine if all succeeded
357
+ forges = [:github, :gitlab, :codeberg]
358
+ all_ok = forges.all? { |f| results[f] }
359
+ new_content = content.dup
360
+ summary_line_with_cs = /<summary>Find this repo on other forges \(Coming soon!\)<\/summary>/
361
+ summary_line_no_cs = "<summary>Find this repo on other forges</summary>"
362
+ if all_ok
363
+ new_content.gsub!(summary_line_with_cs, summary_line_no_cs)
364
+ else
365
+ # Ensure the line contains (Coming soon!) so readers know it's partial
366
+ unless content =~ summary_line_with_cs
367
+ new_content.gsub!("<summary>Find this repo on other forges</summary>", "<summary>Find this repo on other forges (Coming soon!)</summary>")
368
+ end
369
+ end
370
+ if new_content != content
371
+ File.write(readme_path, new_content)
372
+ say("Updated README federation summary to reflect current forge status")
373
+ end
374
+ # Print import links for any failed forge
375
+ unless all_ok
376
+ say("\nSome forges are not yet available. Use these import links to create mirrors:")
377
+ [:github, :gitlab, :codeberg].each do |forge|
378
+ next if results[forge]
379
+ say(" - #{forge.capitalize} import: #{FORGE_MIGRATION_TOOLS[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.3"
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"
@@ -37,10 +38,9 @@ module Kettle
37
38
  class Error < StandardError; end
38
39
 
39
40
  # Whether debug logging is enabled for kettle-dev internals.
41
+ # KETTLE_DEV_DEBUG overrides DEBUG.
40
42
  # @return [Boolean]
41
- DEBUGGING = ENV.fetch("DEBUG", "false").casecmp("true").zero?
42
- # Backwards-compat for kettle-dev specific debug variable
43
- DEBUGGING ||= ENV.fetch("KETTLE_DEV_DEBUG", "false").casecmp("true").zero?
43
+ DEBUGGING = ENV.fetch("KETTLE_DEV_DEBUG", ENV.fetch("DEBUG", "false")).casecmp("true").zero?
44
44
  # Whether we are running on CI.
45
45
  # @return [Boolean]
46
46
  IS_CI = ENV.fetch("CI", "false").casecmp("true") == 0
@@ -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.3
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.3
351
+ changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.3/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.3
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