Class: RightGit::Git::Repository

Inherits:
Object
  • Object
show all
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)

    for logging (default = STDOUT)



44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/right_git/git/repository.rb', line 44

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
  @logger = options[:logger] || ::RightGit::Shell::Default.default_logger
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



38
39
40
# File 'lib/right_git/git/repository.rb', line 38

def logger
  @logger
end

#repo_dirObject (readonly)

Returns the value of attribute repo_dir.



38
39
40
# File 'lib/right_git/git/repository.rb', line 38

def repo_dir
  @repo_dir
end

#shellObject (readonly)

Returns the value of attribute shell.



38
39
40
# File 'lib/right_git/git/repository.rb', line 38

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:



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/right_git/git/repository.rb', line 69

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_for(branch_name) ⇒ Branch

Factory method for a branch object referencing this repository.

Parameters:

  • branch_name (String)

    for reference

Returns:



118
119
120
# File 'lib/right_git/git/repository.rb', line 118

def branch_for(branch_name)
  Branch.new(self, 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 (TrueClass|FalseClass)

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

Returns:

  • (Array)

    list of branches



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/right_git/git/repository.rb', line 129

def branches(options = {})
  options = {
    :all => true
  }.merge(options)
  git_args = ['branch']
  git_args << '-a' if options[:all]  # note older versions of git don't accept --all
  branches = BranchCollection.new(self)
  git_output(git_args).lines.each do |line|
    # ignore the no-branch branch that git helpfully provides when current
    # HEAD is a tag or otherwise not-a-branch.
    unless line.strip == '* (no branch)'
      branch = Branch.new(self, line)
      branches << branch if branch
    end
  end
  branches
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



232
233
234
235
236
237
238
239
240
# File 'lib/right_git/git/repository.rb', line 232

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



197
198
199
200
201
# File 'lib/right_git/git/repository.rb', line 197

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



211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/right_git/git/repository.rb', line 211

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



93
94
95
96
# File 'lib/right_git/git/repository.rb', line 93

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



104
105
106
107
108
109
110
111
# File 'lib/right_git/git/repository.rb', line 104

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



322
323
324
# File 'lib/right_git/git/repository.rb', line 322

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



248
249
250
251
252
253
# File 'lib/right_git/git/repository.rb', line 248

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

  • :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



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/right_git/git/repository.rb', line 173

def log(revision, options = {})
  options = {
    :skip        => nil,
    :tail        => 1_000,
    :no_merges   => false,
    :full_hashes => false,
  }.merge(options)
  skip = options[:skip]
  git_args = [
    'log',
    "-n#{options[:tail]}",
    "--format=\"%#{options[:full_hashes] ? 'H' : 'h'} %at %aE\""  # double-quotes are Windows friendly
  ]
  git_args << "--skip #{skip}" if skip
  git_args << "--no-merges" if options[:no_merges]
  git_args << revision if revision
  git_output(git_args).lines.map { |line| Commit.new(self, line) }
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



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/right_git/git/repository.rb', line 299

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



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

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



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/right_git/git/repository.rb', line 261

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:



152
153
154
# File 'lib/right_git/git/repository.rb', line 152

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



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

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



284
285
286
287
288
289
290
291
292
# File 'lib/right_git/git/repository.rb', line 284

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



342
343
344
345
346
347
348
349
# File 'lib/right_git/git/repository.rb', line 342

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