Class: Dev::Git

Inherits:
Object show all
Defined in:
lib/firespring_dev_commands/git.rb,
lib/firespring_dev_commands/git/info.rb

Overview

Class for performing git functions

Defined Under Namespace

Classes: Config, Info

Constant Summary collapse

DEFAULT_MAIN_BRANCH =

The default base branch to use

'master'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(main_branch: self.class.config.main_branch, staging_branch: self.class.config.staging_branch, info: self.class.config.info) ⇒ Git

Returns a new instance of Git.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/firespring_dev_commands/git.rb', line 43

def initialize(
  main_branch: self.class.config.main_branch,
  staging_branch: self.class.config.staging_branch,
  info: self.class.config.info
)
  @main_branch = main_branch
  raise 'main branch must be configured' if main_branch.to_s.empty?

  @staging_branch = staging_branch || main_branch
  @info = Array(info)
  raise 'git repositories must be configured' if @info.empty? || !@info.all?(Dev::Git::Info)

  check_version
end

Instance Attribute Details

#infoObject

Returns the value of attribute info.



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

def info
  @info
end

#main_branchObject

Returns the value of attribute main_branch.



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

def main_branch
  @main_branch
end

#original_branchesObject

Populates and returns a hash containing the original version of branches



78
79
80
# File 'lib/firespring_dev_commands/git.rb', line 78

def original_branches
  @original_branches
end

#release_branchesObject

Returns the value of attribute release_branches.



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

def release_branches
  @release_branches
end

#staging_branchObject

Returns the value of attribute staging_branch.



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

def staging_branch
  @staging_branch
end

Class Method Details

.config {|@config| ... } ⇒ Object Also known as: configure

Instantiates a new top level config object if one hasn’t already been created Yields that config object to any given block Returns the resulting config object

Yields:



26
27
28
29
30
# File 'lib/firespring_dev_commands/git.rb', line 26

def config
  @config ||= Config.new
  yield(@config) if block_given?
  @config
end

.versionObject

Returns the version of the git executable running on the system



36
37
38
# File 'lib/firespring_dev_commands/git.rb', line 36

def version
  @version ||= ::Git::Lib.new(nil, nil).current_command_version.join('.')
end

Instance Method Details

#add(*paths, dir: default_project_dir, raise_errors: false) ⇒ Object

Add the given paths to git Defaults to the current directory optionally raise errors



269
270
271
272
273
274
275
276
277
278
# File 'lib/firespring_dev_commands/git.rb', line 269

def add(*paths, dir: default_project_dir, raise_errors: false)
  g = ::Git.open(dir)
  indent g.add(paths)
  true
rescue ::Git::GitExecuteError => e
  raise e if raise_errors

  print_errors(e.message)
  false
end

#branch_exists?(project_dir, branch_name) ⇒ Boolean

Returns true if the remote branch exists, false otherwise

Returns:

  • (Boolean)


105
106
107
# File 'lib/firespring_dev_commands/git.rb', line 105

def branch_exists?(project_dir, branch_name)
  ::Git.ls_remote(project_dir)['remotes']["origin/#{branch_name}"]
end

#branch_name(dir: default_project_dir) ⇒ Object

Returns the branch name associated with the given repository Defaults to the current directory



97
98
99
100
101
102
# File 'lib/firespring_dev_commands/git.rb', line 97

def branch_name(dir: default_project_dir)
  return unless File.exist?(dir)

  g = ::Git.open(dir)
  g.current_branch || "HEAD detached at #{g.object('HEAD').sha[0..7]}"
end

#changes(dir: default_project_dir) ⇒ Object

Print the changes on the given repo Defaults to the current directory



145
146
147
148
149
# File 'lib/firespring_dev_commands/git.rb', line 145

def changes(dir: default_project_dir)
  return unless File.exist?(dir)

  Dir.chdir(dir) { `git status --porcelain | grep -v '^?'` }.split("\n").map(&:strip)
end

#changes_slow(dir: default_project_dir) ⇒ Object

Print the changes on the given repo using the ruby built-in method… which seems REALLY slow compared to the porcelain version Defaults to the current directory



153
154
155
156
157
158
159
160
# File 'lib/firespring_dev_commands/git.rb', line 153

def changes_slow(dir: default_project_dir)
  return unless File.exist?(dir)

  s = ::Git.open(dir).status
  s.added.keys.map { |it| " A #{it}" } +
    s.changed.keys.map { |it| " M #{it}" } +
    s.deleted.keys.map { |it| " D #{it}" }
end

#check_versionObject

Checks the min and max version against the current git version if they have been configured



59
60
61
62
63
64
65
# File 'lib/firespring_dev_commands/git.rb', line 59

def check_version
  min_version = self.class.config.min_version
  raise "requires git version >= #{min_version} (found #{self.class.version})" if min_version && !Dev::Common.new.version_greater_than(min_version, self.class.version)

  max_version = self.class.config.max_version
  raise "requires git version < #{max_version} (found #{self.class.version})" if max_version && Dev::Common.new.version_greater_than(max_version, self.class.version)
end

#checkout(branch, dir: default_project_dir, raise_errors: false) ⇒ Object

Checks out the given branch in the given repo Defaults to the current directory optionally raise errors



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/firespring_dev_commands/git.rb', line 208

def checkout(branch, dir: default_project_dir, raise_errors: false)
  raise 'branch is required' if branch.to_s.strip.empty?
  return unless File.exist?(dir)

  # Make sure the original branch hash has been created before we change anything
  original_branches

  g = ::Git.open(dir)
  g.fetch('origin', prune: true)

  # If the branch we are checking out doesn't exist, check out either the staging branch or the main branch
  actual_branch = branch
  unless branch_exists?(dir, branch)
    actual_branch = [staging_branch, main_branch].uniq.find { |it| branch_exists?(dir, it) }
    puts "Branch #{branch} not found, checking out #{actual_branch} instead".light_yellow
  end

  indent g.checkout(actual_branch)
  indent g.pull('origin', actual_branch)
  true
rescue ::Git::GitExecuteError => e
  raise e if raise_errors

  print_errors(e.message)
  false
end

#checkout_all(branch) ⇒ Object

Checks out the given branch in all repositories with some additional formatting



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/firespring_dev_commands/git.rb', line 188

def checkout_all(branch)
  @success = true
  puts
  puts "Checking out #{branch} in each repo".light_yellow if project_dirs.length > 1
  project_dirs.each do |project_dir|
    next unless File.exist?(project_dir)

    repo_basename = File.basename(File.realpath(project_dir))
    puts Dev::Common.new.center_pad(repo_basename).light_green
    @success &= checkout(branch, dir: project_dir)
    puts Dev::Common.new.center_pad.light_green
  end
  puts

  raise "Failed checking out branch #{branch} one or more repositories" unless @success
end

#clone_repo(dir:, repo_name:, repo_org: nil, branch: nil, depth: nil) ⇒ Object

Clones the repo_name into the dir Optionally specify a repo_org Optionally specify a branch to check out (defaults to the repository default branch)



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/firespring_dev_commands/git.rb', line 412

def clone_repo(dir:, repo_name:, repo_org: nil, branch: nil, depth: nil)
  # TODO: Split out the default of 'firespring' into a configuration variable
  repo_org = 'firespring' if repo_org.to_s.strip.empty?

  if Dir.exist?("#{dir}/.git")
    puts "#{dir} already cloned".light_green
    return
  end

  FileUtils.mkdir_p(dir.to_s)

  puts "Cloning #{dir} from #{ssh_repo_url(repo_name, repo_org)}".light_yellow

  opts = {}
  opts[:branch] = branch unless branch.to_s.strip.empty?
  opts[:depth] = depth unless depth.to_s.strip.empty?
  g = ::Git.clone(ssh_repo_url(repo_name, repo_org), dir, opts)
  g.fetch('origin', prune: true)
end

#clone_reposObject

Clones all repositories



405
406
407
# File 'lib/firespring_dev_commands/git.rb', line 405

def clone_repos
  info.each { |it| clone_repo(dir: it.path, repo_name: it.name) }
end

#commit_status(token:, repo_name:, commit_id:, status:, repo_org: nil, options: {}) ⇒ Object



432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/firespring_dev_commands/git.rb', line 432

def commit_status(token:, repo_name:, commit_id:, status:, repo_org: nil, options: {})
  # TODO: Split out the default of 'firespring' into a configuration variable
  repo_org = 'firespring' if repo_org.to_s.strip.empty?
  repo = "#{repo_org}/#{repo_name}"

  # Set up the GitHub client
  client = Octokit::Client.new(access_token: token)

  # Create the commit status
  puts "Tagging commit #{commit_id} in #{repo} as #{status} for #{options[:context]}"
  client.create_status(repo, commit_id, status, options)
end

#create_branch(branch, dir: default_project_dir, raise_errors: false) ⇒ Object

Create the given branch in the given repo Defaults to the current directory optionally raise errors



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/firespring_dev_commands/git.rb', line 238

def create_branch(branch, dir: default_project_dir, raise_errors: false)
  raise 'branch is required' if branch.to_s.strip.empty?
  raise "refusing to create protected branch '#{branch}'" if %w(master develop).any?(branch.to_s.strip)
  return unless File.exist?(dir)

  # Make sure the original branch hash has been created before we change anything
  original_branches

  g = ::Git.open(dir)
  g.fetch('origin', prune: true)

  puts "Fetching the latest changes for base branch \"#{staging_branch}\""
  g.checkout(staging_branch)
  g.pull('origin', staging_branch)

  puts "Creating branch #{branch}, pushing to origin, and updating remote tracking"
  g.branch(branch).checkout
  g.push('origin', branch)
  g.config("branch.#{branch}.remote", 'origin')
  g.config("branch.#{branch}.merge", "refs/heads/#{branch}")
  puts
rescue ::Git::GitExecuteError => e
  raise e if raise_errors

  print_errors(e.message)
  false
end

#current_branchesObject

Returns a hash of each project repo and the branch that is currently checked out



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/firespring_dev_commands/git.rb', line 83

def current_branches
  {}.tap do |hsh|
    project_dirs.each do |project_dir|
      next unless File.exist?(project_dir)

      Dir.chdir(project_dir) do
        hsh[project_dir] = branch_name(dir: project_dir)
      end
    end
  end
end

#default_project_dirObject

Returns the first configured project dire



73
74
75
# File 'lib/firespring_dev_commands/git.rb', line 73

def default_project_dir
  project_dirs.first
end

#indent(string, padding: ' ') ⇒ Object

Split on newlines and add additional padding



451
452
453
# File 'lib/firespring_dev_commands/git.rb', line 451

def indent(string, padding: '  ')
  string.to_s.split("\n").each { |line| puts "#{padding}#{line}" }
end

#merge(branch, dir: default_project_dir, raise_errors: false) ⇒ Object

Merge the given branch into the given repo Defaults to the current directory optionally raise errors



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/firespring_dev_commands/git.rb', line 303

def merge(branch, dir: default_project_dir, raise_errors: false)
  raise 'branch is required' if branch.to_s.strip.empty?
  return unless File.exist?(dir)

  # Make sure the original branch hash has been created before we change anything
  original_branches

  g = ::Git.open(dir)
  g.fetch('origin', prune: true)
  raise 'branch does not exist' unless branch_exists?(dir, branch)

  # No need to merge into ourself
  current_branch = branch_name(dir:)
  return true if current_branch == branch

  indent "Merging #{branch} into #{current_branch}"
  indent g.merge(branch)
  true
rescue ::Git::GitExecuteError => e
  raise e if raise_errors

  print_errors(e.message)
  false
end

#merge_all(branch) ⇒ Object

Merge the branch into all repositories



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/firespring_dev_commands/git.rb', line 281

def merge_all(branch)
  @success = true
  puts
  puts "Merging #{branch} into each repo".light_yellow if project_dirs.length > 1
  project_dirs.each do |project_dir|
    next unless File.exist?(project_dir)

    repo_basename = File.basename(File.realpath(project_dir))
    puts Dev::Common.new.center_pad(repo_basename).light_green
    @success &= merge(branch, dir: project_dir)
    puts Dev::Common.new.center_pad.light_green
  end
  puts

  raise "Failed merging branch #{branch} in one or more repositories" unless @success

  push_all
end

#project_dirsObject

Returns all git paths configured in our info



68
69
70
# File 'lib/firespring_dev_commands/git.rb', line 68

def project_dirs
  @project_dirs ||= @info.map(&:path).sort
end

#pull(dir: default_project_dir, raise_errors: false) ⇒ Object

Pull the given repo Defaults to the current directory optionally raise errors



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/firespring_dev_commands/git.rb', line 349

def pull(dir: default_project_dir, raise_errors: false)
  return unless File.exist?(dir)

  g = ::Git.open(dir)
  g.fetch('origin', prune: true)

  branch = branch_name(dir:)
  indent "Pulling branch #{branch} from origin"
  indent g.pull('origin', branch)
  true
rescue ::Git::GitExecuteError => e
  raise e if raise_errors

  print_errors(e.message)
  false
end

#pull_allObject

Pull the latest in all repositories



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/firespring_dev_commands/git.rb', line 329

def pull_all
  @success = true
  puts
  puts 'Pulling current branch into each repo'.light_yellow if project_dirs.length > 1
  project_dirs.each do |project_dir|
    next unless File.exist?(project_dir)

    repo_basename = File.basename(File.realpath(project_dir))
    puts Dev::Common.new.center_pad(repo_basename).light_green
    @success &= pull(dir: project_dir)
    puts Dev::Common.new.center_pad.light_green
  end
  puts

  raise 'Failed pulling branch in one or more repositories' unless @success
end

#push(dir: default_project_dir, raise_errors: false) ⇒ Object

Push the given repo Defaults to the current directory optionally raise errors



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/firespring_dev_commands/git.rb', line 387

def push(dir: default_project_dir, raise_errors: false)
  return unless File.exist?(dir)

  g = ::Git.open(dir)
  g.fetch('origin', prune: true)

  branch = branch_name(dir:)
  indent "Pushing branch #{branch} to origin"
  indent g.push('origin', branch)
  true
rescue ::Git::GitExecuteError => e
  raise e if raise_errors

  print_errors(e.message)
  false
end

#push_allObject

Push to remote in all repositories



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/firespring_dev_commands/git.rb', line 367

def push_all
  @success = true
  puts
  puts 'Pushing current branch into each repo'.light_yellow if project_dirs.length > 1
  project_dirs.each do |project_dir|
    next unless File.exist?(project_dir)

    repo_basename = File.basename(File.realpath(project_dir))
    puts Dev::Common.new.center_pad(repo_basename).light_green
    @success &= push(dir: project_dir)
    puts Dev::Common.new.center_pad.light_green
  end
  puts

  raise 'Failed pushing branch in one or more repositories' unless @success
end

#repos_with_changesObject

Returns the name of any repositories which have changes



139
140
141
# File 'lib/firespring_dev_commands/git.rb', line 139

def repos_with_changes
  info.filter_map { |it| it.name unless changes(dir: it.path).empty? }
end

#reset(dir: default_project_dir) ⇒ Object

Runs a git reset on the given repo Defaults to the current directory



180
181
182
183
184
185
# File 'lib/firespring_dev_commands/git.rb', line 180

def reset(dir: default_project_dir)
  return unless File.exist?(dir)

  g = ::Git.open(dir)
  indent g.reset_hard
end

#reset_allObject

Runs a git reset on all given repositories with some additional formatting



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/firespring_dev_commands/git.rb', line 163

def reset_all
  puts
  puts 'Resetting each repo'.light_yellow if project_dirs.length > 1
  project_dirs.each do |project_dir|
    next unless File.exist?(project_dir)

    repo_basename = File.basename(File.realpath(project_dir))
    header = "#{repo_basename} (#{original_branches[project_dir]})"
    puts Dev::Common.new.center_pad(header).light_green
    reset(dir: project_dir)
    puts Dev::Common.new.center_pad.light_green
  end
  puts
end

#ssh_repo_url(name, org) ⇒ Object

Builds an ssh repo URL using the org and repo name given



446
447
448
# File 'lib/firespring_dev_commands/git.rb', line 446

def ssh_repo_url(name, org)
  "[email protected]:#{org}/#{name}.git"
end

#status(dir: default_project_dir) ⇒ Object

Prints the results of the status command Currently running “git status” instead of using the library because it doesn’t do well formatting the output



130
131
132
133
134
135
136
# File 'lib/firespring_dev_commands/git.rb', line 130

def status(dir: default_project_dir)
  return unless File.exist?(dir)

  # NOTE: git library doesn't have a good "status" analog. So just run the standard "git" one
  # splitting and puts'ing to prefix each line with spaces...
  Dir.chdir(dir) { indent `git status` }
end

#status_allObject

Prints the status of multiple repository directories and displays the results in a nice format



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/firespring_dev_commands/git.rb', line 110

def status_all
  @success = true
  puts
  puts 'Getting status in each repo'.light_yellow if project_dirs.length > 1
  project_dirs.each do |project_dir|
    next unless File.exist?(project_dir)

    repo_basename = File.basename(File.realpath(project_dir))
    header = "#{repo_basename} (#{original_branches[project_dir]})"
    puts Dev::Common.new.center_pad(header).light_green
    @success &= status(dir: project_dir)
    puts Dev::Common.new.center_pad.light_green
  end
  puts

  raise 'Failed getting status on one or more repositories' unless @success
end