kk-git 0.2.4 → 0.2.6
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 +2 -0
- data/lib/kk/git/auto_format.rb +193 -0
- data/lib/kk/git/git_ops.rb +24 -6
- data/lib/kk/git/version.rb +1 -1
- data/lib/kk/git.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae39abac1e3638de958917ebc2fcbbffdace00c23e13fdd53929cec3e416930f
|
|
4
|
+
data.tar.gz: eb7191f382ae86ff2c25aac1ce6f7af7a1288be5a25c503641416bcb321ba234
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df09df41cb4383f3654dc161b33703798e069cd16be48dd160162076eca1d03417c7272bb6dc80cdfb7a06c2a46e8a8029597e94c40e6fe5d79e5be9a2f1c6a4
|
|
7
|
+
data.tar.gz: a52d189d3bf6606506e9fb06f3ff8289926d9f62864e9a2e3628a0657813a009e32840cfda0b2d4e414f3a8b768e940211566f247c06a998db4c9732d29471a9
|
data/exe/kk-git
CHANGED
|
@@ -169,6 +169,8 @@ def main_help
|
|
|
169
169
|
KK_GIT_DEFAULT_TYPE default type for code edits (default: chore)
|
|
170
170
|
KK_GIT_PUSH_URL push to explicit URL (bypass SSH remote)
|
|
171
171
|
KK_GIT_PULL_URL pull from explicit URL (bypass SSH remote)
|
|
172
|
+
KK_GIT_SKIP_FORMAT=1 skip pre-commit auto-format
|
|
173
|
+
KK_GIT_FORMAT_ALL=1 format all tracked files (default: changed only)
|
|
172
174
|
|
|
173
175
|
Run `kk-git <command> --help` for command-specific options.
|
|
174
176
|
HELP
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module KKGit
|
|
6
|
+
# Best-effort format changed files before commit; skip missing tools.
|
|
7
|
+
module AutoFormat
|
|
8
|
+
Language = Struct.new(:id, :label, :extensions, :tools, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
Tool = Struct.new(:name, :bin, :argv, keyword_init: true) do
|
|
11
|
+
def build(files, repo_dir:)
|
|
12
|
+
bin_path = AutoFormat.resolve_bin(bin, repo_dir: repo_dir)
|
|
13
|
+
return nil unless bin_path
|
|
14
|
+
|
|
15
|
+
args = argv.flat_map { |part| part == '%{files}' ? files : [part] }
|
|
16
|
+
[bin_path, *args]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
LANGUAGES = [
|
|
21
|
+
Language.new(
|
|
22
|
+
id: :ruby,
|
|
23
|
+
label: 'Ruby',
|
|
24
|
+
extensions: %w[.rb],
|
|
25
|
+
tools: [
|
|
26
|
+
Tool.new(name: 'rubocop', bin: 'rubocop', argv: ['-A', '--force-exclusion', '%{files}'])
|
|
27
|
+
]
|
|
28
|
+
),
|
|
29
|
+
Language.new(
|
|
30
|
+
id: :python,
|
|
31
|
+
label: 'Python',
|
|
32
|
+
extensions: %w[.py],
|
|
33
|
+
tools: [
|
|
34
|
+
Tool.new(name: 'ruff', bin: 'ruff', argv: ['format', '%{files}']),
|
|
35
|
+
Tool.new(name: 'black', bin: 'black', argv: ['%{files}'])
|
|
36
|
+
]
|
|
37
|
+
),
|
|
38
|
+
Language.new(
|
|
39
|
+
id: :go,
|
|
40
|
+
label: 'Go',
|
|
41
|
+
extensions: %w[.go],
|
|
42
|
+
tools: [
|
|
43
|
+
Tool.new(name: 'gofmt', bin: 'gofmt', argv: ['-w', '%{files}'])
|
|
44
|
+
]
|
|
45
|
+
),
|
|
46
|
+
Language.new(
|
|
47
|
+
id: :bash,
|
|
48
|
+
label: 'Bash',
|
|
49
|
+
extensions: %w[.sh .bash],
|
|
50
|
+
tools: [
|
|
51
|
+
Tool.new(name: 'shfmt', bin: 'shfmt', argv: ['-w', '%{files}'])
|
|
52
|
+
]
|
|
53
|
+
),
|
|
54
|
+
Language.new(
|
|
55
|
+
id: :prettier,
|
|
56
|
+
label: 'JS/TS/Node',
|
|
57
|
+
extensions: %w[.js .jsx .mjs .cjs .ts .tsx .json .css .scss .less .html .md .yaml .yml],
|
|
58
|
+
tools: [
|
|
59
|
+
Tool.new(name: 'prettier', bin: 'prettier', argv: ['--write', '%{files}'])
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
module_function
|
|
65
|
+
|
|
66
|
+
def enabled?
|
|
67
|
+
ENV['KK_GIT_SKIP_FORMAT'] != '1'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def format_all?
|
|
71
|
+
ENV['KK_GIT_FORMAT_ALL'] == '1'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def run!(repo_dir: '.', dry_run: GitOps.dry_run?)
|
|
75
|
+
return { ran: [], skipped: [], failed: [] } unless enabled?
|
|
76
|
+
|
|
77
|
+
files = target_files(repo_dir: repo_dir)
|
|
78
|
+
grouped = group_files(files)
|
|
79
|
+
return { ran: [], skipped: [], failed: [] } if grouped.empty?
|
|
80
|
+
|
|
81
|
+
ran = []
|
|
82
|
+
skipped = []
|
|
83
|
+
failed = []
|
|
84
|
+
|
|
85
|
+
grouped.each do |lang_id, paths|
|
|
86
|
+
language = LANGUAGES.find { |l| l.id == lang_id }
|
|
87
|
+
next unless language
|
|
88
|
+
|
|
89
|
+
tool = pick_tool(language, paths, repo_dir: repo_dir)
|
|
90
|
+
unless tool
|
|
91
|
+
skipped << "#{language.label}: no formatter (#{language.tools.map(&:name).join(' / ')})"
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
cmd = tool.build(paths, repo_dir: repo_dir)
|
|
96
|
+
label = "#{language.label} (#{tool.name})"
|
|
97
|
+
if dry_run
|
|
98
|
+
puts "[dry-run] format #{label}: #{cmd.join(' ')}"
|
|
99
|
+
ran << label
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
unless executable?(cmd&.first)
|
|
104
|
+
skipped << "#{label}: not found (#{tool.name})"
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
out, err, ok = run_command(cmd, repo_dir: repo_dir)
|
|
109
|
+
if ok
|
|
110
|
+
puts "Formatted #{label}: #{paths.length} file(s)"
|
|
111
|
+
ran << label
|
|
112
|
+
else
|
|
113
|
+
msg = err.to_s.strip.empty? ? out.to_s.strip : err.to_s.strip
|
|
114
|
+
warn "Format failed #{label} (continuing): #{msg.lines.first}"
|
|
115
|
+
failed << label
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
skipped.each { |line| puts "Format skip: #{line}" }
|
|
120
|
+
|
|
121
|
+
{ ran: ran, skipped: skipped, failed: failed }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def target_files(repo_dir:)
|
|
125
|
+
format_all? ? all_tracked_files(repo_dir: repo_dir) : changed_files(repo_dir: repo_dir)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def changed_files(repo_dir:)
|
|
129
|
+
out, _err, ok = GitOps.run_cmd('git', 'status', '--porcelain', '-z', chdir: repo_dir)
|
|
130
|
+
return [] unless ok
|
|
131
|
+
|
|
132
|
+
out.split("\0").filter_map do |entry|
|
|
133
|
+
next if entry.nil? || entry.empty?
|
|
134
|
+
|
|
135
|
+
path = entry[3..]
|
|
136
|
+
next if path.nil? || path.empty? || path.end_with?('/')
|
|
137
|
+
|
|
138
|
+
path
|
|
139
|
+
end.uniq
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def all_tracked_files(repo_dir:)
|
|
143
|
+
out, _err, ok = GitOps.run_cmd('git', 'ls-files', '-z', chdir: repo_dir)
|
|
144
|
+
return [] unless ok
|
|
145
|
+
|
|
146
|
+
out.split("\0").reject(&:empty?)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def group_files(files)
|
|
150
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
|
151
|
+
files.each do |path|
|
|
152
|
+
ext = File.extname(path)
|
|
153
|
+
language = LANGUAGES.find { |lang| lang.extensions.include?(ext) }
|
|
154
|
+
grouped[language.id] << path if language
|
|
155
|
+
end
|
|
156
|
+
grouped.transform_values { |paths| paths.uniq.sort }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def pick_tool(language, _files, repo_dir:)
|
|
160
|
+
language.tools.find do |tool|
|
|
161
|
+
resolve_bin(tool.bin, repo_dir: repo_dir)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def resolve_bin(name, repo_dir:)
|
|
166
|
+
path = which(name)
|
|
167
|
+
return path if executable?(path)
|
|
168
|
+
|
|
169
|
+
local = File.expand_path(File.join(repo_dir, 'node_modules', '.bin', name))
|
|
170
|
+
return local if executable?(local)
|
|
171
|
+
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def which(cmd)
|
|
176
|
+
out, _, ok = Open3.capture3('command', '-v', cmd)
|
|
177
|
+
return nil unless ok
|
|
178
|
+
|
|
179
|
+
path = out.strip
|
|
180
|
+
path.empty? ? nil : path
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def executable?(path)
|
|
184
|
+
path && File.file?(path) && File.executable?(path)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def run_command(cmd, repo_dir:)
|
|
188
|
+
Open3.capture3(*cmd, chdir: repo_dir)
|
|
189
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
190
|
+
['', e.message, false]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/kk/git/git_ops.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'open3'
|
|
3
4
|
require 'tempfile'
|
|
4
5
|
require_relative 'git_runner'
|
|
5
6
|
require_relative 'network_hints'
|
|
7
|
+
require_relative 'auto_format'
|
|
6
8
|
|
|
7
9
|
module KKGit
|
|
8
10
|
# Git 仓库操作:供 Rake task 与 CLI 复用。
|
|
@@ -10,14 +12,16 @@ module KKGit
|
|
|
10
12
|
class Error < StandardError; end
|
|
11
13
|
|
|
12
14
|
SENSITIVE_PATH_PATTERNS = [
|
|
13
|
-
|
|
15
|
+
/\A\.env(\.|$)/i,
|
|
14
16
|
/credentials/i,
|
|
15
|
-
|
|
17
|
+
/\.pem\z/i,
|
|
16
18
|
/id_rsa/i,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
/\.key\z/i,
|
|
20
|
+
/\Asecrets?\./i
|
|
19
21
|
].freeze
|
|
20
22
|
|
|
23
|
+
SAFE_ENV_TEMPLATE_SUFFIXES = %w[example sample template dist].freeze
|
|
24
|
+
|
|
21
25
|
# 仓库同步状态快照
|
|
22
26
|
Status = Struct.new(
|
|
23
27
|
:branch, :remote, :clean, :ahead, :behind,
|
|
@@ -58,10 +62,14 @@ module KKGit
|
|
|
58
62
|
ENV.fetch('KK_GIT_REMOTE', 'origin')
|
|
59
63
|
end
|
|
60
64
|
|
|
61
|
-
def branch(explicit: ENV
|
|
65
|
+
def branch(explicit: ENV.fetch('KK_GIT_BRANCH', nil), repo_dir: '.')
|
|
62
66
|
explicit.to_s.strip.empty? ? current_branch(repo_dir: repo_dir) : explicit.to_s.strip
|
|
63
67
|
end
|
|
64
68
|
|
|
69
|
+
def skip_format?
|
|
70
|
+
ENV['KK_GIT_SKIP_FORMAT'] == '1'
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
def add_paths
|
|
66
74
|
ENV.fetch('KK_GIT_ADD_PATHS', '.').split(/\s+/).reject(&:empty?)
|
|
67
75
|
end
|
|
@@ -113,7 +121,7 @@ module KKGit
|
|
|
113
121
|
def ensure_ok!(ok, title, stdout: nil, stderr: nil, repo_dir: '.')
|
|
114
122
|
return if ok
|
|
115
123
|
|
|
116
|
-
msg =
|
|
124
|
+
msg = "#{title} failed"
|
|
117
125
|
msg << "\n#{stderr}" unless stderr.to_s.strip.empty?
|
|
118
126
|
msg << "\n#{stdout}" unless stdout.to_s.strip.empty?
|
|
119
127
|
|
|
@@ -252,9 +260,17 @@ module KKGit
|
|
|
252
260
|
end
|
|
253
261
|
|
|
254
262
|
def sensitive_path?(path)
|
|
263
|
+
return false if safe_env_template?(path)
|
|
264
|
+
|
|
255
265
|
SENSITIVE_PATH_PATTERNS.any? { |pattern| path.match?(pattern) }
|
|
256
266
|
end
|
|
257
267
|
|
|
268
|
+
def safe_env_template?(path)
|
|
269
|
+
SAFE_ENV_TEMPLATE_SUFFIXES.any? do |suffix|
|
|
270
|
+
path.match?(/\A\.env\.#{suffix}\z/i)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
258
274
|
def sensitive_staged_paths(repo_dir: '.')
|
|
259
275
|
out, _err, ok = run_cmd('git', 'diff', '--cached', '--name-only', chdir: repo_dir)
|
|
260
276
|
return [] unless ok
|
|
@@ -318,6 +334,8 @@ module KKGit
|
|
|
318
334
|
return :noop
|
|
319
335
|
end
|
|
320
336
|
|
|
337
|
+
AutoFormat.run!(repo_dir: repo_dir) unless skip_format?
|
|
338
|
+
|
|
321
339
|
add_all!(repo_dir: repo_dir)
|
|
322
340
|
ensure_no_sensitive_staged!(repo_dir: repo_dir)
|
|
323
341
|
|
data/lib/kk/git/version.rb
CHANGED
data/lib/kk/git.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kk-git
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kk
|
|
@@ -21,6 +21,7 @@ extra_rdoc_files: []
|
|
|
21
21
|
files:
|
|
22
22
|
- exe/kk-git
|
|
23
23
|
- lib/kk/git.rb
|
|
24
|
+
- lib/kk/git/auto_format.rb
|
|
24
25
|
- lib/kk/git/commit_message.rb
|
|
25
26
|
- lib/kk/git/git_ops.rb
|
|
26
27
|
- lib/kk/git/git_runner.rb
|