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 +4 -4
- data/exe/kk-git +153 -89
- data/lib/kk/git/git_ops.rb +278 -0
- data/lib/kk/git/rake_tasks.rb +28 -70
- data/lib/kk/git/version.rb +1 -1
- data/lib/kk/git.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74daf125db0cf895ef3d9c7c6e0315f56a70a1e5de4cfff743ed0bb281d9c73c
|
|
4
|
+
data.tar.gz: 0dda4d4d4544b1249d3b95a019ea4da0f6d9d1469b3815b21c5069ea213a2ca6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
115
|
-
|
|
157
|
+
if ['--version', '-v'].include?(ARGV[0])
|
|
158
|
+
puts KKGit::VERSION
|
|
159
|
+
exit 0
|
|
116
160
|
end
|
|
117
161
|
|
|
118
|
-
|
|
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
|
data/lib/kk/git/rake_tasks.rb
CHANGED
|
@@ -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
|
-
|
|
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) 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
|
-
|
data/lib/kk/git/version.rb
CHANGED
data/lib/kk/git.rb
CHANGED
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
|
|
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-
|
|
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: ''
|