Class: Octopolo::Git

Inherits:
Object
  • Object
show all
Extended by:
CLIWrapper
Includes:
CLIWrapper
Defined in:
lib/octopolo/git.rb

Overview

Abstraction around local Git commands

Constant Summary collapse

NO_BRANCH =
"(no branch)"
DEFAULT_DIRTY_MESSAGE =
"Your Git index is not clean. Commit, stash, or otherwise clean up the index before continuing."
DIRTY_CONFIRM_MESSAGE =
"Your Git index is not clean. Do you want to continue?"
RESERVED_BRANCH_MESSAGE =
"Please choose another name for your new branch."
RESERVED_BRANCH_CONFIRM_MESSAGE =
"Your new branch may be misidentified as a reserved branch based on its name. Do you want to continue?"
RELEASE_TAG_FILTER =

we use date-based tags, so look for anything starting with a 4-digit year

/^\d{4}.*/
RECENT_TAG_LIMIT =
9
SEMVER_TAG_FILTER =

for semver tags

Semantic::Version::SemVerRegexp
DEPLOYABLE_PREFIX =

branch prefixes

"deployable"
STAGING_PREFIX =
"staging"
QAREADY_PREFIX =
"qaready"
RESERVED_BRANCH_PREFIXES =

To check if the new branch’s name starts with one of these

[ DEPLOYABLE_PREFIX, STAGING_PREFIX, QAREADY_PREFIX ]
NotOnBranch =

Exceptions

Class.new(StandardError)
CheckoutFailed =
Class.new(StandardError)
MergeFailed =
Class.new(StandardError)
NoBranchOfType =
Class.new(StandardError)
DirtyIndex =
Class.new(StandardError)
ReservedBranch =
Class.new(StandardError)

Instance Attribute Summary

Attributes included from CLIWrapper

#cli

Class Method Summary collapse

Class Method Details

.alert_dirty_index(message) ⇒ Object

Public: Display the message and show the git status

Raises:



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

def self.alert_dirty_index(message)
  cli.say " "
  cli.say message
  cli.say " "
  perform "status"
  raise DirtyIndex
end

.alert_reserved_branch(message) ⇒ Object

Raises:



137
138
139
140
141
142
143
144
145
# File 'lib/octopolo/git.rb', line 137

def self.alert_reserved_branch(message)
  cli.say " "
  cli.say message
  cli.say " "
  cli.say "Here's the list of the reserved branch prefixes:"
  cli.say RESERVED_BRANCH_PREFIXES.join(" ")
  cli.say " "
  raise ReservedBranch
end

.branches_for(prefix) ⇒ Object

Public: List of branches starting with the given string

prefix - String to match branch names against

Returns an Array of Strings containing the branch names



204
205
206
207
208
# File 'lib/octopolo/git.rb', line 204

def self.branches_for(prefix)
  remote_branches.select do |branch_name|
    branch_name =~ /^#{prefix}/
  end
end

.check_out(branch_name, do_after_pull = true) ⇒ Object

Public: Check out the given branch name

branch_name - The name of the branch to check out do_after_pull - Should a pull be done after checkout?



82
83
84
85
86
87
88
89
# File 'lib/octopolo/git.rb', line 82

def self.check_out(branch_name, do_after_pull=true)
  fetch
  perform "checkout #{branch_name}"
  pull if do_after_pull
  unless current_branch == branch_name
    raise CheckoutFailed, "Failed to check out '#{branch_name}'"
  end
end

.clean?Boolean

Public: Whether the Git index is clean (has no uncommited changes)

Returns a Boolean

Returns:

  • (Boolean)


109
110
111
112
113
114
115
116
# File 'lib/octopolo/git.rb', line 109

def self.clean?
  # git status --short returns one line for any uncommited changes, if any
  # e.g.,
  # ?? untracked.txt
  # D  deleted.txt
  # M  modified.txt
  cli.perform_quietly("git status --short").empty?
end

.current_branchObject

Public: The name of the currently check-out branch

Returns a String of the branch name



60
61
62
63
64
65
66
67
68
69
# File 'lib/octopolo/git.rb', line 60

def self.current_branch
  # cut trims the first three characters (whitespace or "*  " for current branch)
  # the chomp removes the newline from the command output
  name = cli.perform_quietly("git branch | grep '^* ' | cut -c 3-").chomp
  if name == NO_BRANCH
    raise NotOnBranch, "Not currently checked out to a particular branch"
  else
    name
  end
end

.delete_branch(branch_name) ⇒ Object

Public: Delete the given branch

branch_name - The name of the branch to delete



266
267
268
269
# File 'lib/octopolo/git.rb', line 266

def self.delete_branch(branch_name)
  perform "push origin :#{branch_name}"
  perform "branch -D #{branch_name}", :ignore_non_zero => true
end

.deployable_branchObject

Public: The name of the current deployable branch



215
216
217
# File 'lib/octopolo/git.rb', line 215

def self.deployable_branch
  latest_branch_for(DEPLOYABLE_PREFIX)
end

.fetchObject

Public: Fetch the latest changes from GitHub



167
168
169
# File 'lib/octopolo/git.rb', line 167

def self.fetch
  perform_quietly "fetch --prune"
end

.if_clean(message = DEFAULT_DIRTY_MESSAGE) ⇒ Object

Public: Perform the block if the Git index is clean



119
120
121
122
123
124
125
126
# File 'lib/octopolo/git.rb', line 119

def self.if_clean(message=DEFAULT_DIRTY_MESSAGE)
  if clean? || cli.ask_boolean(DIRTY_CONFIRM_MESSAGE)
    yield
  else
    alert_dirty_index message
    exit 1
  end
end

.latest_branch_for(branch_prefix) ⇒ Object



210
211
212
# File 'lib/octopolo/git.rb', line 210

def self.latest_branch_for(branch_prefix)
  branches_for(branch_prefix).last || raise(NoBranchOfType, "No #{branch_prefix} branch")
end

.merge(branch_name) ⇒ Object

Public: Merge the given remote branch into the current branch



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/octopolo/git.rb', line 148

def self.merge(branch_name)
  Git.if_clean do
    Git.fetch
    perform "merge --no-ff origin/#{branch_name}", :ignore_non_zero => true
    unless Git.clean?
      if @resolver_used.nil? && Octopolo.config.merge_resolver
        %x(#{Octopolo.config.merge_resolver})
        @resolver_used = true
        if Git.clean?
          merge(branch_name)
        end
      end
    end
    raise MergeFailed unless Git.clean?
    Git.push
  end
end

.new_branch(new_branch_name, source_branch_name) ⇒ Object

Public: Create a new branch from the given source

new_branch_name - The name of the branch to create source_branch_name - The name of the branch to branch from

Example:

Git.new_branch("bug-123-fix-thing", "master")


99
100
101
102
103
104
# File 'lib/octopolo/git.rb', line 99

def self.new_branch(new_branch_name, source_branch_name)
  fetch
  perform("branch --no-track #{new_branch_name} origin/#{source_branch_name}")
  check_out(new_branch_name, false)
  perform("push --set-upstream origin #{new_branch_name}")
end

.new_tag(tag_name) ⇒ Object

Public: Create a new tag with the given name

tag_name - The name of the tag to create



257
258
259
260
261
# File 'lib/octopolo/git.rb', line 257

def self.new_tag(tag_name)
  perform "tag #{tag_name}"
  push
  perform "push --tag"
end

.perform(subcommand, options = {}) ⇒ Object

Public: Perform the given Git subcommand

subcommand - String containing the subcommand and its parameters options - Hash

ignore_non_zero - Ignore exception for non-zero exit status of command.

Example:

> Git.perform "status"
# => output of `git status`


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

def self.perform(subcommand, options={})
  options[:ignore_non_zero] ||= false
  cli.perform("git #{subcommand}", true, options[:ignore_non_zero])
end

.perform_quietly(subcommand) ⇒ Object

Public: Perform the given Git subcommand without displaying the output

subcommand - String containing the subcommand and its parameters

Example:

> Git.perform_quietly "status"
# => no output


53
54
55
# File 'lib/octopolo/git.rb', line 53

def self.perform_quietly(subcommand)
  cli.perform_quietly "git #{subcommand}"
end

.pullObject

Public: Pull the latest changes for the checked-out branch



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

def self.pull
  if_clean do
    perform "pull"
  end
end

.pushObject

Public: Push the current branch to GitHub



172
173
174
175
176
# File 'lib/octopolo/git.rb', line 172

def self.push
  if_clean do
    perform "push origin #{current_branch}"
  end
end

.qaready_branchObject

Public: The name of the current QA-ready branch



225
226
227
# File 'lib/octopolo/git.rb', line 225

def self.qaready_branch
  latest_branch_for(QAREADY_PREFIX)
end

.recent_release_tagsObject

Public: Only the most recent release tags

Returns an Array of Strings containing the tag names



241
242
243
# File 'lib/octopolo/git.rb', line 241

def self.recent_release_tags
  release_tags.last(RECENT_TAG_LIMIT)
end

.release_tagsObject

Public: The list of releases which have been tagged

Returns an Array of Strings containing the tag names



232
233
234
235
236
# File 'lib/octopolo/git.rb', line 232

def self.release_tags
  Git.perform_quietly("tag").split("\n").select do |tag|
    tag =~ RELEASE_TAG_FILTER
  end
end

.remote_branchesObject

Public: The list of branches on GitHub

Returns an Array of Strings containing the branch names



188
189
190
191
192
193
194
195
196
197
# File 'lib/octopolo/git.rb', line 188

def self.remote_branches
  Git.fetch
  raw = Git.perform_quietly "branch --remote"
  all_branches = raw.split("\n").map do |raw_name|
    # will come in as "  origin/foo", we want just "foo"
    raw_name.split("/").last
  end

  all_branches.uniq.sort
end

.reserved_branch?(branch = current_branch) ⇒ Boolean

Public: Determine if current_branch is reserved

Returnsa boolean value

Returns:

  • (Boolean)


74
75
76
# File 'lib/octopolo/git.rb', line 74

def self.reserved_branch?(branch=current_branch)
  !(branch =~ /^(?:#{Git::RESERVED_BRANCH_PREFIXES.join('|')})/).nil?
end

.semver_tagsObject

Public: The list of releases with semantic versioning which have been tagged

Returns an Array of Strings containing the tag names



248
249
250
251
252
# File 'lib/octopolo/git.rb', line 248

def self.semver_tags
  Git.perform_quietly("tag").split("\n").select do |tag|
    tag.sub(/\Av/i,'') =~ SEMVER_TAG_FILTER
  end
end

.staging_branchObject

Public: The name of the current staging branch



220
221
222
# File 'lib/octopolo/git.rb', line 220

def self.staging_branch
  latest_branch_for(STAGING_PREFIX)
end

.stale_branches(source_branch_name = "master", branches_to_ignore = []) ⇒ Object

Public: Branches which have been merged into the given branch

source_branch_name - The name of the branch to check against branches_to_ignore - An Array of branches to exclude from results

Returns an Array of Strings



277
278
279
280
281
282
# File 'lib/octopolo/git.rb', line 277

def self.stale_branches(source_branch_name="master", branches_to_ignore=[])
  Git.fetch
  command = "branch --remote --merged #{recent_sha(source_branch_name)} | grep -E -v '(#{stale_branches_to_ignore(branches_to_ignore).join("|")})'"
  raw_result = Git.perform_quietly command
  raw_result.split.map { |full_name| full_name.gsub("origin/", "") }
end