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 +4 -4
- checksums.yaml.gz.sig +2 -3
- data/.gitignore +1 -0
- data/.gitlab-ci.yml.example +14 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +35 -1
- data/CONTRIBUTING.md +4 -3
- data/README.md +46 -1
- data/README.md.example +1 -1
- data/Rakefile.example +1 -1
- data/exe/kettle-dvcs +72 -0
- data/kettle-dev.gemspec.example +1 -1
- data/lib/kettle/dev/dvcs_cli.rb +396 -0
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +3 -3
- data/sig/kettle/dev/dvcscli.rbs +8 -0
- data.tar.gz.sig +0 -0
- metadata +8 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3eb6a72bd85bbfde7317e4001bed71b6f06c23e8094ec3d7028135ea787cb8a
|
4
|
+
data.tar.gz: f8a6c719e95549c12739aead51912b68e49713cf1c12a2aa134a61404d82825c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe84cb073f601bc8b2c0e50979d229d81d5fa45b3f64455837d17b624fbd2ac3d75c2180b1d87df306e4d7b6efbda20a9d5c94926783fc06091b3b21f61aa368
|
7
|
+
data.tar.gz: 3a0b31c5be00d77c9b8c9073413caa80a9023f1291b5dd7c98c2a323e712e0756904bdb70fd00e319ca28021e44bdcf16f5506c9e25e18b8aa8c006f0102d254
|
checksums.yaml.gz.sig
CHANGED
@@ -1,3 +1,2 @@
|
|
1
|
-
|
2
|
-
�
|
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��D�k�Q��T����k���0���p�:����ޜ}�Z��yi_|[�Х�*'
|
data/.gitignore
CHANGED
data/.gitlab-ci.yml.example
CHANGED
@@ -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
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.
|
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
|
93
|
-
-
|
94
|
-
-
|
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.
|
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.
|
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
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
|
data/kettle-dev.gemspec.example
CHANGED
@@ -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")
|
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
|
data/lib/kettle/dev/version.rb
CHANGED
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
|
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.
|
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.
|
347
|
-
changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.
|
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.
|
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
|