Module: KKGit::GitOps

Defined in:
lib/kk/git/git_ops.rb

Overview

Git 仓库操作:供 Rake task 与 CLI 复用。

Defined Under Namespace

Classes: Error, Status

Constant Summary collapse

MUTATING_GIT_COMMANDS =

会修改仓库状态的 git 子命令;dry-run 时跳过这些命令

%w[add commit push pull merge rebase checkout reset cherry-pick revert].freeze

Class Method Summary collapse

Class Method Details

.add_all!Object



213
214
215
216
217
# File 'lib/kk/git/git_ops.rb', line 213

def add_all!
  paths = add_paths
  out, err, ok = run_cmd('git', 'add', *paths)
  ensure_ok!(ok, 'git add', stdout: out, stderr: err)
end

.add_pathsObject

git add 路径,默认 ‘.`;可用 KK_GIT_ADD_PATHS 指定多个路径(空格分隔)



59
60
61
# File 'lib/kk/git/git_ops.rb', line 59

def add_paths
  ENV.fetch('KK_GIT_ADD_PATHS', '.').split(/\s+/).reject(&:empty?)
end

.ahead_count(remote, branch) ⇒ Object

相对 upstream / remote/branch 领先的 commit 数



130
131
132
133
134
135
136
137
138
# File 'lib/kk/git/git_ops.rb', line 130

def ahead_count(remote, branch)
  out, _err, ok = run_cmd('git', 'rev-list', '--count', '@{u}..HEAD')
  return out.strip.to_i if ok

  out, _err, ok = run_cmd('git', 'rev-list', '--count', "#{remote}/#{branch}..HEAD")
  return out.strip.to_i if ok

  0
end

.amend?Boolean

Returns:

  • (Boolean)


45
46
47
# File 'lib/kk/git/git_ops.rb', line 45

def amend?
  ENV['KK_GIT_AMEND'] == '1'
end

.auto_commit_push!(commit_message_generator: nil) ⇒ Symbol

自动 add → commit → pull → push 主流程

Returns:

  • (Symbol)

    :synced | :committed_and_synced | :noop



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/kk/git/git_ops.rb', line 242

def auto_commit_push!(commit_message_generator: nil)
  ensure_in_repo!
  remote_name = remote
  branch_name = branch

  if working_tree_clean?
    if status(remote: remote_name, branch: branch_name).needs_sync?
      sync_with_remote!(remote_name, branch_name)
      return :synced
    end

    puts 'No changes to commit or push'
    return :noop
  end

  add_all!

  message =
    if commit_message_generator
      commit_message_generator.call
    else
      KKGit::CommitMessage.generate(mode: :all)
    end
  message = message.to_s.strip
  message = "chore(repo): update project files\n\n#{Time.now}" if message.empty?

  committed = commit_with_message!(message)
  if committed || unpushed_commits?(remote_name, branch_name)
    sync_with_remote!(remote_name, branch_name)
    return committed ? :committed_and_synced : :synced
  end

  :noop
end

.behind_count(remote, branch) ⇒ Object

相对 upstream / remote/branch 落后的 commit 数



141
142
143
144
145
146
147
148
149
# File 'lib/kk/git/git_ops.rb', line 141

def behind_count(remote, branch)
  out, _err, ok = run_cmd('git', 'rev-list', '--count', 'HEAD..@{u}')
  return out.strip.to_i if ok

  out, _err, ok = run_cmd('git', 'rev-list', '--count', "HEAD..#{remote}/#{branch}")
  return out.strip.to_i if ok

  0
end

.branch(explicit: ENV['KK_GIT_BRANCH']) ⇒ Object

Parameters:

  • explicit (String, nil) (defaults to: ENV['KK_GIT_BRANCH'])

    KK_GIT_BRANCH 或显式传入



54
55
56
# File 'lib/kk/git/git_ops.rb', line 54

def branch(explicit: ENV['KK_GIT_BRANCH'])
  explicit.to_s.strip.empty? ? current_branch : explicit.to_s.strip
end

.commit_with_message!(message) ⇒ Boolean

Returns commit 是否成功.

Returns:

  • (Boolean)

    commit 是否成功



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/kk/git/git_ops.rb', line 220

def commit_with_message!(message)
  commit_args = amend? ? %w[commit --amend -F] : %w[commit -F]

  Tempfile.create('commit_message') do |f|
    f.write(message)
    f.flush
    out, err, ok = run_cmd('git', *commit_args, f.path)
    if ok
      true
    elsif err.include?('nothing to commit') || out.include?('nothing to commit')
      puts 'No staged changes to commit'
      false
    else
      ensure_ok!(ok, 'git commit', stdout: out, stderr: err)
      false
    end
  end
end

.current_branchObject



104
105
106
107
108
# File 'lib/kk/git/git_ops.rb', line 104

def current_branch
  out, err, ok = run_cmd('git', 'rev-parse', '--abbrev-ref', 'HEAD')
  ensure_ok!(ok, 'Get current branch', stdout: out, stderr: err)
  out.strip
end

.detached_head?Boolean

Returns:

  • (Boolean)


110
111
112
# File 'lib/kk/git/git_ops.rb', line 110

def detached_head?
  current_branch == 'HEAD'
end

.dry_run?Boolean

Returns KK_GIT_DRY_RUN=1 时只打印命令不执行.

Returns:

  • (Boolean)

    KK_GIT_DRY_RUN=1 时只打印命令不执行



33
34
35
# File 'lib/kk/git/git_ops.rb', line 33

def dry_run?
  ENV['KK_GIT_DRY_RUN'] == '1'
end

.ensure_in_repo!Object

Raises:



100
101
102
# File 'lib/kk/git/git_ops.rb', line 100

def ensure_in_repo!
  raise Error, 'Not a git repository' unless in_git_repo?
end

.ensure_not_detached!Object

Raises:



114
115
116
# File 'lib/kk/git/git_ops.rb', line 114

def ensure_not_detached!
  raise Error, 'Cannot push from detached HEAD' if detached_head?
end

.ensure_ok!(ok, title, stdout: nil, stderr: nil) ⇒ Object

Raises:



86
87
88
89
90
91
92
93
# File 'lib/kk/git/git_ops.rb', line 86

def ensure_ok!(ok, title, stdout: nil, stderr: nil)
  return if ok

  msg = +"#{title} failed"
  msg << "\n#{stderr}" unless stderr.to_s.strip.empty?
  msg << "\n#{stdout}" unless stdout.to_s.strip.empty?
  raise Error, msg
end

.in_git_repo?Boolean

Returns:

  • (Boolean)


95
96
97
98
# File 'lib/kk/git/git_ops.rb', line 95

def in_git_repo?
  _, _, ok = run_cmd('git', 'rev-parse', '--git-dir')
  ok
end

.mutating_git_command?(cmd) ⇒ Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/kk/git/git_ops.rb', line 82

def mutating_git_command?(cmd)
  cmd[0] == 'git' && MUTATING_GIT_COMMANDS.include?(cmd[1])
end

.pull_remote!(remote, branch) ⇒ Object



186
187
188
189
190
191
192
# File 'lib/kk/git/git_ops.rb', line 186

def pull_remote!(remote, branch)
  return if skip_pull?

  pull_args = ENV.fetch('KK_GIT_PULL_ARGS', '--ff-only').split
  out, err, ok = run_cmd('git', 'pull', remote, branch, *pull_args)
  ensure_ok!(ok, 'git pull', stdout: out, stderr: err)
end

.push_remote!(remote, branch) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/kk/git/git_ops.rb', line 194

def push_remote!(remote, branch)
  return if skip_push?

  ensure_not_detached! unless dry_run?

  if upstream_configured?
    out, err, ok = run_cmd('git', 'push', remote, branch)
  else
    out, err, ok = run_cmd('git', 'push', '-u', remote, branch)
  end
  ensure_ok!(ok, 'git push', stdout: out, stderr: err)
end

.remoteObject



49
50
51
# File 'lib/kk/git/git_ops.rb', line 49

def remote
  ENV.fetch('KK_GIT_REMOTE', 'origin')
end

.run_cmd(*cmd, chdir: nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/kk/git/git_ops.rb', line 66

def run_cmd(*cmd, chdir: nil)
  if dry_run? && mutating_git_command?(cmd)
    label = chdir ? "(cd #{chdir} && #{cmd.join(' ')})" : cmd.join(' ')
    puts "[dry-run] #{label}"
    return ['', '', true]
  end

  stdout, stderr, status =
    if chdir
      Open3.capture3(*cmd, chdir: chdir)
    else
      Open3.capture3(*cmd)
    end
  [stdout.to_s, stderr.to_s, status.success?]
end

.skip_pull?Boolean

Returns:

  • (Boolean)


37
38
39
# File 'lib/kk/git/git_ops.rb', line 37

def skip_pull?
  ENV['KK_GIT_SKIP_PULL'] == '1'
end

.skip_push?Boolean

Returns:

  • (Boolean)


41
42
43
# File 'lib/kk/git/git_ops.rb', line 41

def skip_push?
  ENV['KK_GIT_SKIP_PUSH'] == '1'
end

.status(remote: nil, branch: nil) ⇒ Status

Returns:



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/kk/git/git_ops.rb', line 156

def status(remote: nil, branch: nil)
  ensure_in_repo!
  remote ||= self.remote
  branch ||= self.branch

  Status.new(
    branch: branch,
    remote: remote,
    clean: working_tree_clean?,
    ahead: ahead_count(remote, branch),
    behind: behind_count(remote, branch),
    upstream_configured: upstream_configured?,
    detached: detached_head?
  )
end

.status_hash(remote: nil, branch: nil) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/kk/git/git_ops.rb', line 172

def status_hash(remote: nil, branch: nil)
  s = status(remote: remote, branch: branch)
  {
    branch: s.branch,
    remote: s.remote,
    clean: s.clean,
    ahead: s.ahead,
    behind: s.behind,
    upstream_configured: s.upstream_configured,
    detached: s.detached,
    needs_sync: s.needs_sync?
  }
end

.sync_with_remote!(remote, branch) ⇒ Object



207
208
209
210
211
# File 'lib/kk/git/git_ops.rb', line 207

def sync_with_remote!(remote, branch)
  pull_remote!(remote, branch)
  push_remote!(remote, branch)
  puts "Synced: #{remote} #{branch}" unless dry_run?
end

.unpushed_commits?(remote, branch) ⇒ Boolean

Returns:

  • (Boolean)


151
152
153
# File 'lib/kk/git/git_ops.rb', line 151

def unpushed_commits?(remote, branch)
  ahead_count(remote, branch).positive?
end

.upstream_configured?Boolean

Returns:

  • (Boolean)


124
125
126
127
# File 'lib/kk/git/git_ops.rb', line 124

def upstream_configured?
  _, _, ok = run_cmd('git', 'rev-parse', '--abbrev-ref', '@{u}')
  ok
end

.working_tree_clean?Boolean

Returns:

  • (Boolean)


118
119
120
121
122
# File 'lib/kk/git/git_ops.rb', line 118

def working_tree_clean?
  out, err, ok = run_cmd('git', 'status', '--porcelain')
  ensure_ok!(ok, 'Check git status', stdout: out, stderr: err)
  out.strip.empty?
end