Class: Gitscape::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/gitscape/base.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeBase

Returns a new instance of Base.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/gitscape/base.rb', line 6

def initialize

  # Always add a merge commit at the end of a merge
  @merge_options = "--no-ff"
  
  # Setup additional merge options based on the version of Git we have
  if git_version_at_least "1.7.4.0"
    @merge_options += " -s recursive -Xignore-space-change"
  else
    warn "Ignoring whitespace changes in merges is only available on Git 1.7.4+"
  end
  
  @env_branch_by_dev_branch = Hash.new do |h, k|  
    case k
      when "master"
        "staging"
      when /release\/i\d+/
        "qa"
      when "live"
        "live"
    end
  end

end

Class Method Details

.commit_exists?(commit_id) ⇒ Boolean

Returns true if the supplied Git commit hash or reference exists

Returns:

  • (Boolean)


376
377
378
379
380
381
382
383
# File 'lib/gitscape/base.rb', line 376

def self.commit_exists?(commit_id)
  `git rev-parse #{commit_id}`
  if $? == 0
    true
  else
    raise "Invalid commit/ref ID: #{commit_id}"
  end
end

Instance Method Details

#bugfix_finish(branch_name = nil, options = {:env_depth=>:staging, :push=>true, :update_env=>false}) ⇒ Object



130
131
132
# File 'lib/gitscape/base.rb', line 130

def bugfix_finish branch_name=nil, options={:env_depth=>:staging, :push=>true, :update_env=>false}
  generic_branch_finish 'bugfix', branch_name, options
end

#bugfix_start(new_branch = nil, options = {:push=>false}) ⇒ Object



113
114
115
116
117
118
119
120
# File 'lib/gitscape/base.rb', line 113

def bugfix_start new_branch=nil, options={:push=>false}
  name = current_release_branch_name
  if name.nil?
    puts 'There is not a current release branch. You cannot use this command.'
  else
    generic_branch_start 'bugfix', name, new_branch, options
  end
end

#current_branch_nameObject



57
58
59
60
# File 'lib/gitscape/base.rb', line 57

def current_branch_name
  toRet = `git branch`.scan(/\* (.*)$/).flatten[0]
  toRet
end

#current_release_branch_nameObject



70
71
72
73
74
75
76
77
# File 'lib/gitscape/base.rb', line 70

def current_release_branch_name
  release_number = current_release_branch_number
  if !release_number.nil?
    "release/i#{current_release_branch_number}"
  else
    nil
  end
end

#current_release_branch_numberObject



62
63
64
65
66
67
68
# File 'lib/gitscape/base.rb', line 62

def current_release_branch_number
  unmerged_into_live_branch_names = `git branch -a --no-merged origin/live`.split("\n")
  release_branch_regex = /release\/i(\d+)$/

  candidates = unmerged_into_live_branch_names.select{ |b| release_branch_regex.match b}.map{|b| b.scan(release_branch_regex).flatten[0].to_i}.sort
  candidates.last
end

#feature_finish(branch_name = nil, options = {:env_depth=>:staging, :push=>true, :update_env=>false}) ⇒ Object



134
135
136
# File 'lib/gitscape/base.rb', line 134

def feature_finish branch_name=nil, options={:env_depth=>:staging, :push=>true, :update_env=>false}
  generic_branch_finish 'feature', branch_name, options
end

#feature_start(new_branch = nil, options = {:push=>false}) ⇒ Object



122
123
124
# File 'lib/gitscape/base.rb', line 122

def feature_start new_branch=nil, options={:push=>false}
  generic_branch_start 'feature', 'master', new_branch, options
end

#finish_usage_string(branch_type) ⇒ Object



410
411
412
413
414
# File 'lib/gitscape/base.rb', line 410

def finish_usage_string(branch_type)
  "expected usage: #{branch_type}_finish [<#{branch_type}_branch>]
  #{branch_type}_branch: the name of the #{branch_type} branch to finish.
    if omitted, you must currently be on a #{branch_type} branch"
end

#generic_branch_finish(branch_type, source_name, options) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/gitscape/base.rb', line 138

def generic_branch_finish branch_type, source_name, options
  # option defaults
  options[:env_depth]   = :staging  if options[:env_depth].nil?
  options[:push]        = true      if options[:push].nil?
  options[:update_env]  = false     if options[:update_env].nil?

  if (options[:env_depth] == :qa) && branch_type == 'feature'
    puts "*** --qa may not be used with feature branches"
    exit 1
  end
  if (options[:env_depth] == :live) && branch_type != 'hotfix'
    puts "*** --live may only be used with hotfix branches"
    exit 1
  end


  # Check that the working copy is clean
  exit 1 unless git_working_copy_is_clean

  source_branch = "#{branch_type}/#{source_branch}"

  previous_branch = current_branch_name
  if previous_branch.to_s.start_with? "#{branch_type}/"
    source_branch = previous_branch
  end

  if source_branch.to_s.empty?
    puts "!!! Not currently on a #{branch_type} branch, and no branch name was provided as an argument !!!"
    puts finish_usage_string(branch_type)
    exit 1
  end

  # Collect the set of branches we'd like to merge the hotfix into
  merge_branches = ["master"]
  if %w{bugfix hotfix}.include?(branch_type)
    if current_release_branch_name.nil?
      puts "!!! There is no current release branch: the command will bypass the release and qa branches"
    else
      merge_branches << current_release_branch_name if [:qa, :live].include?(options[:env_depth])
    end
  end
  if %w{hotfix}.include?(branch_type)
    merge_branches << "live" if options[:env_depth] == :live
  end

  # Merge the source branch into merge_branches
  puts "=== Merging #{branch_type} into branches #{merge_branches} ==="
  for branch in merge_branches

    # Calculate merge_options
    merge_options = @merge_options
    merge_options += " --log" if branch == "master"

    # Attempt merge
    puts `git checkout #{branch}`
    puts `git pull`
    puts `git merge #{merge_options} #{source_branch}`
    
    # Bail on failures
    exit 1 if !$?.success?
    raise "Merge failure(s) on #{branch}.\nResolve conflicts, and run the script again." if git_has_conflicts
    
    puts `git push origin #{branch}` if options[:push]
    puts `git push origin #{branch}:#{@env_branch_by_dev_branch[branch]}` if options[:update_env]
    
    # If we just merged the live branch, tag this revision, and push that tag to origin
    if branch == "live"
      puts `git tag live/i#{live_iteration}/#{source_branch}`
      puts `git push --tags`
    end

  end

  # Checkout previous branch for user convenience
  `git checkout #{previous_branch}`
end

#generic_branch_start(branch_type, from_branch, new_branch, options) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/gitscape/base.rb', line 88

def generic_branch_start branch_type, from_branch, new_branch, options
  # option defaults
  options[:push] = false if options[:push].nil?
  
  # Check that the working copy is clean
  exit 1 unless git_working_copy_is_clean
  
  if new_branch.to_s.length == 0
    raise "*** Improper Usage ***\nExpected Usage: #{branch_type}_start <#{branch_type}_name> [--[no-]push]"
  end

  puts `git checkout #{from_branch}`
  puts `git pull origin #{from_branch}`

  new_branch = "#{branch_type}/#{new_branch}"
  puts "=== Creating #{branch_type} branch '#{new_branch}' ==="

  puts `git checkout -b #{new_branch}`
  puts `git push origin #{new_branch}` if options[:push]
end

#git_has_conflicts(puts_conflicts = true) ⇒ Object



79
80
81
82
83
84
85
86
# File 'lib/gitscape/base.rb', line 79

def git_has_conflicts puts_conflicts=true
  conflicts_status = `git status --porcelain`
  has_conflicts = conflicts_status.scan(/[AUD]{2}/).count > 0

  puts conflicts_status if has_conflicts && puts_conflicts

  has_conflicts
end

#git_versionObject

Get the system’s current git version



386
387
388
# File 'lib/gitscape/base.rb', line 386

def git_version
  @git_version ||= `git --version`.match(/\d+(?:\.\d+)+/).to_s
end

#git_version_at_least(min_version) ⇒ Object

Check if the system’s git version is at least as recent as the version specified



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/gitscape/base.rb', line 391

def git_version_at_least(min_version)
  def split_version(v)
    v.split(".").map { |x| x.to_i }
  end
  local_version = split_version(git_version)
  min_version = split_version(min_version)

  raise "Git version string must have 4 parts" if min_version.size != 4
  4.times do |i|
    if local_version[i].nil?
      return false
    end
    next if local_version[i] == min_version[i]
    return local_version[i] > min_version[i]
  end

  true # If you get all the way here, all 4 positions match precisely
end

#git_working_copy_is_clean(puts_changes = true) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/gitscape/base.rb', line 31

def git_working_copy_is_clean puts_changes=true

  # Check if the working copy is clean, if not, exit
  
  changes = `git status -uno --ignore-submodules=all --porcelain`
  working_copy_clean = changes.length == 0
  if !working_copy_clean && puts_changes
    puts "*** Your working copy is not clean, either commit, stash, or reset your changes then try again. ***"
    puts changes
  end

  working_copy_clean
end

#hotfix_finish(branch_name = nil, options = {:env_depth=>:staging, :push=>true, :update_env=>false}) ⇒ Object



126
127
128
# File 'lib/gitscape/base.rb', line 126

def hotfix_finish branch_name=nil, options={:env_depth=>:staging, :push=>true, :update_env=>false}
  generic_branch_finish 'hotfix', branch_name, options
end

#hotfix_start(new_branch = nil, options = {:push=>false}) ⇒ Object



109
110
111
# File 'lib/gitscape/base.rb', line 109

def hotfix_start new_branch=nil, options={:push=>false}
  generic_branch_start 'hotfix', 'live', new_branch, options
end

#live_iterationObject

Assume the highest branch already merged into live of the form release/i+ is the live branch



47
48
49
50
51
52
53
54
55
# File 'lib/gitscape/base.rb', line 47

def live_iteration
  live_iteration_tag_regex = /^live\/i(\d+)/
  toRet = `git tag`.split("\n").select { |tag| live_iteration_tag_regex.match tag }.map { |tag| tag.scan(live_iteration_tag_regex).flatten[0].to_i }.sort.last
  # A bit of a chicken-and-egg problem. You might not have any tags for live, so look for something else...
  if toRet.nil?
    toRet = `git branch -a --merged origin/live`.split("\n").select{|b| /release\/i(\d+)$/.match b}.map{|b| b.scan(/release\/i(\d+)$/).flatten[0].to_i}.sort.last
  end
  toRet
end

#release_finish(options = {:push=>true, :update_env=>true}) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/gitscape/base.rb', line 255

def release_finish options={:push=>true, :update_env=>true}
  # Handle default options
  options[:push]        = true  if options[:push].nil?
  options[:update_env]  = true  if options[:update_env].nil?
  
  # Check if the working copy is clean, if not, exit
  exit 1 unless git_working_copy_is_clean

  # Do a git fetch to ensure everything all refs are sync with origin
  puts `git fetch`

  # Get the right release_branch_name to merge
  current_version_number = live_iteration
  new_version_number = current_version_number + 1
  
  release_branch = "release/i#{new_version_number}"

  # Fetch in order to have the latest branch revisions

  # Get branch information for checks
  branch_keys = ["name", "revision", "message"]
  branch_values = `git branch -av`.scan(/^[ \*]*([^ \*]+) +([^ ]+) +(.*)$/)
  branches = branch_values.map {|components| Hash[ branch_keys.zip components ] }
  branch_revisions = Hash[ branches.map {|branch| [branch["name"], branch["revision"]] } ]

  # Check if the required branches in sync
  required_synced_branches = [[release_branch, "remotes/origin/qa"]]
  required_synced_branches.each do |branch_pair|
    if branch_revisions[ branch_pair[0] ] != branch_revisions[ branch_pair[0] ]
      puts "*** ERROR: The #{branch_pair[0]} branch is not the same as the #{branch_pair[1]} branch.
      \tPlease resolve this and try again."
      exit 3
    end
  end

  # Checkout and pull release_branch
  puts `git checkout #{release_branch}`
  puts `git pull origin #{release_branch}`

  # Checkout and pull live
  puts `git checkout live`
  puts `git pull origin live`

  merge_options = "--no-ff -s recursive -Xignore-space-change"

  # Merge the release branch into live
  puts `git merge #{merge_options} #{release_branch}`

  # Error and conflict checking
  if !$?.success? then exit 4 end
  if git_has_conflicts then
    puts "Merge conflicts when pulling #{release_branch} into live"
    puts "Please report a problem if you see this message :)"
    exit 2
  end

  # Ensure there is zero diff between what was tested on origin/qa and the new live
  critical_diff = `git diff live origin/qa`
  if critical_diff.length > 0
    puts "\n!!! This live merge has code that was not on the qa branch !!!\nDiff:"
    puts critical_diff
    puts "!!! Run the command 'git reset --hard' to undo the merge, and raise this error with QA and others involved to determine next step !!!"
    exit 3
  end

  # Record the revision of live used for the release tag
  live_release_revision = `git log -n1 --oneline`.scan(/(^[^ ]+) .*$/).flatten[0]

  # Merge the release branch into master 
  puts `git checkout master`
  puts `git pull origin master`
  puts `git merge #{merge_options} #{release_branch}`

  # Error and conflict checking
  if !$?.success? then exit 4 end
  if git_has_conflicts then
    puts "Merge conflicts when pulling #{release_branch} into master"
    puts "Please report a problem if you see this message :)"
    exit 2
  end

  # Tag the state of live for both release and rollback
  puts `git tag rollback-to/i#{current_version_number} live~`
  if !$?.success? then
    puts "=== WARNING: Failed to create rollback-to/i#{current_version_number} tag"
  end

  `git tag live/i#{new_version_number}/release #{live_release_revision}`
  if !$?.success? then
    puts "=== WARNING: Failed to create live/i#{new_version_number}/release"
    puts `git tag -d rollback-to/i#{current_version_number}`
    exit 4
  end
  
  if options[:push] && options[:update_env]
    puts `git push origin live --tags`
    puts `git push origin master`
  end
end

#release_start(options = {:push=>true, :update_env=>true}) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/gitscape/base.rb', line 215

def release_start options={:push=>true, :update_env=>true}
  # Handle default options
  options[:push]        = true  if options[:push].nil?
  options[:update_env]  = true  if options[:update_env].nil?
  
  # Switch to master branch
  puts `git checkout master`
  puts `git pull origin master`

  # Check that the working copy is clean
  exit 1 unless git_working_copy_is_clean

  new_version_number = live_iteration + 1
  release_branch_name = "release/i#{new_version_number}"

  # Cut the branch
  puts `git checkout -b "#{release_branch_name}" master`
  exit 1 unless $?.exitstatus == 0

  # Bump the version number
  `echo "i#{new_version_number}" > ./version`
  exit 1 unless $?.exitstatus == 0

  # Commit the bump
  puts `git commit -a -m "Begin i#{new_version_number} release candidate"`
  exit 1 unless $?.exitstatus == 0
  
  # Push to origin
  if options[:push]
    puts `git push origin -u "#{release_branch_name}"`
    exit 1 unless $?.exitstatus == 0
  end

  # Update qa to the new commit
  if options[:update_env]
    puts `git push origin "#{release_branch_name}:qa"`
    exit 1 unless $?.exitstatus == 0
  end
end

#split_version(v) ⇒ Object



392
393
394
# File 'lib/gitscape/base.rb', line 392

def split_version(v)
  v.split(".").map { |x| x.to_i }
end

#tag_cleanup(options) ⇒ Object



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/gitscape/base.rb', line 355

def tag_cleanup options
  # Handle default options
  options[:push] = false if options[:push].nil?
  
  # Do a fetch in order to ensure we have all the latest tags
  `git fetch`
  
  # Select which tags to keep.
  # We currently keep tags which fulfill any of the following
  # 1. starts with 'service/'
  # 2. starts with 'rollback-to/' or 'live/', and has an iteration number >= the live_iteration number - 3
  tags = `git tag`.split "\n" 
  tags_to_delete = tags.select { |tag| !(!/^service\//.match(tag).nil? || /^(?:live|rollback-to)\/i(\d+)/.match(tag).to_a[1].to_i >= live_iteration - 3) }
  
  puts "Deleting the following tags.\nThese changes #{options[:push] ? "will" : "will not"} be pushed to origin.\n"
  
  tags_to_delete.each { |tag| puts `git tag -d #{tag}` }
  tags_to_delete.each { |tag| puts `git push origin :refs/tags/#{tag}` } if options[:push]
end