Class: RightGit::Git::Repository

Inherits:
Object
  • Object
show all
Includes:
RightSupport::Log::Mixin
Defined in:
lib/right_git/git/repository.rb

Overview

Provides an API for managing a git repository that is suitable for automation. It is assumed that gestures like creating a new repository, branch or tag are manual tasks beyond the scope of automation so those are not covered here. What is provided are APIs for cloning, fetching, listing and grooming git-related objects.

Constant Summary collapse

COMMIT_SHA1_REGEX =
/^commit ([0-9a-fA-F]{40})$/
SUBMODULE_STATUS_REGEX =
/^([+\- ])([0-9a-fA-F]{40}) (.*) (.*)$/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(repo_dir, options = {}) ⇒ Repository

Returns a new instance of Repository.

Parameters:

  • repo_dir (String)

    for git actions or ‘.’

  • options (Hash) (defaults to: {})

    for repository

Options Hash (options):

  • :shell (Object)

    for git command execution (default = DefaultShell)

  • :logger (Logger)

    custom logger to use; (default = class-level logger provided by Log::Mixin)



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/right_git/git/repository.rb', line 49

def initialize(repo_dir, options = {})
  options = {
    :shell  => nil,
    :logger => nil
  }.merge(options)

  if repo_dir && ::File.directory?(repo_dir)
    @repo_dir = ::File.expand_path(repo_dir)
  else
    raise ::ArgumentError.new('A valid repo_dir is required')
  end

  @shell = options[:shell] || ::RightGit::Shell::Default
  self.logger = options[:logger] || self.class.logger
end

Instance Attribute Details

#repo_dirObject (readonly)

Returns the value of attribute repo_dir.



43
44
45
# File 'lib/right_git/git/repository.rb', line 43

def repo_dir
  @repo_dir
end

#shellObject (readonly)

Returns the value of attribute shell.



43
44
45
# File 'lib/right_git/git/repository.rb', line 43

def shell
  @shell
end

Class Method Details

.clone_to(repo_url, destination, options = {}) ⇒ Repository

Factory method to clone the repo given by URL to the given destination and return a new Repository object.

Note that cloning to the default working directory-relative location is not currently supported.

Parameters:

  • repo_url (String)

    to clone

  • destination (String)

    path where repo is cloned

  • options (Hash) (defaults to: {})

    for repository

Returns:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/right_git/git/repository.rb', line 76

def self.clone_to(repo_url, destination, options = {})
  destination = ::File.expand_path(destination)
  git_args = ['clone', '--', repo_url, destination]
  expected_git_dir = ::File.join(destination, '.git')
  if ::File.directory?(expected_git_dir)
    raise ::ArgumentError,
          "Destination is already a git repository: #{destination.inspect}"
  end
  repo = self.new('.', options)
  repo.vet_output(git_args)
  if ::File.directory?(expected_git_dir)
    repo.instance_variable_set(:@repo_dir, destination)
  else
    raise GitError,
          "Failed to clone #{repo_url.inspect} to #{destination.inspect}"
  end
  repo
end

Instance Method Details

#branch(branch_name) ⇒ Branch

Factory method for a branch object referencing this repository. The branch may be hypothetical (e.g. does not exist yet).

Parameters:

  • branch_name (String)

    for reference

Returns:



132
133
134
# File 'lib/right_git/git/repository.rb', line 132

def branch(branch_name)
  Branch.new(self, branch_name)
end

#branch_for(branch_name) ⇒ Object

Deprecated.

alias for #branch



121
122
123
124
# File 'lib/right_git/git/repository.rb', line 121

def branch_for(branch_name)
  warn "#{self.class.name}#branch_for is deprecated; please use #{self.class.name}#branch instead"
  branch(branch_name)
end

#branches(options = {}) ⇒ Array

Generates a list of known (checked-out) branches from the current git directory.

Parameters:

  • options (Hash) (defaults to: {})

    for branches

Options Hash (options):

  • :all (Boolean)

    true to include remote branches, else local only (default)

Returns:

  • (Array)

    list of branches



143
144
145
146
147
148
149
150
151
# File 'lib/right_git/git/repository.rb', line 143

def branches(options = {})
  branches = BranchCollection.new(self)

  if options[:all]
    branches
  else
    branches.local
  end
end

#checkout_to(revision, options = {}) ⇒ TrueClass

Checkout.

Parameters:

  • revision (String)

    for checkout

  • options (Hash) (defaults to: {})

    for checkout

Options Hash (options):

  • :force (TrueClass|FalseClass)

    as true to force checkout

Returns:

  • (TrueClass)

    always true



241
242
243
244
245
246
247
248
249
# File 'lib/right_git/git/repository.rb', line 241

def checkout_to(revision, options = {})
  options = {
    :force => false
  }.merge(options)
  git_args = ['checkout', revision]
  git_args << '--force' if options[:force]
  vet_output(git_args)
  true
end

#clean(*args) ⇒ TrueClass

Cleans the current repository of untracked files.

Parameters:

  • args (Array)

    for clean

Returns:

  • (TrueClass)

    always true



206
207
208
209
210
# File 'lib/right_git/git/repository.rb', line 206

def clean(*args)
  git_args = ['clean', args]
  spit_output(git_args)
  true
end

#clean_all(options = {}) ⇒ TrueClass

Cleans everything and optionally cleans .gitignored files.

Parameters:

  • options (Hash) (defaults to: {})

    for checkout

Options Hash (options):

  • :directories (TrueClass|FalseClass)

    as true to clean untracked directories (but not untracked submodules)

  • :gitignored (TrueClass|FalseClass)

    as true to clean gitignored (untracked) files

  • :submodules (TrueClass|FalseClass)

    as true to clean untracked submodules (requires force)

Returns:

  • (TrueClass)

    always true



220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/right_git/git/repository.rb', line 220

def clean_all(options = {})
  options = {
    :directories => false,
    :gitignored  => false,
    :submodules  => false,
  }.merge(options)
  git_args = ['-f']  # force is required or else -n only lists files.
  git_args << '-f' if options[:submodules]  # double-tap -f to kill untracked submodules
  git_args << '-d' if options[:directories]
  git_args << '-x' if options[:gitignored]
  clean(git_args)
  true
end

#fetch(*args) ⇒ TrueClass

Fetches using the given options, if any.

Parameters:

  • args (Array)

    for fetch

Returns:

  • (TrueClass)

    always true



100
101
102
103
# File 'lib/right_git/git/repository.rb', line 100

def fetch(*args)
  vet_output(['fetch', args])
  true
end

#fetch_all(options = {}) ⇒ TrueClass

Fetches branch and tag information from remote origin.

Parameters:

  • options (Hash) (defaults to: {})

    for fetch all

Options Hash (options):

  • :prune (TrueClass|FalseClass)

    as true to prune dead branches

Returns:

  • (TrueClass)

    always true



111
112
113
114
115
116
117
118
# File 'lib/right_git/git/repository.rb', line 111

def fetch_all(options = {})
  options = { :prune => false }.merge(options)
  git_args = ['--all']
  git_args << '--prune' if options[:prune]
  fetch(git_args)
  fetch('--tags')  # need a separate call for tags or else you don't get all the tags
  true
end

#git_output(*args) ⇒ String

Executes and returns the output for a git command. Raises on failure.

Parameters:

  • args (String|Array)

    to execute

Returns:

  • (String)

    output



331
332
333
# File 'lib/right_git/git/repository.rb', line 331

def git_output(*args)
  inner_execute(:output_for, args)
end

#hard_reset_to(revision) ⇒ TrueClass

Performs a hard reset to the given revision, if given, or else the last checked-out SHA.

Parameters:

  • revision (String)

    as target for hard reset or nil for hard reset to HEAD

Returns:

  • (TrueClass)

    always true



257
258
259
260
261
262
# File 'lib/right_git/git/repository.rb', line 257

def hard_reset_to(revision)
  git_args = ['reset', '--hard']
  git_args << revision if revision
  vet_output(git_args)
  true
end

#log(revision, options = {}) ⇒ Array

Generates a list of commits using the given ‘git log’ arguments.

Parameters:

  • revision (String)

    to log or nil

  • options (Hash) (defaults to: {})

    for log

Options Hash (options):

  • :skip (Integer)

    as lines of most recent history to skip (Default = include most recent)

  • :tail (Integer)

    as max history of log

  • :merges (TrueClass|FalseClass)

    as true to exclude non-merge commits

  • :no_merges (TrueClass|FalseClass)

    as true to exclude merge commits

  • :full_hashes (TrueClass|FalseClass)

    as true show full hashes, false for (7-character) abbreviations

Returns:

  • (Array)

    list of commits



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/right_git/git/repository.rb', line 180

def log(revision, options = {})
  options = {
    :skip        => nil,
    :tail        => 10_000,
    :merges      => false,
    :no_merges   => false,
    :full_hashes => false,
  }.merge(options)
  skip = options[:skip]
  git_args = [
    'log',
    "-n#{options[:tail]}",
    "--format=\"#{options[:full_hashes] ? Commit::LOG_FORMAT_LONG : Commit::LOG_FORMAT}\""  # double-quotes are Windows friendly
  ]
  git_args << "--skip #{skip}" if skip
  git_args << "--merges" if options[:merges]
  git_args << "--no-merges" if options[:no_merges]
  git_args << revision if revision
  git_output(git_args).lines.map { |line| Commit.new(self, line.strip) }
end

#sha_for(revision) ⇒ String

Determines the SHA referenced by the given revision. Raises on failure.

Parameters:

  • revision (String)

    or nil for current SHA

Returns:

  • (String)

    SHA for revision



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/right_git/git/repository.rb', line 308

def sha_for(revision)
  # note that 'git show-ref' produces easier-to-parse output but it matches
  # both local and remote branch to a simple branch name whereas 'git show'
  # matches at-most-one and requires origin/ for remote branches.
  git_args = ['show', revision].compact
  result = nil
  git_output(git_args).lines.each do |line|
    if matched = COMMIT_SHA1_REGEX.match(line.strip)
      result = matched[1]
      break
    end
  end
  unless result
    raise GitError, 'Unable to locate commit in show output.'
  end
  result
end

#spit_output(*args) ⇒ TrueClass

Prints the output for a git command. Raises on failure.

Parameters:

  • args (String|Array)

    to execute

Returns:

  • (TrueClass)

    always true



340
341
342
# File 'lib/right_git/git/repository.rb', line 340

def spit_output(*args)
  inner_execute(:execute, args)
end

#submodule_paths(options = {}) ⇒ Array

Queries the recursive list of submodule paths for the current workspace.

Parameters:

  • options (Hash) (defaults to: {})

    for submodules

Options Hash (options):

  • :recursive (TrueClass|FalseClass)

    as true to recursively get submodule paths

Returns:

  • (Array)

    list of submodule paths or empty



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/right_git/git/repository.rb', line 270

def submodule_paths(options = {})
  options = {
    :recursive => false
  }.merge(options)
  git_args = ['submodule', 'status']
  git_args << '--recursive' if options[:recursive]
  git_output(git_args).lines.map do |line|
    data = line.chomp
    if matched = SUBMODULE_STATUS_REGEX.match(data)
      matched[3]
    else
      raise GitError,
            "Unexpected output from submodule status: #{data.inspect}"
    end
  end
end

#tag_for(tag_name) ⇒ Branch

Factory method for a tag object referencing this repository.

Parameters:

  • tag_name (String)

    for reference

Returns:



158
159
160
# File 'lib/right_git/git/repository.rb', line 158

def tag_for(tag_name)
  Tag.new(self, tag_name)
end

#tagsArray

Generates a list of known (fetched) tags from the current git directory.

Returns:

  • (Array)

    list of tags



165
166
167
# File 'lib/right_git/git/repository.rb', line 165

def tags
  git_output('tag').lines.map { |line| Tag.new(self, line.strip) }
end

#update_submodules(options = {}) ⇒ TrueClass

Updates submodules for the current workspace.

Parameters:

  • options (Hash) (defaults to: {})

    for submodules

Options Hash (options):

  • :recursive (TrueClass|FalseClass)

    as true to recursively update submodules

Returns:

  • (TrueClass)

    always true



293
294
295
296
297
298
299
300
301
# File 'lib/right_git/git/repository.rb', line 293

def update_submodules(options = {})
  options = {
    :recursive => false
  }.merge(options)
  git_args = ['submodule', 'update', '--init']
  git_args << '--recursive' if options[:recursive]
  spit_output(git_args)
  true
end

#vet_output(*args) ⇒ TrueClass

msysgit on Windows exits zero even when checkout|reset|fetch fails so we need to scan the output for error or fatal messages. it does no harm to do the same on Linux even though the exit code works properly there.

Parameters:

  • args (String|Array)

    to execute

Returns:

  • (TrueClass)

    always true



351
352
353
354
355
356
357
358
# File 'lib/right_git/git/repository.rb', line 351

def vet_output(*args)
  last_output = git_output(*args).strip
  logger.info(last_output) unless last_output.empty?
  if last_output.downcase =~ /^(error|fatal):/
    raise GitError, "Git exited zero but an error was detected in output."
  end
  true
end