kk-git 0.2.3 → 0.2.5

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: 476a578e1aeea922d2d1c7835f42066e625a9cc976f5d19d713def1f8f11aa6c
4
- data.tar.gz: dac594a319b48a7a089b58f45b5739fc9070cedb74ff113e64306c1c1faa202c
3
+ metadata.gz: 4b432c2d8b24c8a15fe3185d0f8d9c26bc07816a788a051933b7eaeb7b2a5409
4
+ data.tar.gz: f7666db73c822b09c1918e656437fc165f88ef13a6f23e1fd32fd0d3eb53c0f4
5
5
  SHA512:
6
- metadata.gz: 44f2c4ddbff4478b973096815179d39a28711497e3f9bc2bd3b4461e6a7c44283565eb52e93b25588aca78a6ca9a95f57b170f0af0b5fafe6b50e6ae13e283cc
7
- data.tar.gz: 54679b9c73d3e181db53ba4fccab282f3ae3878d07215663af441458e9215b6d935af1e41b735d9821e78baf74f4aad0e31db6f6d4209eb4c21f7b38f430c214
6
+ metadata.gz: a08f91f62c4252dee11fc5be7a414f75c7f1c96de5151b83c25a7c78bce8d33419623c35f9768c02f5e16c81485d8622e645f1dbf8af4298cc8bd80d2b93ccd7
7
+ data.tar.gz: 2167ef28d0b789fda42ac855834340e968ccaf973d0858281b7dbb8d8059128624bbb2b997100ee8d4ac6720c9356dc7d76b8989feb7011c36380d8046619944
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
@@ -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,12 +12,12 @@ module KKGit
10
12
  class Error < StandardError; end
11
13
 
12
14
  SENSITIVE_PATH_PATTERNS = [
13
- %r{\A\.env(\.|$)}i,
15
+ /\A\.env(\.|$)/i,
14
16
  /credentials/i,
15
- %r{\.pem\z}i,
17
+ /\.pem\z/i,
16
18
  /id_rsa/i,
17
- %r{\.key\z}i,
18
- %r{\Asecrets?\.}i
19
+ /\.key\z/i,
20
+ /\Asecrets?\./i
19
21
  ].freeze
20
22
 
21
23
  # 仓库同步状态快照
@@ -58,10 +60,14 @@ module KKGit
58
60
  ENV.fetch('KK_GIT_REMOTE', 'origin')
59
61
  end
60
62
 
61
- def branch(explicit: ENV['KK_GIT_BRANCH'], repo_dir: '.')
63
+ def branch(explicit: ENV.fetch('KK_GIT_BRANCH', nil), repo_dir: '.')
62
64
  explicit.to_s.strip.empty? ? current_branch(repo_dir: repo_dir) : explicit.to_s.strip
63
65
  end
64
66
 
67
+ def skip_format?
68
+ ENV['KK_GIT_SKIP_FORMAT'] == '1'
69
+ end
70
+
65
71
  def add_paths
66
72
  ENV.fetch('KK_GIT_ADD_PATHS', '.').split(/\s+/).reject(&:empty?)
67
73
  end
@@ -113,7 +119,7 @@ module KKGit
113
119
  def ensure_ok!(ok, title, stdout: nil, stderr: nil, repo_dir: '.')
114
120
  return if ok
115
121
 
116
- msg = +"#{title} failed"
122
+ msg = "#{title} failed"
117
123
  msg << "\n#{stderr}" unless stderr.to_s.strip.empty?
118
124
  msg << "\n#{stdout}" unless stdout.to_s.strip.empty?
119
125
 
@@ -318,6 +324,8 @@ module KKGit
318
324
  return :noop
319
325
  end
320
326
 
327
+ AutoFormat.run!(repo_dir: repo_dir) unless skip_format?
328
+
321
329
  add_all!(repo_dir: repo_dir)
322
330
  ensure_no_sensitive_staged!(repo_dir: repo_dir)
323
331
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KKGit
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.5'
5
5
  end
data/lib/kk/git.rb CHANGED
@@ -5,8 +5,8 @@ require_relative 'git/ruby_warnings'
5
5
  require_relative 'git/git_runner'
6
6
  require_relative 'git/commit_message'
7
7
  require_relative 'git/git_ops'
8
+ require_relative 'git/auto_format'
8
9
  require_relative 'git/release'
9
10
 
10
11
  module KKGit
11
12
  end
12
-
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.3
4
+ version: 0.2.5
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