Class: PivotalIntegration::Util::Git

Inherits:
Object
  • Object
show all
Defined in:
lib/pivotal-integration/util/git.rb

Overview

Utilities for dealing with Git

Constant Summary collapse

KEY_REMOTE =
'remote'.freeze
KEY_ROOT_BRANCH =
'root-branch'.freeze
KEY_ROOT_REMOTE =
'root-remote'.freeze
KEY_FINISH_MODE =
'finish-mode'.freeze
RELEASE_BRANCH_NAME =
'pivotal-tracker-release'.freeze

Class Method Summary collapse

Class Method Details

.add_hook(name, source, overwrite = false) ⇒ void

This method returns an undefined value.

Adds a Git hook to the current repository

Parameters:

  • name (String)

    the name of the hook to add

  • source (String)

    the file to use as the source for the created hook

  • overwrite (Boolean) (defaults to: false)

    whether to overwrite the hook if it already exists



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/pivotal-integration/util/git.rb', line 35

def self.add_hook(name, source, overwrite = false)
  hooks_directory =  File.join repository_root, '.git', 'hooks'
  hook = File.join hooks_directory, name

  if overwrite || !File.exist?(hook)
    print "Creating Git hook #{name}...  "

    FileUtils.mkdir_p hooks_directory
    File.open(source, 'r') do |input|
      File.open(hook, 'w') do |output|
        output.write(input.read)
        output.chmod(0755)
      end
    end

    puts 'OK'
  end
end

.branch_nameString

Returns the name of the currently checked out branch

Returns:

  • (String)

    the name of the currently checked out branch



57
58
59
# File 'lib/pivotal-integration/util/git.rb', line 57

def self.branch_name
  PivotalIntegration::Util::Shell.exec('git branch').scan(/\* (.*)/)[0][0]
end

.create_branch(name, print_messages = true) ⇒ void

This method returns an undefined value.

Creates a branch with a given name. First pulls the current branch to ensure that it is up to date and then creates and checks out the new branch. If specified, sets branch-specific properties that are passed in.

Parameters:

  • name (String)

    the name of the branch to create

  • print_messages (Boolean) (defaults to: true)

    whether to print messages



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/pivotal-integration/util/git.rb', line 68

def self.create_branch(name, print_messages = true)
  root_branch = branch_name
  root_remote = get_config KEY_REMOTE, :branch

  if print_messages; print "Pulling #{root_branch}... " end
  PivotalIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
  if print_messages; puts 'OK'
  end

  if print_messages; print "Creating and checking out #{name}... " end
  PivotalIntegration::Util::Shell.exec "git checkout --quiet -B #{name}"
  set_config KEY_ROOT_BRANCH, root_branch, :branch
  set_config KEY_ROOT_REMOTE, root_remote, :branch
  if print_messages; puts 'OK'
  end
end

.create_commit(message, story) ⇒ void

This method returns an undefined value.

Creates a commit with a given message. The commit includes all change files.

Parameters:

  • message (String)

    The commit message, which will be appended with [#<story-id]

  • story (PivotalTracker::Story)

    the story associated with the current commit



97
98
99
# File 'lib/pivotal-integration/util/git.rb', line 97

def self.create_commit(message, story)
  PivotalIntegration::Util::Shell.exec "git commit --quiet --all --allow-empty --message \"#{message}\n\n[##{story.id}]\""
end

.create_pull_request(story) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/pivotal-integration/util/git.rb', line 174

def self.create_pull_request(story)
  case get_config_with_default('pivotal.pull-request-editor', :web).to_sym
    when :web
      # Open the PR editor in a web browser
      repo = get_config('remote.origin.url')[/(?<[email protected]:)[a-z0-9_-]+\/[a-z0-9_-]+/]
      title = CGI::escape("[##{story.id}] #{story.name}]")
      Launchy.open "https://github.com/#{repo}/compare/#{branch_name}?expand=1&pull_request[title]=#{title}"

    else
      print 'Checking for hub installation... '
      if PivotalIntegration::Util::Shell.exec('which hub', false).empty?
        puts "FAIL"
        puts "Hub required to use this feature (brew install hub / https://github.com/github/hub)."
        abort
      else
        puts "OK"
      end

      puts "Creating a pull request for #{branch_name}... "
      system "hub pull-request"
      puts 'OK'
    end
end

.create_release_tag(name, story) ⇒ void

This method returns an undefined value.

Creates a tag with the given name. Before creating the tag, commits all outstanding changes with a commit message that reflects that these changes are for a release.

Parameters:

  • name (String)

    the name of the tag to create

  • story (PivotalTracker::Story)

    the story associated with the current tag



109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/pivotal-integration/util/git.rb', line 109

def self.create_release_tag(name, story)
  root_branch = branch_name

  print "Creating tag v#{name}... "

  create_branch RELEASE_BRANCH_NAME, false
  create_commit "#{name} Release", story
  PivotalIntegration::Util::Shell.exec "git tag v#{name}"
  PivotalIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
  PivotalIntegration::Util::Shell.exec "git branch --quiet -D #{RELEASE_BRANCH_NAME}"

  puts 'OK'
end

.finish_modeObject



148
149
150
# File 'lib/pivotal-integration/util/git.rb', line 148

def self.finish_mode
  get_config_with_default('pivotal.finish-mode', :merge).to_sym
end

.get_config(key, scope = :inherited) ⇒ String

Returns a Git configuration value. This value is read using the git config command. The scope of the value to read can be controlled with the scope parameter.

Parameters:

  • key (String)

    the key of the configuration to retrieve

  • scope (:branch, :inherited) (defaults to: :inherited)

    the scope to read the configuration from

    • :branch: equivalent to calling git config branch.branch-name.key

    • :inherited: equivalent to calling git config key

Returns:

  • (String)

    the value of the configuration

Raises:

  • if the specified scope is not :branch or :inherited



133
134
135
136
137
138
139
140
141
# File 'lib/pivotal-integration/util/git.rb', line 133

def self.get_config(key, scope = :inherited)
  if :branch == scope
    PivotalIntegration::Util::Shell.exec("git config branch.#{branch_name}.#{key}", false).strip
  elsif :inherited == scope
    PivotalIntegration::Util::Shell.exec("git config #{key}", false).strip
  else
    raise "Unable to get Git configuration for scope '#{scope}'"
  end
end

.get_config_with_default(key, default = nil, scope = :inherited) ⇒ Object



143
144
145
146
# File 'lib/pivotal-integration/util/git.rb', line 143

def self.get_config_with_default(key, default = nil, scope = :inherited)
  value = get_config(key, scope)
  value.blank? ? default : value
end

.merge(story, no_complete, no_delete) ⇒ void

This method returns an undefined value.

Merges the current branch to its root branch and deletes the current branch

Parameters:

  • story (PivotalTracker::Story)

    the story associated with the current branch

  • no_complete (Boolean)

    whether to suppress the Completes statement in the commit message

  • no_delete (Boolean)

    whether to delete development branch



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/pivotal-integration/util/git.rb', line 158

def self.merge(story, no_complete, no_delete)
  development_branch = branch_name
  root_branch = get_config KEY_ROOT_BRANCH, :branch

  print "Merging #{development_branch} to #{root_branch}... "
  PivotalIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
  PivotalIntegration::Util::Shell.exec "git merge --quiet --no-ff -m \"Merge #{development_branch} to #{root_branch}\n\n[#{no_complete ? '' : 'Completes '}##{story.id}]\" #{development_branch}"
  puts 'OK'

  unless no_delete
    print "Deleting #{development_branch}... "
    PivotalIntegration::Util::Shell.exec "git branch --quiet -D #{development_branch}"
    puts 'OK'
  end
end

.push(*refs) ⇒ void

This method returns an undefined value.

Push changes to the remote of the current branch

Parameters:

  • refs (String)

    the explicit references to push



202
203
204
205
206
207
208
# File 'lib/pivotal-integration/util/git.rb', line 202

def self.push(*refs)
  remote = get_config KEY_REMOTE, :branch

  print "Pushing to #{remote}... "
  PivotalIntegration::Util::Shell.exec "git push --quiet origin #{remote} " + refs.join(' ')
  puts 'OK'
end

.repository_rootString

Returns the root path of the current Git repository. The root is determined by ascending the path hierarchy, starting with the current working directory (+Dir#pwd+), until a directory is found that contains a .git/ sub directory.

Returns:

  • (String)

    the root path of the Git repository

Raises:

  • if the current working directory is not in a Git repository



217
218
219
220
221
222
223
224
225
226
227
# File 'lib/pivotal-integration/util/git.rb', line 217

def self.repository_root
  repository_root = Dir.pwd

  until Dir.entries(repository_root).any? { |child| File.directory?(child) && (child =~ /^.git$/) }
    next_repository_root = File.expand_path('..', repository_root)
    abort('Current working directory is not in a Git repository') unless repository_root != next_repository_root
    repository_root =  next_repository_root
  end

  repository_root
end

.set_config(key, value, scope = :local) ⇒ void

This method returns an undefined value.

Sets a Git configuration value. This value is set using the git config command. The scope of the set value can be controlled with the scope parameter.

Parameters:

  • key (String)

    the key of configuration to store

  • value (String)

    the value of the configuration to store

  • scope (:branch, :global, :local) (defaults to: :local)

    the scope to store the configuration value in.

    • :branch: equivalent to calling git config –local branch.branch-name.key value

    • :global: equivalent to calling git config –global key value

    • :local: equivalent to calling git config –local key value

Raises:

  • if the specified scope is not :branch, :global, or :local



241
242
243
244
245
246
247
248
249
250
251
# File 'lib/pivotal-integration/util/git.rb', line 241

def self.set_config(key, value, scope = :local)
  if :branch == scope
    PivotalIntegration::Util::Shell.exec "git config --local branch.#{branch_name}.#{key} #{value}"
  elsif :global == scope
    PivotalIntegration::Util::Shell.exec "git config --global #{key} #{value}"
  elsif :local == scope
    PivotalIntegration::Util::Shell.exec "git config --local #{key} #{value}"
  else
    raise "Unable to set Git configuration for scope '#{scope}'"
  end
end

.switch_branch(name) ⇒ Object



85
86
87
# File 'lib/pivotal-integration/util/git.rb', line 85

def self.switch_branch(name)
  PivotalIntegration::Util::Shell.exec "git checkout --quiet #{name}"
end

.trivial_merge?void

This method returns an undefined value.

Checks whether merging the current branch back to its root branch would be a trivial merge. A trivial merge is defined as one where the net change of the merge would be the same as the net change of the branch being merged. The easiest way to ensure that a merge is trivial is to rebase a development branch onto the tip of its root branch.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/pivotal-integration/util/git.rb', line 260

def self.trivial_merge?
  development_branch = branch_name
  root_branch = get_config KEY_ROOT_BRANCH, :branch

  print "Checking for trivial merge from #{development_branch} to #{root_branch}... "

  PivotalIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
  PivotalIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
  PivotalIntegration::Util::Shell.exec "git checkout --quiet #{development_branch}"

  root_tip = PivotalIntegration::Util::Shell.exec "git rev-parse #{root_branch}"
  common_ancestor = PivotalIntegration::Util::Shell.exec "git merge-base #{root_branch} #{development_branch}"

  if root_tip != common_ancestor
    abort 'FAIL'
  end

  puts 'OK'
end