kk-git 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/kk-git +154 -84
- data/lib/kk/git/commit_message.rb +55 -55
- data/lib/kk/git/git_ops.rb +278 -0
- data/lib/kk/git/rake_tasks.rb +37 -79
- data/lib/kk/git/version.rb +1 -1
- data/lib/kk/git.rb +1 -0
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6db1717b25840058e422b1b8955fb33222666cffdb8692e366e76821b93ea6a9
|
|
4
|
+
data.tar.gz: 3f2c67d3187d973923bc26b49fbc3d2e2bc5fe83b47738bc86d1ca97999ecdff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53560be756360a59510ad02eb3888936b9a63cb9285c5386c765c3173bc320bdbe41ebc8c9c2d984ff511729b556279061d5369ed448050b8642ec64eea99687
|
|
7
|
+
data.tar.gz: 414075a7880e76253a3ba7fda59a93731121ce4979c86ee1ba0629c42c4e899bdae70e3ddd99fdb0252f770c6c16a5690fee805dbbefec17177d3f9caf2b2595
|
data/exe/kk-git
CHANGED
|
@@ -10,104 +10,174 @@ rescue LoadError
|
|
|
10
10
|
require_relative '../lib/kk/git'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
options[:format] = v.to_s.downcase.to_sym
|
|
13
|
+
def to_utf8(str)
|
|
14
|
+
return nil if str.nil?
|
|
15
|
+
|
|
16
|
+
s = str.to_s.dup
|
|
17
|
+
s.force_encoding(Encoding::UTF_8)
|
|
18
|
+
s.scrub('�')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_commit_message!(argv)
|
|
22
|
+
options = {
|
|
23
|
+
mode: :staged,
|
|
24
|
+
format: :text,
|
|
25
|
+
include_body: true,
|
|
26
|
+
repo_dir: '.',
|
|
27
|
+
type: nil,
|
|
28
|
+
scope: nil,
|
|
29
|
+
subject: nil,
|
|
30
|
+
detect_breaking: true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parser = OptionParser.new do |opts|
|
|
34
|
+
opts.banner = 'Usage: kk-git commit-message [options]'
|
|
35
|
+
opts.on('--repo DIR', 'Git repo directory (default: current dir)') { |v| options[:repo_dir] = v }
|
|
36
|
+
opts.on('--staged', 'Use staged changes only (default)') { options[:mode] = :staged }
|
|
37
|
+
opts.on('--worktree', 'Use working-tree changes only (includes untracked)') { options[:mode] = :worktree }
|
|
38
|
+
opts.on('--all', 'Combine staged + working-tree changes') { options[:mode] = :all }
|
|
39
|
+
opts.on('--[no-]body', 'Include body for multi-file changes (default: yes)') { |v| options[:include_body] = v }
|
|
40
|
+
opts.on('--type TYPE', 'Override inferred type') { |v| options[:type] = v }
|
|
41
|
+
opts.on('--scope SCOPE', 'Override inferred scope') { |v| options[:scope] = v }
|
|
42
|
+
opts.on('--subject SUBJECT', 'Override inferred subject') { |v| options[:subject] = v }
|
|
43
|
+
opts.on('--[no-]detect-breaking', 'Detect BREAKING markers (default: yes)') { |v| options[:detect_breaking] = v }
|
|
44
|
+
opts.on('--format FORMAT', 'Output format: text/json (default: text)') { |v| options[:format] = v.to_s.downcase.to_sym }
|
|
45
|
+
opts.on('-h', '--help', 'Show help') { puts opts; exit 0 }
|
|
46
|
+
end
|
|
47
|
+
parser.parse!(argv)
|
|
48
|
+
|
|
49
|
+
options[:type] = to_utf8(options[:type])
|
|
50
|
+
options[:scope] = to_utf8(options[:scope])
|
|
51
|
+
options[:subject] = to_utf8(options[:subject])
|
|
52
|
+
|
|
53
|
+
result =
|
|
54
|
+
if options[:format] == :json
|
|
55
|
+
KKGit::CommitMessage.generate_hash(
|
|
56
|
+
repo_dir: options[:repo_dir],
|
|
57
|
+
mode: options[:mode],
|
|
58
|
+
include_body: options[:include_body],
|
|
59
|
+
type_override: options[:type],
|
|
60
|
+
scope_override: options[:scope],
|
|
61
|
+
subject_override: options[:subject],
|
|
62
|
+
detect_breaking: options[:detect_breaking]
|
|
63
|
+
)
|
|
64
|
+
else
|
|
65
|
+
KKGit::CommitMessage.generate(
|
|
66
|
+
repo_dir: options[:repo_dir],
|
|
67
|
+
mode: options[:mode],
|
|
68
|
+
include_body: options[:include_body],
|
|
69
|
+
type_override: options[:type],
|
|
70
|
+
scope_override: options[:scope],
|
|
71
|
+
subject_override: options[:subject],
|
|
72
|
+
detect_breaking: options[:detect_breaking]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if result.nil? || (result.is_a?(Hash) && result[:empty])
|
|
77
|
+
exit 1
|
|
45
78
|
end
|
|
46
79
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
puts(options[:format] == :json ? JSON.pretty_generate(result) : result)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_status!(argv)
|
|
84
|
+
options = { format: :text }
|
|
85
|
+
parser = OptionParser.new do |opts|
|
|
86
|
+
opts.banner = 'Usage: kk-git status [options]'
|
|
87
|
+
opts.on('--format FORMAT', 'Output format: text/json (default: text)') { |v| options[:format] = v.to_s.downcase.to_sym }
|
|
88
|
+
opts.on('-h', '--help', 'Show help') { puts opts; exit 0 }
|
|
50
89
|
end
|
|
90
|
+
parser.parse!(argv)
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
hash = KKGit::GitOps.status_hash
|
|
93
|
+
if options[:format] == :json
|
|
94
|
+
puts JSON.pretty_generate(hash)
|
|
95
|
+
else
|
|
96
|
+
puts "Branch: #{hash[:branch]} (#{hash[:remote]})"
|
|
97
|
+
puts "Working tree: #{hash[:clean] ? 'clean' : 'dirty'}"
|
|
98
|
+
puts "Ahead: #{hash[:ahead]}, Behind: #{hash[:behind]}"
|
|
99
|
+
puts "Upstream: #{hash[:upstream_configured] ? 'configured' : 'not set'}"
|
|
100
|
+
puts 'Detached HEAD: yes' if hash[:detached]
|
|
55
101
|
end
|
|
56
102
|
end
|
|
57
103
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
104
|
+
def run_sync!(argv)
|
|
105
|
+
parser = OptionParser.new do |opts|
|
|
106
|
+
opts.banner = 'Usage: kk-git sync [options]'
|
|
107
|
+
opts.on('-h', '--help', 'Show help') { puts opts; exit 0 }
|
|
108
|
+
end
|
|
109
|
+
parser.parse!(argv)
|
|
63
110
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
111
|
+
remote = KKGit::GitOps.remote
|
|
112
|
+
branch = KKGit::GitOps.branch
|
|
113
|
+
s = KKGit::GitOps.status(remote: remote, branch: branch)
|
|
114
|
+
|
|
115
|
+
if s.needs_sync?
|
|
116
|
+
KKGit::GitOps.sync_with_remote!(remote, branch)
|
|
117
|
+
else
|
|
118
|
+
puts 'Already in sync with remote'
|
|
119
|
+
end
|
|
68
120
|
end
|
|
69
121
|
|
|
70
|
-
|
|
122
|
+
def run_push!(argv)
|
|
123
|
+
parser = OptionParser.new do |opts|
|
|
124
|
+
opts.banner = 'Usage: kk-git push [options]'
|
|
125
|
+
opts.on('-h', '--help', 'Show help') { puts opts; exit 0 }
|
|
126
|
+
end
|
|
127
|
+
parser.parse!(argv)
|
|
71
128
|
|
|
72
|
-
|
|
73
|
-
return nil if str.nil?
|
|
74
|
-
s = str.to_s.dup
|
|
75
|
-
# ARGV 在不同 locale 下可能被标记为 ASCII-8BIT/US-ASCII,但字节本身通常是 UTF-8。
|
|
76
|
-
# 这里优先按 UTF-8 解释字节,并对非法序列做 scrub。
|
|
77
|
-
s.force_encoding(Encoding::UTF_8)
|
|
78
|
-
s.scrub('�')
|
|
129
|
+
KKGit::GitOps.auto_commit_push!
|
|
79
130
|
end
|
|
80
131
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
end
|
|
132
|
+
def main_help
|
|
133
|
+
<<~HELP
|
|
134
|
+
Usage:
|
|
135
|
+
kk-git --version
|
|
136
|
+
kk-git commit-message [options]
|
|
137
|
+
kk-git status [options]
|
|
138
|
+
kk-git sync [options]
|
|
139
|
+
kk-git push [options]
|
|
140
|
+
|
|
141
|
+
Git helper: generate Conventional Commits messages and automate sync.
|
|
142
|
+
|
|
143
|
+
Environment variables (push/sync):
|
|
144
|
+
KK_GIT_REMOTE remote name (default: origin)
|
|
145
|
+
KK_GIT_BRANCH branch name (default: current branch)
|
|
146
|
+
KK_GIT_PULL_ARGS extra args for git pull (default: --ff-only)
|
|
147
|
+
KK_GIT_ADD_PATHS paths for git add (default: .)
|
|
148
|
+
KK_GIT_DRY_RUN=1 print commands without executing
|
|
149
|
+
KK_GIT_SKIP_PULL=1 skip pull step
|
|
150
|
+
KK_GIT_SKIP_PUSH=1 skip push step
|
|
151
|
+
KK_GIT_AMEND=1 amend last commit instead of creating new one
|
|
152
|
+
|
|
153
|
+
Run `kk-git <command> --help` for command-specific options.
|
|
154
|
+
HELP
|
|
155
|
+
end
|
|
107
156
|
|
|
108
|
-
if
|
|
109
|
-
|
|
157
|
+
if ['--version', '-v'].include?(ARGV[0])
|
|
158
|
+
puts KKGit::VERSION
|
|
159
|
+
exit 0
|
|
110
160
|
end
|
|
111
161
|
|
|
112
|
-
|
|
162
|
+
if ['--help', '-h'].include?(ARGV[0]) && ARGV.length == 1
|
|
163
|
+
puts main_help
|
|
164
|
+
exit 0
|
|
165
|
+
end
|
|
113
166
|
|
|
167
|
+
subcommand = ARGV.shift
|
|
168
|
+
case subcommand
|
|
169
|
+
when 'commit-message'
|
|
170
|
+
run_commit_message!(ARGV)
|
|
171
|
+
when 'status'
|
|
172
|
+
run_status!(ARGV)
|
|
173
|
+
when 'sync'
|
|
174
|
+
run_sync!(ARGV)
|
|
175
|
+
when 'push'
|
|
176
|
+
run_push!(ARGV)
|
|
177
|
+
when nil
|
|
178
|
+
warn "Missing command.\n\n#{main_help}"
|
|
179
|
+
exit 2
|
|
180
|
+
else
|
|
181
|
+
warn "Unknown command: #{subcommand}\n\n#{main_help}"
|
|
182
|
+
exit 2
|
|
183
|
+
end
|
|
@@ -4,13 +4,13 @@ require 'json'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
|
|
6
6
|
module KKGit
|
|
7
|
-
#
|
|
7
|
+
# Generate Conventional Commits messages from git changes.
|
|
8
8
|
#
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
11
|
-
# -
|
|
9
|
+
# - Supports staged and working-tree changes
|
|
10
|
+
# - Supports untracked files
|
|
11
|
+
# - Output format: "<type>(<scope>): <subject>\n\n<body>"
|
|
12
12
|
class CommitMessage
|
|
13
|
-
#
|
|
13
|
+
# Change entry (supports rename/copy old/new paths)
|
|
14
14
|
Change = Struct.new(:status, :path, :old_path, :source, keyword_init: true)
|
|
15
15
|
|
|
16
16
|
TYPE_PRIORITY = {
|
|
@@ -25,19 +25,19 @@ module KKGit
|
|
|
25
25
|
'chore' => 9
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Generate a commit message.
|
|
29
29
|
#
|
|
30
|
-
# @param repo_dir [String]
|
|
31
|
-
# @param mode [Symbol] :staged
|
|
32
|
-
# @param include_body [Boolean]
|
|
33
|
-
# @param fallback_scope [String]
|
|
34
|
-
# @param type_override [String, nil]
|
|
35
|
-
# @param scope_override [String, nil]
|
|
36
|
-
# @param subject_override [String, nil]
|
|
37
|
-
# @param detect_breaking [Boolean]
|
|
38
|
-
# @param max_diff_bytes [Integer] diff
|
|
30
|
+
# @param repo_dir [String] git repo directory (default: current dir)
|
|
31
|
+
# @param mode [Symbol] :staged / :worktree / :all
|
|
32
|
+
# @param include_body [Boolean] include body for multi-file changes
|
|
33
|
+
# @param fallback_scope [String] scope used when inference can't decide
|
|
34
|
+
# @param type_override [String, nil] force type (feat/fix/docs/...)
|
|
35
|
+
# @param scope_override [String, nil] force scope
|
|
36
|
+
# @param subject_override [String, nil] force subject
|
|
37
|
+
# @param detect_breaking [Boolean] detect "BREAKING" markers and emit "type(scope)!:" (default: true)
|
|
38
|
+
# @param max_diff_bytes [Integer] cap diff size for breaking detection
|
|
39
39
|
#
|
|
40
|
-
# @return [String, nil]
|
|
40
|
+
# @return [String, nil] returns nil when there are no changes
|
|
41
41
|
def self.generate(
|
|
42
42
|
repo_dir: '.',
|
|
43
43
|
mode: :staged,
|
|
@@ -69,7 +69,7 @@ module KKGit
|
|
|
69
69
|
message
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
#
|
|
72
|
+
# Generate structured data (useful for scripts/CI).
|
|
73
73
|
#
|
|
74
74
|
# @return [Hash]
|
|
75
75
|
def self.generate_hash(
|
|
@@ -169,7 +169,7 @@ module KKGit
|
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
def self.normalize_and_dedup(changes)
|
|
172
|
-
#
|
|
172
|
+
# Keyed by new_path; same path can show up in staged + worktree.
|
|
173
173
|
dedup = {}
|
|
174
174
|
changes.each do |c|
|
|
175
175
|
next if c.path.nil? || c.path.strip.empty?
|
|
@@ -181,10 +181,10 @@ module KKGit
|
|
|
181
181
|
next
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
#
|
|
185
|
-
# - staged
|
|
186
|
-
# - rename/copy
|
|
187
|
-
# - A(
|
|
184
|
+
# Priority:
|
|
185
|
+
# - staged wins over worktree (closer to what will be committed)
|
|
186
|
+
# - rename/copy wins over plain modifications
|
|
187
|
+
# - A(add) wins over M(modify)
|
|
188
188
|
priority = change_priority(c)
|
|
189
189
|
existing_priority = change_priority(existing)
|
|
190
190
|
dedup[key] = c if priority < existing_priority
|
|
@@ -212,16 +212,16 @@ module KKGit
|
|
|
212
212
|
|
|
213
213
|
def self.run_git(args, repo_dir:)
|
|
214
214
|
stdout, stderr, status = Open3.capture3('git', *args, chdir: repo_dir)
|
|
215
|
-
# Open3
|
|
215
|
+
# Open3 stdout/stderr may be ASCII-8BIT (BINARY). Normalize to UTF-8 to avoid concat errors.
|
|
216
216
|
stdout = stdout.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '�')
|
|
217
217
|
stderr = stderr.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '�')
|
|
218
218
|
|
|
219
|
-
raise "git #{args.join(' ')}
|
|
219
|
+
raise "git #{args.join(' ')} failed: #{stderr.strip}" unless status.success?
|
|
220
220
|
|
|
221
221
|
stdout
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
-
#
|
|
224
|
+
# Infer type/scope/subject (with optional breaking detection)
|
|
225
225
|
#
|
|
226
226
|
# @return [Hash] {:type,:scope,:subject,:breaking}
|
|
227
227
|
def self.infer(changes:, repo_dir:, mode:, detect_breaking:, max_diff_bytes:, fallback_scope:)
|
|
@@ -243,14 +243,14 @@ module KKGit
|
|
|
243
243
|
return fallback_scope if uniq.empty?
|
|
244
244
|
return uniq.first if uniq.length == 1
|
|
245
245
|
|
|
246
|
-
#
|
|
246
|
+
# When multiple scopes exist, prefer "repo"; otherwise fallback.
|
|
247
247
|
return 'repo' if uniq.include?('repo')
|
|
248
248
|
|
|
249
249
|
fallback_scope
|
|
250
250
|
end
|
|
251
251
|
|
|
252
252
|
def self.infer_scope(paths, fallback_scope:)
|
|
253
|
-
#
|
|
253
|
+
# Tooling/script/build changes: prefer a stable scope
|
|
254
254
|
if paths.any? && paths.all? { |p| tooling_path?(p) || doc_path?(p) || ci_path?(p) }
|
|
255
255
|
return 'tools'
|
|
256
256
|
end
|
|
@@ -261,7 +261,7 @@ module KKGit
|
|
|
261
261
|
return uniq.first if uniq.length == 1
|
|
262
262
|
return 'repo' if uniq.include?('repo')
|
|
263
263
|
|
|
264
|
-
#
|
|
264
|
+
# Multiple top-level dirs: use repo to keep scope stable/short.
|
|
265
265
|
'repo'
|
|
266
266
|
end
|
|
267
267
|
|
|
@@ -277,7 +277,7 @@ module KKGit
|
|
|
277
277
|
def self.infer_type(changes)
|
|
278
278
|
paths = changes.map(&:path)
|
|
279
279
|
|
|
280
|
-
#
|
|
280
|
+
# Fast paths
|
|
281
281
|
only_docs = paths.all? { |p| doc_path?(p) }
|
|
282
282
|
return 'docs' if only_docs
|
|
283
283
|
|
|
@@ -290,12 +290,12 @@ module KKGit
|
|
|
290
290
|
only_deps = paths.all? { |p| deps_path?(p) }
|
|
291
291
|
return 'chore' if only_deps
|
|
292
292
|
|
|
293
|
-
#
|
|
293
|
+
# Tooling/script/build related: prefer chore (even if code is added)
|
|
294
294
|
if paths.any? && paths.all? { |p| tooling_path?(p) || doc_path?(p) || ci_path?(p) || deps_path?(p) }
|
|
295
295
|
return 'chore'
|
|
296
296
|
end
|
|
297
297
|
|
|
298
|
-
#
|
|
298
|
+
# Heuristics for code changes
|
|
299
299
|
has_code = paths.any? { |p| code_path?(p) }
|
|
300
300
|
has_new_code = changes.any? { |c| c.status == 'A' && code_path?(c.path) }
|
|
301
301
|
has_fix_keyword = paths.any? { |p| p.match?(/fix|bug|error|issue/i) }
|
|
@@ -305,7 +305,7 @@ module KKGit
|
|
|
305
305
|
return 'fix' if has_code && has_fix_keyword
|
|
306
306
|
return 'refactor' if has_code && has_delete
|
|
307
307
|
|
|
308
|
-
#
|
|
308
|
+
# Mixed: aggregate by priority
|
|
309
309
|
types = changes.map { |c| type_by_path(c.path) }
|
|
310
310
|
pick_main_type(types)
|
|
311
311
|
end
|
|
@@ -321,7 +321,7 @@ module KKGit
|
|
|
321
321
|
|
|
322
322
|
return 'chore' unless code_path?(path)
|
|
323
323
|
|
|
324
|
-
#
|
|
324
|
+
# Default: treat code changes as fix (conservative). Adds are handled as feat by infer_type.
|
|
325
325
|
'fix'
|
|
326
326
|
end
|
|
327
327
|
|
|
@@ -409,11 +409,11 @@ module KKGit
|
|
|
409
409
|
c = changes.first
|
|
410
410
|
action =
|
|
411
411
|
case c.status
|
|
412
|
-
when 'A' then '
|
|
413
|
-
when 'D' then '
|
|
414
|
-
when 'R' then '
|
|
415
|
-
when 'C' then '
|
|
416
|
-
else '
|
|
412
|
+
when 'A' then 'Add'
|
|
413
|
+
when 'D' then 'Remove'
|
|
414
|
+
when 'R' then 'Rename'
|
|
415
|
+
when 'C' then 'Copy'
|
|
416
|
+
else 'Update'
|
|
417
417
|
end
|
|
418
418
|
|
|
419
419
|
if %w[R C].include?(c.status) && c.old_path
|
|
@@ -424,21 +424,21 @@ module KKGit
|
|
|
424
424
|
|
|
425
425
|
label =
|
|
426
426
|
case scope
|
|
427
|
-
when 'repo' then '
|
|
428
|
-
when 'tools' then '
|
|
427
|
+
when 'repo' then 'project'
|
|
428
|
+
when 'tools' then 'tools'
|
|
429
429
|
else scope
|
|
430
430
|
end
|
|
431
431
|
|
|
432
432
|
case type
|
|
433
|
-
when 'feat' then "
|
|
434
|
-
when 'fix' then "
|
|
435
|
-
when 'docs' then "
|
|
436
|
-
when 'refactor' then "
|
|
437
|
-
when 'style' then "
|
|
438
|
-
when 'perf' then "
|
|
439
|
-
when 'test' then "
|
|
440
|
-
when 'ci' then "
|
|
441
|
-
else "
|
|
433
|
+
when 'feat' then "Add #{label} features"
|
|
434
|
+
when 'fix' then "Fix #{label} issues"
|
|
435
|
+
when 'docs' then "Update #{label} docs"
|
|
436
|
+
when 'refactor' then "Refactor #{label}"
|
|
437
|
+
when 'style' then "Format #{label}"
|
|
438
|
+
when 'perf' then "Improve #{label} performance"
|
|
439
|
+
when 'test' then "Update #{label} tests"
|
|
440
|
+
when 'ci' then "Update #{label} CI"
|
|
441
|
+
else "Update #{label}"
|
|
442
442
|
end
|
|
443
443
|
end
|
|
444
444
|
|
|
@@ -458,12 +458,12 @@ module KKGit
|
|
|
458
458
|
end
|
|
459
459
|
|
|
460
460
|
lines = []
|
|
461
|
-
append_group(lines, '
|
|
462
|
-
append_group(lines, '
|
|
463
|
-
append_group(lines, '
|
|
464
|
-
append_group(lines, '
|
|
465
|
-
append_group(lines, '
|
|
466
|
-
append_group(lines, '
|
|
461
|
+
append_group(lines, 'Added', groups['A'])
|
|
462
|
+
append_group(lines, 'Changed', groups['M'])
|
|
463
|
+
append_group(lines, 'Removed', groups['D'])
|
|
464
|
+
append_group(lines, 'Renamed', groups['R'])
|
|
465
|
+
append_group(lines, 'Copied', groups['C'])
|
|
466
|
+
append_group(lines, 'Other', groups['?'])
|
|
467
467
|
lines.join("\n")
|
|
468
468
|
end
|
|
469
469
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module KKGit
|
|
7
|
+
# Git 仓库操作:供 Rake task 与 CLI 复用。
|
|
8
|
+
module GitOps
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# 仓库同步状态快照
|
|
12
|
+
Status = Struct.new(
|
|
13
|
+
:branch, :remote, :clean, :ahead, :behind,
|
|
14
|
+
:upstream_configured, :detached,
|
|
15
|
+
keyword_init: true
|
|
16
|
+
) do
|
|
17
|
+
# 是否需要与远端同步(有未推送 commit 或落后远端)
|
|
18
|
+
def needs_sync?
|
|
19
|
+
ahead.positive? || behind.positive?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unpushed?
|
|
23
|
+
ahead.positive?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def behind_remote?
|
|
27
|
+
behind.positive?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# @return [Boolean] KK_GIT_DRY_RUN=1 时只打印命令不执行
|
|
33
|
+
def dry_run?
|
|
34
|
+
ENV['KK_GIT_DRY_RUN'] == '1'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def skip_pull?
|
|
38
|
+
ENV['KK_GIT_SKIP_PULL'] == '1'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def skip_push?
|
|
42
|
+
ENV['KK_GIT_SKIP_PUSH'] == '1'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def amend?
|
|
46
|
+
ENV['KK_GIT_AMEND'] == '1'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def remote
|
|
50
|
+
ENV.fetch('KK_GIT_REMOTE', 'origin')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param explicit [String, nil] KK_GIT_BRANCH 或显式传入
|
|
54
|
+
def branch(explicit: ENV['KK_GIT_BRANCH'])
|
|
55
|
+
explicit.to_s.strip.empty? ? current_branch : explicit.to_s.strip
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# git add 路径,默认 `.`;可用 KK_GIT_ADD_PATHS 指定多个路径(空格分隔)
|
|
59
|
+
def add_paths
|
|
60
|
+
ENV.fetch('KK_GIT_ADD_PATHS', '.').split(/\s+/).reject(&:empty?)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 会修改仓库状态的 git 子命令;dry-run 时跳过这些命令
|
|
64
|
+
MUTATING_GIT_COMMANDS = %w[add commit push pull merge rebase checkout reset cherry-pick revert].freeze
|
|
65
|
+
|
|
66
|
+
def run_cmd(*cmd, chdir: nil)
|
|
67
|
+
if dry_run? && mutating_git_command?(cmd)
|
|
68
|
+
label = chdir ? "(cd #{chdir} && #{cmd.join(' ')})" : cmd.join(' ')
|
|
69
|
+
puts "[dry-run] #{label}"
|
|
70
|
+
return ['', '', true]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
stdout, stderr, status =
|
|
74
|
+
if chdir
|
|
75
|
+
Open3.capture3(*cmd, chdir: chdir)
|
|
76
|
+
else
|
|
77
|
+
Open3.capture3(*cmd)
|
|
78
|
+
end
|
|
79
|
+
[stdout.to_s, stderr.to_s, status.success?]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def mutating_git_command?(cmd)
|
|
83
|
+
cmd[0] == 'git' && MUTATING_GIT_COMMANDS.include?(cmd[1])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ensure_ok!(ok, title, stdout: nil, stderr: nil)
|
|
87
|
+
return if ok
|
|
88
|
+
|
|
89
|
+
msg = +"#{title} failed"
|
|
90
|
+
msg << "\n#{stderr}" unless stderr.to_s.strip.empty?
|
|
91
|
+
msg << "\n#{stdout}" unless stdout.to_s.strip.empty?
|
|
92
|
+
raise Error, msg
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def in_git_repo?
|
|
96
|
+
_, _, ok = run_cmd('git', 'rev-parse', '--git-dir')
|
|
97
|
+
ok
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ensure_in_repo!
|
|
101
|
+
raise Error, 'Not a git repository' unless in_git_repo?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def current_branch
|
|
105
|
+
out, err, ok = run_cmd('git', 'rev-parse', '--abbrev-ref', 'HEAD')
|
|
106
|
+
ensure_ok!(ok, 'Get current branch', stdout: out, stderr: err)
|
|
107
|
+
out.strip
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def detached_head?
|
|
111
|
+
current_branch == 'HEAD'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def ensure_not_detached!
|
|
115
|
+
raise Error, 'Cannot push from detached HEAD' if detached_head?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def working_tree_clean?
|
|
119
|
+
out, err, ok = run_cmd('git', 'status', '--porcelain')
|
|
120
|
+
ensure_ok!(ok, 'Check git status', stdout: out, stderr: err)
|
|
121
|
+
out.strip.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def upstream_configured?
|
|
125
|
+
_, _, ok = run_cmd('git', 'rev-parse', '--abbrev-ref', '@{u}')
|
|
126
|
+
ok
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# 相对 upstream / remote/branch 领先的 commit 数
|
|
130
|
+
def ahead_count(remote, branch)
|
|
131
|
+
out, _err, ok = run_cmd('git', 'rev-list', '--count', '@{u}..HEAD')
|
|
132
|
+
return out.strip.to_i if ok
|
|
133
|
+
|
|
134
|
+
out, _err, ok = run_cmd('git', 'rev-list', '--count', "#{remote}/#{branch}..HEAD")
|
|
135
|
+
return out.strip.to_i if ok
|
|
136
|
+
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 相对 upstream / remote/branch 落后的 commit 数
|
|
141
|
+
def behind_count(remote, branch)
|
|
142
|
+
out, _err, ok = run_cmd('git', 'rev-list', '--count', 'HEAD..@{u}')
|
|
143
|
+
return out.strip.to_i if ok
|
|
144
|
+
|
|
145
|
+
out, _err, ok = run_cmd('git', 'rev-list', '--count', "HEAD..#{remote}/#{branch}")
|
|
146
|
+
return out.strip.to_i if ok
|
|
147
|
+
|
|
148
|
+
0
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unpushed_commits?(remote, branch)
|
|
152
|
+
ahead_count(remote, branch).positive?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [Status]
|
|
156
|
+
def status(remote: nil, branch: nil)
|
|
157
|
+
ensure_in_repo!
|
|
158
|
+
remote ||= self.remote
|
|
159
|
+
branch ||= self.branch
|
|
160
|
+
|
|
161
|
+
Status.new(
|
|
162
|
+
branch: branch,
|
|
163
|
+
remote: remote,
|
|
164
|
+
clean: working_tree_clean?,
|
|
165
|
+
ahead: ahead_count(remote, branch),
|
|
166
|
+
behind: behind_count(remote, branch),
|
|
167
|
+
upstream_configured: upstream_configured?,
|
|
168
|
+
detached: detached_head?
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def status_hash(remote: nil, branch: nil)
|
|
173
|
+
s = status(remote: remote, branch: branch)
|
|
174
|
+
{
|
|
175
|
+
branch: s.branch,
|
|
176
|
+
remote: s.remote,
|
|
177
|
+
clean: s.clean,
|
|
178
|
+
ahead: s.ahead,
|
|
179
|
+
behind: s.behind,
|
|
180
|
+
upstream_configured: s.upstream_configured,
|
|
181
|
+
detached: s.detached,
|
|
182
|
+
needs_sync: s.needs_sync?
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def pull_remote!(remote, branch)
|
|
187
|
+
return if skip_pull?
|
|
188
|
+
|
|
189
|
+
pull_args = ENV.fetch('KK_GIT_PULL_ARGS', '--ff-only').split
|
|
190
|
+
out, err, ok = run_cmd('git', 'pull', remote, branch, *pull_args)
|
|
191
|
+
ensure_ok!(ok, 'git pull', stdout: out, stderr: err)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def push_remote!(remote, branch)
|
|
195
|
+
return if skip_push?
|
|
196
|
+
|
|
197
|
+
ensure_not_detached! unless dry_run?
|
|
198
|
+
|
|
199
|
+
if upstream_configured?
|
|
200
|
+
out, err, ok = run_cmd('git', 'push', remote, branch)
|
|
201
|
+
else
|
|
202
|
+
out, err, ok = run_cmd('git', 'push', '-u', remote, branch)
|
|
203
|
+
end
|
|
204
|
+
ensure_ok!(ok, 'git push', stdout: out, stderr: err)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def sync_with_remote!(remote, branch)
|
|
208
|
+
pull_remote!(remote, branch)
|
|
209
|
+
push_remote!(remote, branch)
|
|
210
|
+
puts "Synced: #{remote} #{branch}" unless dry_run?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def add_all!
|
|
214
|
+
paths = add_paths
|
|
215
|
+
out, err, ok = run_cmd('git', 'add', *paths)
|
|
216
|
+
ensure_ok!(ok, 'git add', stdout: out, stderr: err)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @return [Boolean] commit 是否成功
|
|
220
|
+
def commit_with_message!(message)
|
|
221
|
+
commit_args = amend? ? %w[commit --amend -F] : %w[commit -F]
|
|
222
|
+
|
|
223
|
+
Tempfile.create('commit_message') do |f|
|
|
224
|
+
f.write(message)
|
|
225
|
+
f.flush
|
|
226
|
+
out, err, ok = run_cmd('git', *commit_args, f.path)
|
|
227
|
+
if ok
|
|
228
|
+
true
|
|
229
|
+
elsif err.include?('nothing to commit') || out.include?('nothing to commit')
|
|
230
|
+
puts 'No staged changes to commit'
|
|
231
|
+
false
|
|
232
|
+
else
|
|
233
|
+
ensure_ok!(ok, 'git commit', stdout: out, stderr: err)
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# 自动 add → commit → pull → push 主流程
|
|
240
|
+
#
|
|
241
|
+
# @return [Symbol] :synced | :committed_and_synced | :noop
|
|
242
|
+
def auto_commit_push!(commit_message_generator: nil)
|
|
243
|
+
ensure_in_repo!
|
|
244
|
+
remote_name = remote
|
|
245
|
+
branch_name = branch
|
|
246
|
+
|
|
247
|
+
if working_tree_clean?
|
|
248
|
+
if status(remote: remote_name, branch: branch_name).needs_sync?
|
|
249
|
+
sync_with_remote!(remote_name, branch_name)
|
|
250
|
+
return :synced
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
puts 'No changes to commit or push'
|
|
254
|
+
return :noop
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
add_all!
|
|
258
|
+
|
|
259
|
+
message =
|
|
260
|
+
if commit_message_generator
|
|
261
|
+
commit_message_generator.call
|
|
262
|
+
else
|
|
263
|
+
KKGit::CommitMessage.generate(mode: :all)
|
|
264
|
+
end
|
|
265
|
+
message = message.to_s.strip
|
|
266
|
+
message = "chore(repo): update project files\n\n#{Time.now}" if message.empty?
|
|
267
|
+
|
|
268
|
+
committed = commit_with_message!(message)
|
|
269
|
+
if committed || unpushed_commits?(remote_name, branch_name)
|
|
270
|
+
sync_with_remote!(remote_name, branch_name)
|
|
271
|
+
return committed ? :committed_and_synced : :synced
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
:noop
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
data/lib/kk/git/rake_tasks.rb
CHANGED
|
@@ -1,113 +1,72 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rake'
|
|
4
|
-
|
|
5
|
-
require 'open3'
|
|
6
|
-
require 'tempfile'
|
|
4
|
+
require_relative '../git'
|
|
7
5
|
|
|
8
6
|
module KKGit
|
|
9
|
-
# Rake
|
|
7
|
+
# Rake integration: `require 'kk/git/rake_tasks'` to register tasks.
|
|
10
8
|
#
|
|
11
|
-
#
|
|
12
|
-
# -
|
|
13
|
-
# -
|
|
9
|
+
# Notes:
|
|
10
|
+
# - These tasks do NOT exit/abort so they can be invoked by other tasks.
|
|
11
|
+
# - The generated message is stored in `ENV['KK_GIT_COMMIT_MESSAGE']`.
|
|
14
12
|
module RakeTasks
|
|
15
|
-
def self.run_cmd(*cmd)
|
|
16
|
-
stdout, stderr, status = Open3.capture3(*cmd)
|
|
17
|
-
[stdout.to_s, stderr.to_s, status.success?]
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.ensure_ok!(ok, title, stdout: nil, stderr: nil)
|
|
21
|
-
return if ok
|
|
22
|
-
|
|
23
|
-
msg = +"#{title} 失败"
|
|
24
|
-
msg << "\n#{stderr}" unless stderr.to_s.strip.empty?
|
|
25
|
-
msg << "\n#{stdout}" unless stdout.to_s.strip.empty?
|
|
26
|
-
raise msg
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def self.current_branch
|
|
30
|
-
out, err, ok = run_cmd('git', 'rev-parse', '--abbrev-ref', 'HEAD')
|
|
31
|
-
ensure_ok!(ok, '获取当前分支', stdout: out, stderr: err)
|
|
32
|
-
out.strip
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.working_tree_clean?
|
|
36
|
-
out, err, ok = run_cmd('git', 'status', '--porcelain')
|
|
37
|
-
ensure_ok!(ok, '检查 git 状态', stdout: out, stderr: err)
|
|
38
|
-
out.strip.empty?
|
|
39
|
-
end
|
|
40
|
-
|
|
41
13
|
def self.install!
|
|
42
14
|
extend Rake::DSL
|
|
43
15
|
|
|
44
16
|
namespace :git do
|
|
45
|
-
desc '
|
|
17
|
+
desc 'Show branch sync status (ahead/behind/clean)'
|
|
18
|
+
task :status do
|
|
19
|
+
s = KKGit::GitOps.status
|
|
20
|
+
puts "Branch: #{s.branch} (#{s.remote})"
|
|
21
|
+
puts "Working tree: #{s.clean ? 'clean' : 'dirty'}"
|
|
22
|
+
puts "Ahead: #{s.ahead}, Behind: #{s.behind}"
|
|
23
|
+
puts "Upstream: #{s.upstream_configured ? 'configured' : 'not set'}"
|
|
24
|
+
puts 'Detached HEAD: yes' if s.detached
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc 'Pull and push without committing (sync unpushed or behind commits)'
|
|
28
|
+
task :sync do
|
|
29
|
+
remote = KKGit::GitOps.remote
|
|
30
|
+
branch = KKGit::GitOps.branch
|
|
31
|
+
s = KKGit::GitOps.status(remote: remote, branch: branch)
|
|
32
|
+
|
|
33
|
+
if s.needs_sync?
|
|
34
|
+
KKGit::GitOps.sync_with_remote!(remote, branch)
|
|
35
|
+
else
|
|
36
|
+
puts 'Already in sync with remote'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc 'Generate commit message from staged changes (Conventional Commits)'
|
|
46
41
|
task :commit_message do
|
|
47
42
|
msg = KKGit::CommitMessage.generate(mode: :staged)
|
|
48
43
|
ENV['KK_GIT_COMMIT_MESSAGE'] = msg.to_s
|
|
49
44
|
puts msg if msg
|
|
50
45
|
end
|
|
51
46
|
|
|
52
|
-
desc '
|
|
47
|
+
desc 'Generate commit message from working-tree changes (includes untracked)'
|
|
53
48
|
task :commit_message_worktree do
|
|
54
49
|
msg = KKGit::CommitMessage.generate(mode: :worktree)
|
|
55
50
|
ENV['KK_GIT_COMMIT_MESSAGE'] = msg.to_s
|
|
56
51
|
puts msg if msg
|
|
57
52
|
end
|
|
58
53
|
|
|
59
|
-
desc '
|
|
54
|
+
desc 'Generate commit message from staged + working-tree changes'
|
|
60
55
|
task :auto_commit do
|
|
61
56
|
msg = KKGit::CommitMessage.generate(mode: :all)
|
|
62
57
|
ENV['KK_GIT_COMMIT_MESSAGE'] = msg.to_s
|
|
63
58
|
puts msg if msg
|
|
64
59
|
end
|
|
65
60
|
|
|
66
|
-
desc '
|
|
61
|
+
desc 'Auto add/commit/pull/push (uses git:auto_commit)'
|
|
67
62
|
task :auto_commit_push do
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
generator = lambda do
|
|
64
|
+
Rake::Task['git:auto_commit'].reenable
|
|
65
|
+
Rake::Task['git:auto_commit'].invoke
|
|
66
|
+
ENV['KK_GIT_COMMIT_MESSAGE'].to_s
|
|
71
67
|
end
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
branch = ENV.fetch('KK_GIT_BRANCH', current_branch)
|
|
75
|
-
|
|
76
|
-
# 1) add
|
|
77
|
-
out, err, ok = run_cmd('git', 'add', '.')
|
|
78
|
-
ensure_ok!(ok, 'git add', stdout: out, stderr: err)
|
|
79
|
-
|
|
80
|
-
# 2) 生成 commit message(允许重复 invoke)
|
|
81
|
-
Rake::Task['git:auto_commit'].reenable
|
|
82
|
-
Rake::Task['git:auto_commit'].invoke
|
|
83
|
-
commit_message = ENV['KK_GIT_COMMIT_MESSAGE'].to_s.strip
|
|
84
|
-
commit_message = "chore(repo): 更新项目文件\n\n#{Time.now}" if commit_message.empty?
|
|
85
|
-
|
|
86
|
-
# 3) commit(用临时文件避免转义问题)
|
|
87
|
-
Tempfile.create('commit_message') do |f|
|
|
88
|
-
f.write(commit_message)
|
|
89
|
-
f.flush
|
|
90
|
-
out, err, ok = run_cmd('git', 'commit', '-F', f.path)
|
|
91
|
-
# 没有 staged 变更时 git commit 会失败;这里给出更友好的提示
|
|
92
|
-
unless ok
|
|
93
|
-
if err.include?('nothing to commit') || out.include?('nothing to commit')
|
|
94
|
-
puts '没有暂存变更需要提交'
|
|
95
|
-
next
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
ensure_ok!(ok, 'git commit', stdout: out, stderr: err)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# 4) pull(默认使用 --ff-only,避免非交互环境进入合并流程)
|
|
102
|
-
pull_args = ENV.fetch('KK_GIT_PULL_ARGS', '--ff-only').split
|
|
103
|
-
out, err, ok = run_cmd('git', 'pull', *pull_args)
|
|
104
|
-
ensure_ok!(ok, 'git pull', stdout: out, stderr: err)
|
|
105
|
-
|
|
106
|
-
# 5) push
|
|
107
|
-
out, err, ok = run_cmd('git', 'push', remote, branch)
|
|
108
|
-
ensure_ok!(ok, 'git push', stdout: out, stderr: err)
|
|
109
|
-
|
|
110
|
-
puts "✅ 已推送: #{remote} #{branch}"
|
|
69
|
+
KKGit::GitOps.auto_commit_push!(commit_message_generator: generator)
|
|
111
70
|
end
|
|
112
71
|
end
|
|
113
72
|
end
|
|
@@ -115,4 +74,3 @@ module KKGit
|
|
|
115
74
|
end
|
|
116
75
|
|
|
117
76
|
KKGit::RakeTasks.install!
|
|
118
|
-
|
data/lib/kk/git/version.rb
CHANGED
data/lib/kk/git.rb
CHANGED
metadata
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kk-git
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description:
|
|
14
|
-
Rake
|
|
13
|
+
description: Generate Conventional Commits commit messages from current git changes
|
|
14
|
+
(staged/worktree), designed for Rake/script usage.
|
|
15
15
|
email:
|
|
16
16
|
- ''
|
|
17
17
|
executables:
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- exe/kk-git
|
|
23
23
|
- lib/kk/git.rb
|
|
24
24
|
- lib/kk/git/commit_message.rb
|
|
25
|
+
- lib/kk/git/git_ops.rb
|
|
25
26
|
- lib/kk/git/rake_tasks.rb
|
|
26
27
|
- lib/kk/git/version.rb
|
|
27
28
|
homepage: ''
|
|
@@ -47,5 +48,5 @@ requirements: []
|
|
|
47
48
|
rubygems_version: 3.5.22
|
|
48
49
|
signing_key:
|
|
49
50
|
specification_version: 4
|
|
50
|
-
summary: Git
|
|
51
|
+
summary: 'Git helper: generate Conventional Commits messages'
|
|
51
52
|
test_files: []
|