kk-git 0.1.7 → 0.2.1

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: bf8344aceea8482a3025d31bbcc1293cd6a4fd6aaa3235c3907cf173538e83e5
4
- data.tar.gz: 553a278a015110d0205d9da2e2df75d83429b169428c23fb1d75ca40a139c140
3
+ metadata.gz: 74daf125db0cf895ef3d9c7c6e0315f56a70a1e5de4cfff743ed0bb281d9c73c
4
+ data.tar.gz: 0dda4d4d4544b1249d3b95a019ea4da0f6d9d1469b3815b21c5069ea213a2ca6
5
5
  SHA512:
6
- metadata.gz: f9a2764f0d336ef4ce214133e63d9b8cad0c68d481d95b88ff0f00feca8d358199a1687c37e95bc563eea8758f85c46ee90f9826621cfe5db168cd75087a46e3
7
- data.tar.gz: 58e748e4640395c28e9e27c805e36d3b5ed1fc997e55fcfc9c0d4649fdbe593aaf3c22e39d3d25f82acca5408e71516211118691690a288b6181bc8f01a758c6
6
+ metadata.gz: f9b644cfda470263d0bdef6073c8adf2fa9864d24388e8af13053ce684996c2ec62f2a90d131b82eb9d5daa7c6d073d8d061571ac21c63887d0e0454fe8fdfdf
7
+ data.tar.gz: 59a25a6ad4c915ebe7a48f128ad501585340f9ffc3f9ecccc47bb9303729ac2286783b1ea08e1e18128e075a4d2beb5ece410d59d8435ba09cf1cfb0690578d5
data/exe/kk-git CHANGED
@@ -10,110 +10,174 @@ rescue LoadError
10
10
  require_relative '../lib/kk/git'
11
11
  end
12
12
 
13
- options = {
14
- mode: :staged,
15
- format: :text,
16
- include_body: true,
17
- repo_dir: '.',
18
- type: nil,
19
- scope: nil,
20
- subject: nil,
21
- detect_breaking: true
22
- }
23
-
24
- parser = OptionParser.new do |opts|
25
- opts.banner = "Usage:\n" \
26
- " kk-git --version\n" \
27
- " kk-git commit-message [options]\n\n" \
28
- "Generate Conventional Commits messages from git changes."
29
-
30
- opts.on('--repo DIR', 'Git repo directory (default: current dir)') { |v| options[:repo_dir] = v }
31
-
32
- opts.on('--staged', 'Use staged changes only (default)') { options[:mode] = :staged }
33
- opts.on('--worktree', 'Use working-tree changes only (includes untracked)') { options[:mode] = :worktree }
34
- opts.on('--all', 'Combine staged + working-tree changes') { options[:mode] = :all }
35
-
36
- opts.on('--[no-]body', 'Include body for multi-file changes (default: yes)') { |v| options[:include_body] = v }
37
-
38
- opts.on('--type TYPE', 'Override inferred type (feat/fix/docs/...)') { |v| options[:type] = v }
39
- opts.on('--scope SCOPE', 'Override inferred scope') { |v| options[:scope] = v }
40
- opts.on('--subject SUBJECT', 'Override inferred subject') { |v| options[:subject] = v }
41
- opts.on('--[no-]detect-breaking', 'Detect BREAKING markers (default: yes)') { |v| options[:detect_breaking] = v }
42
-
43
- opts.on('--format FORMAT', 'Output format: text/json (default: text)') do |v|
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
- opts.on('-h', '--help', 'Show help') do
48
- puts opts
49
- exit 0
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
- opts.on('-v', '--version', 'Show version') do
53
- puts KKGit::VERSION
54
- exit 0
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
- # Support `kk-git --version` without subcommand
59
- if ['--version', '-v'].include?(ARGV[0])
60
- puts KKGit::VERSION
61
- exit 0
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
- # Support `kk-git --help` without subcommand
65
- if ['--help', '-h'].include?(ARGV[0])
66
- puts parser
67
- exit 0
68
- end
111
+ remote = KKGit::GitOps.remote
112
+ branch = KKGit::GitOps.branch
113
+ s = KKGit::GitOps.status(remote: remote, branch: branch)
69
114
 
70
- subcommand = ARGV.shift
71
- if subcommand != 'commit-message'
72
- warn "Unknown command: #{subcommand}\n\n#{parser}"
73
- exit 2
115
+ if s.needs_sync?
116
+ KKGit::GitOps.sync_with_remote!(remote, branch)
117
+ else
118
+ puts 'Already in sync with remote'
119
+ end
74
120
  end
75
121
 
76
- parser.parse!(ARGV)
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)
77
128
 
78
- def to_utf8(str)
79
- return nil if str.nil?
80
- s = str.to_s.dup
81
- # In some locales ARGV may be tagged as ASCII-8BIT/US-ASCII, but bytes are usually UTF-8.
82
- # Prefer interpreting bytes as UTF-8 and scrub invalid sequences.
83
- s.force_encoding(Encoding::UTF_8)
84
- s.scrub('�')
129
+ KKGit::GitOps.auto_commit_push!
85
130
  end
86
131
 
87
- options[:type] = to_utf8(options[:type])
88
- options[:scope] = to_utf8(options[:scope])
89
- options[:subject] = to_utf8(options[:subject])
90
-
91
- result =
92
- if options[:format] == :json
93
- KKGit::CommitMessage.generate_hash(
94
- repo_dir: options[:repo_dir],
95
- mode: options[:mode],
96
- include_body: options[:include_body],
97
- type_override: options[:type],
98
- scope_override: options[:scope],
99
- subject_override: options[:subject],
100
- detect_breaking: options[:detect_breaking]
101
- )
102
- else
103
- KKGit::CommitMessage.generate(
104
- repo_dir: options[:repo_dir],
105
- mode: options[:mode],
106
- include_body: options[:include_body],
107
- type_override: options[:type],
108
- scope_override: options[:scope],
109
- subject_override: options[:subject],
110
- detect_breaking: options[:detect_breaking]
111
- )
112
- 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
113
156
 
114
- if result.nil? || (result.is_a?(Hash) && result[:empty])
115
- exit 1
157
+ if ['--version', '-v'].include?(ARGV[0])
158
+ puts KKGit::VERSION
159
+ exit 0
116
160
  end
117
161
 
118
- puts(options[:format] == :json ? JSON.pretty_generate(result) : result)
162
+ if ['--help', '-h'].include?(ARGV[0]) && ARGV.length == 1
163
+ puts main_help
164
+ exit 0
165
+ end
119
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
@@ -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
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'rake'
4
4
  require_relative '../git'
5
- require 'open3'
6
- require 'tempfile'
7
5
 
8
6
  module KKGit
9
7
  # Rake integration: `require 'kk/git/rake_tasks'` to register tasks.
@@ -12,36 +10,33 @@ module KKGit
12
10
  # - These tasks do NOT exit/abort so they can be invoked by other tasks.
13
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} failed"
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, 'Get current branch', 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, 'Check git status', 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
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
+
45
40
  desc 'Generate commit message from staged changes (Conventional Commits)'
46
41
  task :commit_message do
47
42
  msg = KKGit::CommitMessage.generate(mode: :staged)
@@ -65,49 +60,13 @@ module KKGit
65
60
 
66
61
  desc 'Auto add/commit/pull/push (uses git:auto_commit)'
67
62
  task :auto_commit_push do
68
- if working_tree_clean?
69
- puts 'No changes to commit'
70
- next
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
- remote = ENV.fetch('KK_GIT_REMOTE', 'origin')
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) generate commit message (allow re-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): update project files\n\n#{Time.now}" if commit_message.empty?
85
-
86
- # 3) commit (use a tempfile to avoid escaping issues)
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
- # If there are no staged changes, git commit fails; show a friendlier message.
92
- unless ok
93
- if err.include?('nothing to commit') || out.include?('nothing to commit')
94
- puts 'No staged changes to commit'
95
- next
96
- end
97
- end
98
- ensure_ok!(ok, 'git commit', stdout: out, stderr: err)
99
- end
100
-
101
- # 4) pull (default: --ff-only to avoid interactive merges)
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 "Pushed: #{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
-
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KKGit
4
- VERSION = '0.1.7'
4
+ VERSION = '0.2.1'
5
5
  end
6
6
 
data/lib/kk/git.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'git/version'
4
4
  require_relative 'git/commit_message'
5
+ require_relative 'git/git_ops'
5
6
 
6
7
  module KKGit
7
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kk-git
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-25 00:00:00.000000000 Z
11
+ date: 2026-06-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Generate Conventional Commits commit messages from current git changes
14
14
  (staged/worktree), designed for Rake/script usage.
@@ -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: ''