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.



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

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.



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

def info
  @info
end

#main_branchObject

Returns the value of attribute main_branch.



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

def main_branch
  @main_branch
end

#original_branchesObject

Populates and returns a hash containing the original version of branches



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

def original_branches
  @original_branches
end

#release_branchesObject

Returns the value of attribute release_branches.



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

def release_branches
  @release_branches
end

#staging_branchObject

Returns the value of attribute staging_branch.



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

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:



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

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

.versionObject

Returns the version of the git executable running on the system



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

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



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

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)


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

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



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

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

#center_pad(string = '', pad: '-', len: 80) ⇒ Object

Deprecated.

Please use Common#center_pad instead

Center the string and pad on either side with the given padding character



440
441
442
443
# File 'lib/firespring_dev_commands/git.rb', line 440

def center_pad(string = '', pad: '-', len: 80)
  warn '[DEPRECATION] `Dev::Git#center_pad` is deprecated. Please use `Dev::Common#center_pad` instead.'
  Dev::Common.new.center_pad(string, pad:, len:)
end

#changes(dir: default_project_dir) ⇒ Object

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



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

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



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

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



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

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



207
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
# File 'lib/firespring_dev_commands/git.rb', line 207

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



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

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: 'firespring', 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)



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

def clone_repo(dir:, repo_name:, repo_org: 'firespring', branch: nil, depth: nil)
  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



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

def clone_repos
  info.each { |it| clone_repo(dir: it.path, repo_name: it.name) }
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



237
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
# File 'lib/firespring_dev_commands/git.rb', line 237

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



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

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



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

def default_project_dir
  project_dirs.first
end

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

Split on newlines and add additional padding



434
435
436
# File 'lib/firespring_dev_commands/git.rb', line 434

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



429
430
431
# File 'lib/firespring_dev_commands/git.rb', line 429

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



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

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



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

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