Class: Octopolo::Git
- Inherits:
-
Object
- Object
- Octopolo::Git
- 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
Class Method Summary collapse
-
.alert_dirty_index(message) ⇒ Object
Public: Display the message and show the git status.
- .alert_reserved_branch(message) ⇒ Object
-
.branches_for(prefix) ⇒ Object
Public: List of branches starting with the given string.
-
.check_out(branch_name, do_after_pull = true) ⇒ Object
Public: Check out the given branch name.
-
.clean? ⇒ Boolean
Public: Whether the Git index is clean (has no uncommited changes).
-
.current_branch ⇒ Object
Public: The name of the currently check-out branch.
-
.delete_branch(branch_name) ⇒ Object
Public: Delete the given branch.
-
.deployable_branch ⇒ Object
Public: The name of the current deployable branch.
-
.fetch ⇒ Object
Public: Fetch the latest changes from GitHub.
-
.if_clean(message = DEFAULT_DIRTY_MESSAGE) ⇒ Object
Public: Perform the block if the Git index is clean.
- .latest_branch_for(branch_prefix) ⇒ Object
-
.merge(branch_name) ⇒ Object
Public: Merge the given remote branch into the current branch.
-
.new_branch(new_branch_name, source_branch_name) ⇒ Object
Public: Create a new branch from the given source.
-
.new_tag(tag_name) ⇒ Object
Public: Create a new tag with the given name.
-
.perform(subcommand, options = {}) ⇒ Object
Public: Perform the given Git subcommand.
-
.perform_quietly(subcommand) ⇒ Object
Public: Perform the given Git subcommand without displaying the output.
-
.pull ⇒ Object
Public: Pull the latest changes for the checked-out branch.
-
.push ⇒ Object
Public: Push the current branch to GitHub.
-
.qaready_branch ⇒ Object
Public: The name of the current QA-ready branch.
-
.recent_release_tags ⇒ Object
Public: Only the most recent release tags.
-
.release_tags ⇒ Object
Public: The list of releases which have been tagged.
-
.remote_branches ⇒ Object
Public: The list of branches on GitHub.
-
.reserved_branch?(branch = current_branch) ⇒ Boolean
Public: Determine if current_branch is reserved.
-
.semver_tags ⇒ Object
Public: The list of releases with semantic versioning which have been tagged.
-
.staging_branch ⇒ Object
Public: The name of the current staging branch.
-
.stale_branches(source_branch_name = "master", branches_to_ignore = []) ⇒ Object
Public: Branches which have been merged into the given branch.
Class Method Details
.alert_dirty_index(message) ⇒ Object
Public: Display the message and show the git status
129 130 131 132 133 134 135 |
# File 'lib/octopolo/git.rb', line 129 def self.alert_dirty_index() cli.say " " cli.say cli.say " " perform "status" raise DirtyIndex end |
.alert_reserved_branch(message) ⇒ Object
137 138 139 140 141 142 143 144 145 |
# File 'lib/octopolo/git.rb', line 137 def self.alert_reserved_branch() cli.say " " cli.say 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
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_branch ⇒ Object
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_branch ⇒ Object
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 |
.fetch ⇒ Object
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(=DEFAULT_DIRTY_MESSAGE) if clean? || cli.ask_boolean(DIRTY_CONFIRM_MESSAGE) yield else alert_dirty_index 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, ={}) [:ignore_non_zero] ||= false cli.perform("git #{subcommand}", true, [: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 |
.pull ⇒ Object
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 |
.push ⇒ Object
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_branch ⇒ Object
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_tags ⇒ Object
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. .last(RECENT_TAG_LIMIT) end |
.release_tags ⇒ Object
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. Git.perform_quietly("tag").split("\n").select do |tag| tag =~ RELEASE_TAG_FILTER end end |
.remote_branches ⇒ Object
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
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_tags ⇒ Object
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. Git.perform_quietly("tag").split("\n").select do |tag| tag.sub(/\Av/i,'') =~ SEMVER_TAG_FILTER end end |
.staging_branch ⇒ Object
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 |