class Gitdocs::Repository
Attributes
Public Class Methods
Clone a repository, and create the destination path if necessary.
@param [String] path to clone the repository to @param [String] remote URI of the git repository to clone
@raise [RuntimeError] if the clone fails
@return [Gitdocs::Repository]
# File lib/gitdocs/repository.rb, line 56 def self.clone(path, remote) FileUtils.mkdir_p(File.dirname(path)) # TODO: determine how to do this with rugged, and handle SSH and HTTPS # credentials. Grit::Git.new(path).clone({ raise: true, quiet: true }, remote, path) repository = new(path) fail("Unable to clone into #{path}") unless repository.valid? repository rescue Grit::Git::GitTimeout raise("Unable to clone into #{path} because it timed out") rescue Grit::Git::CommandFailed => e raise("Unable to clone into #{path} because of #{e.err}") end
Initialize the repository on the specified path. If the path is not valid for some reason, the object will be initialized but it will be put into an invalid state. @see valid?
@see invalid_reason
@param [String, Share] path_or_share
# File lib/gitdocs/repository.rb, line 29 def initialize(path_or_share) path = path_or_share if path_or_share.respond_to?(:path) path = path_or_share.path @remote_name = path_or_share.remote_name @branch_name = path_or_share.branch_name end @rugged = Rugged::Repository.new(path) @grit = Grit::Repo.new(path) Grit::Git.git_timeout = 120 @invalid_reason = nil @commit_message_path = abs_path('.gitmessage~') rescue Rugged::OSError @invalid_reason = :directory_missing rescue Rugged::RepositoryError @invalid_reason = :no_repository end
Public Instance Methods
@return [nil] if the repository is invalid @return [Array<String>] sorted list of local branches
# File lib/gitdocs/repository.rb, line 91 def available_branches return nil unless valid? @rugged.branches.each_name(:local).sort end
@return [nil] if the repository is invalid @return [Array<String>] sorted list of remote branches
# File lib/gitdocs/repository.rb, line 84 def available_remotes return nil unless valid? @rugged.branches.each_name(:remote).sort end
@param [String] relative_path @param [String] oid
# File lib/gitdocs/repository.rb, line 292 def blob_at(relative_path, ref) @rugged.blob_at(ref, relative_path) end
@return [nil] @return (see Gitdocs::Repository::Comitter#commit)
# File lib/gitdocs/repository.rb, line 191 def commit return unless valid? Committer.new(root).commit end
Excluding the initial commit (without a parent) which keeps things consistent with the original behaviour. TODO: reconsider if this is the correct behaviour
@param [String] relative_path @param [Integer] limit the number of commits which will be returned
@return [Array<Rugged::Commit>]
# File lib/gitdocs/repository.rb, line 272 def commits_for(relative_path, limit) # TODO: should add a filter here for checking that the commit actually has # an associated blob. commits = head_walker.select do |commit| commit.parents.size == 1 && changes?(commit, relative_path) end # TODO: should re-write this limit in a way that will skip walking all of # the commits. commits.first(limit) end
@return [nil] if there are no commits present @return [String] oid of the HEAD of the working directory
# File lib/gitdocs/repository.rb, line 98 def current_oid @rugged.head.target_id rescue Rugged::ReferenceError nil end
Is the working directory dirty
@return [Boolean]
# File lib/gitdocs/repository.rb, line 107 def dirty? return false unless valid? return Dir.glob(abs_path('*')).any? unless current_oid @rugged.diff_workdir(current_oid, include_untracked: true).deltas.any? end
Fetch all the remote branches
@raise [FetchError] if there is an error return message
@return [nil] if the repository is invalid @return [:no_remote] if the remote is not yet set @return [:ok] if the fetch worked
# File lib/gitdocs/repository.rb, line 146 def fetch return nil unless valid? return :no_remote unless remote? @rugged.remotes.each { |x| @grit.remote_fetch(x.name) } :ok rescue Grit::Git::GitTimeout raise(FetchError, "Fetch timed out for #{root}") rescue Grit::Git::CommandFailed => e raise(FetchError, e.err) end
@param [String] term @yield [file, context] Gives the files and context for each of the results @yieldparam file [String] @yieldparam context [String]
# File lib/gitdocs/repository.rb, line 125 def grep(term, &block) @grit.git.grep( { raise: true, bare: false, chdir: root, ignore_case: true }, term ).scan(/(.*?):([^\n]*)/, &block) rescue Grit::Git::GitTimeout # TODO: add logging to record the error details '' rescue Grit::Git::CommandFailed # TODO: add logging to record the error details if they are not just # nothing found '' end
@param [String] relative_path
@return [Rugged::Commit]
# File lib/gitdocs/repository.rb, line 286 def last_commit_for(relative_path) head_walker.find { |commit| changes?(commit, relative_path) } end
Merge the repository
@raise [MergeError] if there is an error, it it will include the message
@return [nil] if the repository is invalid @return [:no_remote] if the remote is not yet set @return [Array<String>] if there is a conflict return the Array of
conflicted file names
@return (see author_count
) if merged with no errors or conflicts
# File lib/gitdocs/repository.rb, line 167 def merge return nil unless valid? return :no_remote unless remote? return :ok unless remote_oid return :ok if remote_oid == current_oid last_oid = current_oid @grit.git.merge( { raise: true, chdir: root }, "#{@remote_name}/#{@branch_name}" ) author_count(last_oid) rescue Grit::Git::GitTimeout raise(MergeError, "Merge timed out for #{root}") rescue Grit::Git::CommandFailed => e # HACK: The rugged in-memory index will not have been updated after the # Grit merge command. Reload it before checking for conflicts. @rugged.index.reload raise(MergeError, e.err) unless @rugged.index.conflicts? mark_conflicts end
@return [Boolean]
# File lib/gitdocs/repository.rb, line 115 def need_sync? return false unless valid? return false unless remote? remote_oid != current_oid end
Push the repository
@return [nil] if the repository is invalid @return [:no_remote] if the remote is not yet set @return [:nothing] if there was nothing to do @return [String] if there is an error return the message @return (see author_count
) if pushed without errors or conflicts
# File lib/gitdocs/repository.rb, line 203 def push return unless valid? return :no_remote unless remote? return :nothing unless current_oid return :nothing if remote_oid == current_oid last_oid = remote_oid @grit.git.push({ raise: true }, @remote_name, @branch_name) author_count(last_oid) rescue Grit::Git::CommandFailed => e return :conflict if e.err[/\[rejected\]/] e.err # return the output on error end
@return [String]
# File lib/gitdocs/repository.rb, line 72 def root return nil unless valid? @rugged.path.sub(/.\.git./, '') end
@return [Hash{:merge,:push => Object}]
# File lib/gitdocs/repository.rb, line 236 def synchronize(type) result = { merge: nil, push: nil } return result unless valid? case type when 'fetch' fetch when 'full' commit fetch result[:merge] = merge result[:push] = push end result rescue Gitdocs::Repository::FetchError result rescue Gitdocs::Repository::MergeError => e result[:merge] = e.message result end
@return [Boolean]
# File lib/gitdocs/repository.rb, line 78 def valid? !@invalid_reason end
@param (see Gitdocs::Repository::Comitter#write_commit_message) @return [void]
# File lib/gitdocs/repository.rb, line 259 def write_commit_message(message) return unless valid? Committer.new(root).write_commit_message(message) end
Private Instance Methods
# File lib/gitdocs/repository.rb, line 379 def abs_path(*path) File.join(root, *path) end
@param [Rugged::Commit] commit @param [String] relative_path @return [Boolean]
# File lib/gitdocs/repository.rb, line 303 def changes?(commit, relative_path) commit.diff(paths: [relative_path]).size > 0 # rubocop:disable ZeroLengthPredicate end
# File lib/gitdocs/repository.rb, line 320 def head_walker walker = Rugged::Walker.new(@rugged) walker.sorting(Rugged::SORT_DATE) walker.push(@rugged.head.target) walker end
# File lib/gitdocs/repository.rb, line 344 def mark_conflicts # assert(@rugged.index.conflicts?) # Collect all the index entries by their paths. index_path_entries = Hash.new { |h, k| h[k] = [] } @rugged.index.map do |index_entry| index_path_entries[index_entry[:path]].push(index_entry) end # Filter to only the conflicted entries. conflicted_path_entries = index_path_entries.delete_if { |_k, v| v.length == 1 } conflicted_path_entries.each_pair do |path, index_entries| # Write out the different versions of the conflicted file. index_entries.each do |index_entry| filename, extension = index_entry[:path].scan(/(.*?)(|\.[^\.]+)$/).first author = ' original' if index_entry[:stage] == 1 short_oid = index_entry[:oid][0..6] new_filename = "#{filename} (#{short_oid}#{author})#{extension}" File.open(abs_path(new_filename), 'wb') do |f| f.write(Rugged::Blob.lookup(@rugged, index_entry[:oid]).content) end end # And remove the original. FileUtils.remove(abs_path(path), force: true) end # NOTE: Let commit be handled by the next regular commit. conflicted_path_entries.keys end
# File lib/gitdocs/repository.rb, line 335 def mark_empty_directories Find.find(root).each do |path| Find.prune if File.basename(path) == '.git' if File.directory?(path) && Dir.entries(path).count == 2 FileUtils.touch(File.join(path, '.gitignore')) end end end
# File lib/gitdocs/repository.rb, line 327 def read_and_delete_commit_message_file return 'Auto-commit from gitdocs' unless File.exist?(@commit_message_path) message = File.read(@commit_message_path) File.delete(@commit_message_path) message end
@return [Boolean]
# File lib/gitdocs/repository.rb, line 308 def remote? @rugged.remotes.any? end
@return [nil] @return [String]
# File lib/gitdocs/repository.rb, line 314 def remote_oid branch = @rugged.branches["#{@remote_name}/#{@branch_name}"] return unless branch branch.target_id end