class Autobuild::Git

Attributes

default_alternates[W]

Sets the default alternates path used by all Git importers

Setting it explicitly overrides any value we get from the AUTOBUILD_CACHE_DIR and AUTOBUILD_GIT_CACHE_DIR environment variables.

@see default_alternates

default_fingerprint_mode[RW]

What git repository fingerprinting uses as reference

Can either be

  • “commit” (the default). Use the commit hash of HEAD

  • “tree”. Use the tree hash. This will return the same fingerprint

    for new commits that do not change the source code. However, it
    will fail to detect changes to the working copy that are due
    to git checkout filters.

@return [String]

shallow[W]

Sets shallow clones globally (applies to submodules as well)

single_branch[W]

Sets the single_branch option globally for all Git importers This can can be overriden in the oporter options

additional_remotes[R]

A list of remotes that should be set up in the git config

Use {#declare_alternate_repository} to add one

@return [(String,String,String)] a list of (name, repository, push_to)

triplets
alternates[RW]

A list of local (same-host) repositories that will be used instead of the remote one when possible. It has one major issue (see below), so use at your own risk.

The paths must point to the git directory, so either the .git directory in a checked out git repository, or the repository itself in a bare repository.

A default reference repository can be given through the AUTOBUILD_GIT_CACHE environment variable.

Note that it has the major caveat that if objects disappear from the reference repository, the current one will be broken. See the git documentation for more information.

@return [Array<String>]

branch[RW]

The branch this importer is tracking

commit[RW]

The commit we are pointing to. It is a commit ID.

Setting it through this method is deprecated, use {#relocate} to set the commit

local_branch[W]

Set {#local_branch}

merge[W]

Set the merge flag. See merge?

push_to[RW]

If set, this URL will be listed as a pushurl for the tracked branch. It makes it possible to have a read-only URL for fetching and specify a push URL for people that have commit rights

It is not used by the importer itself

{#repository} is always used for read-only operations

remote_branch[W]

Set {#remote_branch}

remote_name[RW]

The name of the remote that should be set up by the importer

Defaults to ‘autobuild’

repository[RW]

The remote repository URL.

@see push_to

shallow[W]

Set the {#shallow?} predicate

single_branch[W]

Set the {#single_branch?} predicate

tag[RW]

The tag we are pointing to. It is a tag name.

Setting it through this method is deprecated, use {#relocate} to set the tag

with_submodules[W]

Set to true if checkout should be done with submodules

Defaults to false

Public Class Methods

at_least_version(*version) click to toggle source

Tests the git version

@param [Array<Integer>] version the git version as an array of integer @return [Boolean] true if the git version is at least the requested

one, and false otherwise
# File lib/autobuild/import/git.rb, line 122
def self.at_least_version(*version)
    compare_versions(self.version, version) <= 0
end
can_handle?(path) click to toggle source

Tests whether the given directory is a git repository

# File lib/autobuild/import/git.rb, line 1412
def self.can_handle?(path)
    _, style = Git.resolve_git_dir(path)
    style == :normal
end
compare_versions(actual, required) click to toggle source

Helper method to compare two (partial) versions represented as array of integers

@return [Integer] -1 if actual is greater than required,

0 if equal, and 1 if actual is smaller than required
# File lib/autobuild/import/git.rb, line 105
def self.compare_versions(actual, required)
    return -compare_versions(required, actual) if actual.size > required.size

    actual += [0] * (required.size - actual.size)
    actual.zip(required).each do |v_act, v_req|
        if v_act > v_req then return -1
        elsif v_act < v_req then return 1
        end
    end
    0
end
default_alternates() click to toggle source

A default list of repositories that should be used as reference repositories for all Git importers

It is initialized (by order of priority) using the AUTOBUILD_GIT_CACHE_DIR and AUTOBUILD_CACHE_DIR environment variables

Because of its role within the caching system in autobuild/autoproj, these defaults are not applied to git repositories that are using submodules. The autoproj cache builder does not generate repositories compatible with having submodules

@return [Array] @see default_alternates=, Git#alternates

# File lib/autobuild/import/git.rb, line 56
def default_alternates
    if @default_alternates then @default_alternates
    elsif (cache_dirs = Importer.cache_dirs('git'))
        @default_alternates = cache_dirs.map do |path|
            File.join(File.expand_path(path), '%s')
        end
    else
        Array.new
    end
end
default_config() click to toggle source
# File lib/autobuild/import/git.rb, line 80
def self.default_config
    @default_config ||= Hash.new
end
git_dir(package, require_working_copy) click to toggle source

@api private

(see Git#git_dir)

# File lib/autobuild/import/git.rb, line 455
def self.git_dir(package, require_working_copy)
    dir, style = Git.resolve_git_dir(package.importdir)
    validate_git_dir(package, require_working_copy, dir, style)
    dir
end
has_uncommitted_changes?(package, with_untracked_files = false) click to toggle source

@api private

Tests whether the package’s working copy has uncommitted changes

@param [Package] package @param [Boolean] with_untracked_files whether untracked files are

considered uncommitted changes
# File lib/autobuild/import/git.rb, line 721
def self.has_uncommitted_changes?(package, with_untracked_files = false)
    status = run_git(package, 'status', '--porcelain').map(&:strip)
    if with_untracked_files
        !status.empty?
    else
        status.any? { |l| l[0, 2] !~ /^\?\?|^  / }
    end
end
lfs_installed?() click to toggle source
# File lib/autobuild/import/git.rb, line 1297
def self.lfs_installed?
    return @lfs_installed unless @lfs_installed.nil?

    _, _, status = Open3.capture3('git lfs')
    @lfs_installed = status.success?
end
new(repository, branch = nil, options = {}) click to toggle source

Creates an importer which tracks a repository and branch.

This importer uses the ‘git’ tool to perform the import. It defaults to ‘git’ and can be configured by doing

Autobuild.programs['git'] = 'my_git_tool'

@param [String] branch deprecated, use the ‘branch’ named option

instead

@option options [String] push_to (repository) the URL to set up as

push_to URL in the remote(s). Note that it is not used internally by
this class

@option options [String] branch (master) the branch we should track.

It is used both as {#local_branch} and {#remote_branch}

@option options [String] tag (nil) a tag at which we should pin the

checkout. Cannot be given at the same time than :commit

@option options [String] commit (nil) a commit ID at which we should pin the

checkout. Cannot be given at the same time than :tag

@option options [String] repository_id (git:#{repository}) a string

that allows to uniquely identify a repository. The meaning is
caller-specific. For instance, autoproj uses repository_id to check
whether two Git importers fetches from the same repository.

@option options [Boolean] with_submodules (false) whether the importer should

checkout and update submodules. Note that in an autobuild-based
workflow, it is recommended to not use submodules but checkout all
repositories separately instead.
Calls superclass method
# File lib/autobuild/import/git.rb, line 153
def initialize(repository, branch = nil, options = {})
    @git_dir_cache = Array.new
    @local_branch = @remote_branch = nil
    @tag = @commit = nil

    @merge = false

    if branch.respond_to?(:to_hash)
        options = branch.to_hash
        branch = nil
    end

    if branch
        Autobuild.warn "the git importer now expects you to provide the branch "\
            "as a named option"
        Autobuild.warn "this form is deprecated:"
        Autobuild.warn "Autobuild.git 'git://gitorious.org/rock/buildconf.git',"
        Autobuild.warn "    'master'"
        Autobuild.warn "and should be replaced by"
        Autobuild.warn "Autobuild.git 'git://gitorious.org/rock/buildconf.git',"
        Autobuild.warn "    branch: 'master'"
    end

    gitopts, common = Kernel.filter_options(
        options,
        push_to: nil,
        branch: nil,
        local_branch: nil,
        remote_branch: nil,
        tag: nil,
        commit: nil,
        repository_id: nil,
        source_id: nil,
        with_submodules: false,
        fingerprint_mode: Git.default_fingerprint_mode,
        single_branch: Git.single_branch?,
        shallow: Git.shallow?
    )

    if gitopts[:branch] && branch
        raise ConfigException, "git branch specified with both the option hash "\
            "and the explicit parameter"
    end
    gitopts[:branch] ||= branch

    super(common)

    @single_branch = gitopts[:single_branch]
    @shallow = gitopts[:shallow]
    @with_submodules = gitopts.delete(:with_submodules)
    @alternates =
        if @with_submodules
            []
        else
            Git.default_alternates.dup
        end

    @remote_name = 'autobuild'
    @push_to = nil
    @fingerprint_mode = gitopts[:fingerprint_mode]
    relocate(repository, gitopts)
    @additional_remotes = Array.new
end
resolve_git_dir(path) click to toggle source

@api private

Resolves the git directory associated with path, and tells whether it is a bare repository or not

@param [String] path the path from which we should resolve @return [(String,Symbol),nil] either the path to the git folder and

:bare or :normal, or nil if path is not a git repository.
# File lib/autobuild/import/git.rb, line 416
def self.resolve_git_dir(path)
    gitdir = File.join(path, '.git')
    path = gitdir if File.exist?(gitdir)

    result = `#{Autobuild.tool(:git)} --git-dir="#{path}" rev-parse \
        --is-bare-repository 2>&1`
    if $CHILD_STATUS.success?
        if result.strip == "true"
            [path, :bare]
        else
            [path, :normal]
        end
    end
end
run_git(package, *args) click to toggle source

@api private

(see Git#run_git)

# File lib/autobuild/import/git.rb, line 549
def self.run_git(package, *args)
    options =
        if args.last.kind_of?(Hash)
            args.pop
        else
            Hash.new
        end

    working_directory = File.dirname(git_dir(package, true))
    package.run(:import, Autobuild.tool(:git), *args,
                Hash[resolved_env: Hash.new,
                     working_directory: working_directory].merge(options))
end
run_git_bare(package, *args) click to toggle source

@api private

(see Git#run_git_bare)

# File lib/autobuild/import/git.rb, line 575
def self.run_git_bare(package, *args)
    options =
        if args.last.kind_of?(Hash)
            args.pop
        else
            Hash.new
        end

    package.run(:import, Autobuild.tool(:git),
                '--git-dir', git_dir(package, false),
                *args, Hash[resolved_env: Hash.new].merge(options))
end
shallow?() click to toggle source

Whether shallow clones is enabled globally

# File lib/autobuild/import/git.rb, line 29
def shallow?
    !!@shallow
end
single_branch?() click to toggle source

Whether single_branch is enabled globally

# File lib/autobuild/import/git.rb, line 21
def single_branch?
    !!@single_branch
end
validate_git_dir(package, require_working_copy, _dir, style) click to toggle source

@api private

Validates the return value of {resolve_git_dir}

@param [Package] package the package we are working on @param [Boolean] require_working_copy if false, a bare repository will

be considered as valid, otherwise not

@param [String,nil] dir the path to the repository’s git directory, or nil

if the target is not a valid repository (see the documentation of
{resolve_git_dir}

@param [Symbol,nil] style either :normal for a git checkout with

working copy, :bare for a bare repository or nil if {resolve_git_dir}
did not detect a git repository

@return [void] @raise ConfigException if dir/style are nil, or if

require_working_copy is true and style is :bare
# File lib/autobuild/import/git.rb, line 478
def self.validate_git_dir(package, require_working_copy, _dir, style)
    if !style
        raise ConfigException.new(package, 'import', retry: false),
              "while importing #{package.name}, #{package.importdir} "\
              "does not point to a git repository"
    elsif require_working_copy && (style == :bare)
        raise ConfigException.new(package, 'import', retry: false),
              "while importing #{package.name}, #{package.importdir} "\
              "points to a bare git repository but a working copy was required"
    end
end
vcs_definition_for(path, remote_name = 'autobuild') click to toggle source

Returns a hash that represents the configuration of a git importer based on the information contained in the git configuration

@raise [ArgumentError] if the path does not point to a git repository

# File lib/autobuild/import/git.rb, line 1421
def self.vcs_definition_for(path, remote_name = 'autobuild')
    unless can_handle?(path)
        raise ArgumentError, "#{path} is neither a git repository, "\
            "nor a bare git repository"
    end

    Dir.chdir(path) do
        vars = `#{Autobuild.tool(:git)} config -l`.
            split("\n").
            each_with_object(Hash.new) do |line, h|
                k, v = line.strip.split('=', 2)
                h[k] = v
            end
        url = vars["remote.#{remote_name}.url"] || vars['remote.origin.url']
        if url
            return Hash[type: :git, url: url]
        else
            return Hash[type: :git]
        end
    end
end
version() click to toggle source

Returns the git version as a string

@return [String]

# File lib/autobuild/import/git.rb, line 89
def self.version
    version = Subprocess.run('git', 'setup', Autobuild.tool(:git), '--version').
        first
    if version =~ /^git version (\d[\d.]+)/
        $1.split(".").map { |i| Integer(i) }
    else
        raise ArgumentError, "cannot parse git version string #{version}, "\
            "was expecting something looking like 'git version 2.1.0'"
    end
end

Public Instance Methods

checkout(package, _options = Hash.new) click to toggle source
# File lib/autobuild/import/git.rb, line 1325
def checkout(package, _options = Hash.new)
    shallow_clone = validate_shallow(package)

    base_dir = File.expand_path('..', package.importdir)
    FileUtils.mkdir_p(base_dir) unless File.directory?(base_dir)

    clone_options = Array.new
    if with_submodules?
        clone_options << '--recurse-submodules'
        clone_options << '--shallow-submodules' if shallow_clone
    end

    clone_options << '--depth' << '1' if shallow_clone

    if single_branch?
        if tag
            if tag.start_with?("refs/")
                raise ArgumentError, "you cannot provide a full ref for"\
                    " the tag while cloning a single branch"
            end
            clone_options << "--branch=#{tag}"
        elsif remote_branch
            if remote_branch.start_with?("refs/")
                raise ArgumentError, "you cannot provide a full ref for"\
                    " the remote branch while cloning a single branch"
            end
            clone_options << "--branch=#{remote_branch}"
        end
        clone_options << "--single-branch"
    elsif shallow_clone
        clone_options << "--no-single-branch"
    end

    each_alternate_path(package) do |path|
        clone_options << '--reference' << path
    end
    self.class.default_config.each do |key, value|
        clone_options << "--config=#{key}=#{value}"
    end
    package.run(:import,
                Autobuild.tool('git'), 'clone', '-o', remote_name, *clone_options,
                repository, package.importdir, retry: true)

    update_remotes_configuration(package, only_local: false)
    update(package, only_local: !remote_branch.start_with?("refs/"),
                    reset: :force)
    if with_submodules?
        run_git(package, "submodule", "update", '--init', '--recursive')
    end
end
commit_present_in?(package, rev, reference) click to toggle source

Tests whether a commit is already present in a given history

@param [Package] the package we are working on @param [String] rev what we want to verify the presence of @param [String] reference the reference commit. The method tests that

'commit' is present in the history of 'reference'

@return [Boolean]

# File lib/autobuild/import/git.rb, line 948
def commit_present_in?(package, rev, reference)
    commit = rev_parse(package, rev)
    begin
        merge_base = run_git_bare(package, 'merge-base', commit, reference).first
        merge_base == commit
    rescue Exception
        raise PackageException.new(package, 'import'), "failed to find "\
            "the merge-base between #{rev} and #{reference}. "\
            "Are you sure these commits exist ?"
    end
end
current_branch(package) click to toggle source

Returns the branch HEAD is pointing to

@return [String,nil] the full ref HEAD is pointing to (i.e.

refs/heads/master), or nil if HEAD is detached

@raises SubcommandFailed if git failed

# File lib/autobuild/import/git.rb, line 854
def current_branch(package)
    run_git_bare(package, 'symbolic-ref', 'HEAD', '-q').first.strip
rescue SubcommandFailed => e
    raise if e.status != 1
end
current_remote_commit(package, options = Hash.new) click to toggle source

@api private

Returns the commit ID of what we should consider being the remote commit

@param [Package] package @options options [Boolean] only_local if true, no remote access should be

performed, in which case the current known state of the remote will be
used. If false, we access the remote repository to fetch the actual
commit ID

@options options [Array] refspec list of refs to fetch. Only the first

one is returned by this method

@return [String] the commit ID as a string

# File lib/autobuild/import/git.rb, line 743
def current_remote_commit(package, options = Hash.new)
    options = Hash[only_local: options] unless options.kind_of?(Hash)
    only_local = options.delete(:only_local)

    if only_local
        unless remote_branch.start_with?("refs/")
            refspec =
                options[:refspec] ||
                ("refs/tags/#{tag}" if tag) ||
                "refs/remotes/#{remote_name}/#{remote_branch}"
        end
        unless (refspec = Array(refspec).first)
            raise ArgumentError, "cannot use only_local with no tag,"\
              " and an absolute remote ref"
        end

        begin
            run_git_bare(package, 'show-ref', '-s', refspec).first.strip
        rescue SubcommandFailed
            raise PackageException.new(package, "import"),
                  "cannot resolve #{refspec}"
        end
    else
        refspec =
            options[:refspec] ||
            ("refs/tags/#{tag}" if tag) ||
            remote_branch_to_ref(remote_branch)
        begin fetch_remote(package, refspec: refspec)
        rescue Exception => e
            fallback(e, package, :status, package, only_local)
        end
    end
end
declare_alternate_repository(name, repository, options = Hash.new) click to toggle source
# File lib/autobuild/import/git.rb, line 1443
def declare_alternate_repository(name, repository, options = Hash.new)
    unless name
        raise ArgumentError, "cannot declare alternate repository "\
            "#{repository} without a name"
    end
    additional_remotes << [name, repository, options[:push_to] || repository]
end
delta_between_tags(package, from_tag, to_tag) click to toggle source

Computes the merge status for this package between two existing tags

@param [Package] package @param [String] from_tag the source tag @param [String] to_tag the target tag @raise [ArgumentError] if one of the tags is unknown

# File lib/autobuild/import/git.rb, line 496
def delta_between_tags(package, from_tag, to_tag)
    pkg_tags = tags(package)
    unless pkg_tags.key?(from_tag)
        raise ArgumentError, "tag '#{from_tag}' is unknown to #{package.name} "\
            "-- known tags are: #{pkg_tags.keys}"
    end
    unless pkg_tags.key?(to_tag)
        raise ArgumentError, "tag '#{to_tag}' is unknown to #{package.name} "\
            "-- known tags are: #{pkg_tags.keys}"
    end

    from_commit = pkg_tags[from_tag]
    to_commit = pkg_tags[to_tag]

    merge_status(package, to_commit, from_commit)
end
describe_commit_on_remote(package, rev = 'HEAD', options = Hash.new) click to toggle source

Finds a remote reference that contains a commit

It will favor the configured {#remote_branch} if it matches

@param [Autobuild::Package] package the package we are working on @param [String] commit_id the commit ID (can be a rev) @return [String] a remote ref @raise [PackageException] if there is no such commit on the remote

# File lib/autobuild/import/git.rb, line 968
def describe_commit_on_remote(package, rev = 'HEAD', options = Hash.new)
    rev = rev.to_str
    options = Kernel.validate_options options,
                                      tags: true

    commit_id = rev_parse(package, rev)

    remote_refs = Hash[*each_remote_ref(package).to_a.flatten]
    remote_branch_ref = remote_branch_to_ref(remote_branch)
    remote_branch_id = remote_refs.delete(remote_branch_ref)
    begin
        if commit_present_in?(package, commit_id, remote_branch_id)
            return remote_branch
        end
    rescue PackageException # We have to fetch. Fetch all branches at once
        fetch_remote(package, refspec: [remote_branch_ref, *remote_refs.keys])
        if commit_present_in?(package, commit_id, remote_branch_id)
            return remote_branch
        end
    end

    remote_refs.delete_if { |r| r =~ %r{^refs/tags/} } unless options[:tags]

    # Prefer tags, then heads, then the rest (e.g. github pull requests)
    remote_refs = remote_refs.sort_by do |rev_name, _rev_id|
        case rev_name
        when %r{^refs/tags/} then 0
        when %r{^refs/heads/} then 1
        else 2
        end
    end

    remote_refs.delete_if do |rev_name, rev_id|
            if commit_present_in?(package, commit_id, rev_id)
                return rev_name
            else
                true
            end
    rescue PackageException
            false
    end

    unless remote_refs.empty?
        fetch_remote(package, refspec: remote_refs.map(&:first))
        remote_refs.each do |rev_name, rev_id|
            return rev_name if commit_present_in?(package, commit_id, rev_id)
        end
    end

    raise PackageException.new(package), "current HEAD (#{commit_id}) does not "\
        "seem to be present on the remote"
end
describe_rev(package, rev) click to toggle source

Resolve a commit ref to a tag or commit ID

# File lib/autobuild/import/git.rb, line 646
def describe_rev(package, rev)
    tag = run_git_bare(package, 'describe',
                       '--tags', '--exact-match', rev).first.strip
    [true, tag.encode('UTF-8')]
rescue Autobuild::SubcommandFailed
    commit = rev_parse(package, rev)
    [false, commit.encode('UTF-8')]
end
detached_head?(package) click to toggle source
# File lib/autobuild/import/git.rb, line 835
def detached_head?(package)
    current_branch(package).nil?
end
determine_target_state(package, only_local: false) click to toggle source
# File lib/autobuild/import/git.rb, line 1165
def determine_target_state(package, only_local: false)
    pinned_state =
        if commit then commit
        elsif tag then "refs/tags/#{tag}"
        end

    if pinned_state
        unless has_commit?(package, pinned_state)
            fetch_commit = current_remote_commit(
                package,
                only_local: only_local,
                refspec: [remote_branch_to_ref(remote_branch), tag])
        end
        target_commit = pinned_state = rev_parse(package, pinned_state)
    else
        target_commit = fetch_commit =
            current_remote_commit(package, only_local: only_local)
    end

    [pinned_state, target_commit, fetch_commit]
end
each_alternate_path(package) { |path| ... } click to toggle source
# File lib/autobuild/import/git.rb, line 1277
def each_alternate_path(package)
    return enum_for(__method__, package) unless block_given?

    alternates.each do |path|
        path = format(path, package.name)
        yield(path) if File.directory?(path)
    end
    nil
end
each_configured_remote() { |args, args, args || args| ... } click to toggle source

Enumerates the remotes that this importer would set up on the repository

@yieldparam [String] remote_name the remote name @yieldparam [String] url the remote URL @yieldparam [String] push_to the remote push-to URL

# File lib/autobuild/import/git.rb, line 622
def each_configured_remote
    ([['autobuild', repository, push_to]] + additional_remotes).each do |args|
        yield(args[0], args[1], args[2] || args[1])
    end
end
each_remote_ref(package) { |ref_name, commit_id| ... } click to toggle source

Enumerates the ref that are present on the remote

@yieldparam [String] ref_name the ref name @yieldparam [String] commit_id the ref’s commit ID

# File lib/autobuild/import/git.rb, line 659
def each_remote_ref(package)
    return enum_for(__method__, package) unless block_given?

    run_git_bare(package, 'ls-remote', repository).each do |line|
        commit_id, ref_name = line.split(/\s+/)
        yield(ref_name, commit_id) if ref_name !~ /\^/
    end
end
fetch_remote(package, options = Hash.new) click to toggle source

@api private

Fetches updates from the remote repository. Returns the remote commit ID on success, nil on failure. Expects the current directory to be the package’s source directory.

# File lib/autobuild/import/git.rb, line 673
def fetch_remote(package, options = Hash.new)
    validate_importdir(package)
    unless options[:refspec]
        raise ArgumentError, "required argument 'refspec' not given"
    end

    git_dir = git_dir(package, false)

    # If we are checking out a specific commit, we don't know which
    # branch to refer to in git fetch. So, we have to set up the
    # remotes and call git fetch directly (so that all branches get
    # fetch)
    #
    # Otherwise, do git fetch now
    #
    # Doing it now is better as it makes sure that we replace the
    # configuration parameters only if the repository and branch are
    # OK (i.e. we keep old working configuration instead)
    refspec = Array(options[:refspec])
    tag_arg = ['--tags'] if tag
    run_git_bare(package, 'fetch', repository, *tag_arg, *refspec, retry: true)

    update_remotes_configuration(package)

    # Now get the actual commit ID from the FETCH_HEAD file, and
    # return it
    if File.readable?(File.join(git_dir, 'FETCH_HEAD'))
        fetched_commits = File.readlines(File.join(git_dir, 'FETCH_HEAD')).
            find_all { |l| l !~ /not-for-merge/ }.
            map { |line| line.split(/\s+/).first }
        refspec.zip(fetched_commits).each do |spec, commit_id|
            if spec =~ %r{^refs/heads/(.*)$}
                run_git_bare(package, 'update-ref', "-m", "updated by autobuild",
                             "refs/remotes/#{remote_name}/#{$1}", commit_id)
            end
        end

        fetched_commits.first
    end
end
git_dir(package, require_working_copy) click to toggle source

@api private

Returns either the package’s working copy or git directory

@param [Package] package the package to resolve @param [Boolean] require_working_copy whether a working copy is

required

@raise if the package’s {Package#importdir} is not a git repository,

or if it is a bare repository and require_working_copy is true
# File lib/autobuild/import/git.rb, line 440
def git_dir(package, require_working_copy)
    if @git_dir_cache[0] == package.importdir
        dir, style = *@git_dir_cache[1, 2]
    else
        dir, style = Git.resolve_git_dir(package.importdir)
    end

    @git_dir_cache = [package.importdir, dir, style]
    self.class.validate_git_dir(package, require_working_copy, dir, style)
    dir
end
has_all_branches?() click to toggle source

Whether both the local and remote branches are known

See documentation of {#resolve_all_branches}

# File lib/autobuild/import/git.rb, line 383
def has_all_branches?
    remote_branch && local_branch
end
has_branch?(package, branch_name) click to toggle source
# File lib/autobuild/import/git.rb, line 819
def has_branch?(package, branch_name)
    run_git_bare(package, 'show-ref', '-q', '--verify',
                 remote_branch_to_ref(branch_name))
    true
rescue SubcommandFailed => e
    if e.status == 1
        false
    else
        raise
    end
end
has_commit?(package, commit_id) click to toggle source
# File lib/autobuild/import/git.rb, line 808
def has_commit?(package, commit_id)
    run_git_bare(package, 'rev-parse', '-q', '--verify', "#{commit_id}^{commit}")
    true
rescue SubcommandFailed => e
    if e.status == 1
        false
    else
        raise
    end
end
has_local_branch?(package) click to toggle source
# File lib/autobuild/import/git.rb, line 831
def has_local_branch?(package)
    has_branch?(package, local_branch) if local_branch
end
local_branch() click to toggle source

The branch that should be used on the local clone

Defaults to {#branch}

# File lib/autobuild/import/git.rb, line 283
def local_branch
    @local_branch || branch
end
merge?() click to toggle source

True if it is allowed to merge remote updates automatically. If false (the default), the import will fail if the updates do not resolve as a fast-forward

# File lib/autobuild/import/git.rb, line 309
def merge?
    @merge
end
merge_if_simple(package, target_commit) click to toggle source

@api private

# File lib/autobuild/import/git.rb, line 1250
def merge_if_simple(package, target_commit)
    status = merge_status(package, target_commit)
    if status.needs_update?
        if !merge? && status.status == Status::NEEDS_MERGE
            raise PackageException.new(package, 'import'), "the local branch "\
                "'#{local_branch}' and the remote branch #{branch} of "\
                "#{package.name} have diverged, and I therefore refuse "\
                "to update automatically. Go into #{package.importdir} "\
                "and either reset the local branch or merge the remote changes"
        end
        run_git(package, 'merge', target_commit)
        return true
    end
    false
end
merge_status(package, fetch_commit, reference_commit = "HEAD") click to toggle source

Computes the update status to update a branch whose tip is at reference_commit (which can be a symbolic reference) using the fetch_commit commit

I.e. this compute what happens if one would do

git checkout reference_commit
git merge fetch_commit
# File lib/autobuild/import/git.rb, line 1030
def merge_status(package, fetch_commit, reference_commit = "HEAD")
    begin
        common_commit = run_git_bare(package, 'merge-base',
                                     reference_commit, fetch_commit).first.strip
    rescue Exception
        raise PackageException.new(package, 'import'), "failed to find "\
            "the merge-base between #{reference_commit} and #{fetch_commit}. "\
            "Are you sure these commits exist ?"
    end
    remote_commit = rev_parse(package, fetch_commit)
    head_commit   = rev_parse(package, reference_commit)

    status = if common_commit != remote_commit
                 if common_commit == head_commit
                     Status::SIMPLE_UPDATE
                 else
                     Status::NEEDS_MERGE
                 end
             elsif common_commit == head_commit
                 Status::UP_TO_DATE
             else
                 Status::ADVANCED
             end

    Status.new(package, status, fetch_commit, head_commit, common_commit)
end
on_local_branch?(package) click to toggle source

Checks if the current branch is the target branch. Expects that the current directory is the package’s directory

# File lib/autobuild/import/git.rb, line 862
def on_local_branch?(package)
    if (current_branch = self.current_branch(package))
        current_branch == "refs/heads/#{local_branch}"
    end
end
on_target_branch?(package) click to toggle source

@deprecated use on_local_branch? instead

# File lib/autobuild/import/git.rb, line 869
def on_target_branch?(package)
    on_local_branch?(package)
end
relocate(repository, options = Hash.new) click to toggle source

Changes the repository this importer is pointing to

# File lib/autobuild/import/git.rb, line 1377
def relocate(repository, options = Hash.new)
    options = options.transform_keys(&:to_sym)

    local_branch  =
        options[:local_branch] || options[:branch] ||
        self.local_branch || options[:default_branch] || nil
    remote_branch =
        options[:remote_branch] || options[:branch] ||
        self.remote_branch || options[:default_branch] || nil
    if local_branch&.start_with?("refs/")
        raise ArgumentError, "you cannot provide a full ref for"\
            " the local branch, only for the remote branch"
    end

    @push_to = options[:push_to] || @push_to
    @branch = @local_branch = @remote_branch = nil
    if local_branch == remote_branch
        @branch = local_branch
    else
        @local_branch = local_branch
        @remote_branch = remote_branch
    end
    @tag    = options.fetch(:tag, @tag)
    @commit = options.fetch(:commit, @commit)

    @repository = repository.to_str
    @repository_id =
        options[:repository_id] ||
        "git:#{@repository}"
    @source_id =
        options[:source_id] ||
        "#{@repository_id} branch=#{remote_branch} tag=#{tag} commit=#{commit}"
end
remote_branch() click to toggle source

The remote branch to which we should push

Defaults to {#branch}

# File lib/autobuild/import/git.rb, line 290
def remote_branch
    @remote_branch || branch
end
reset_head_to_commit(package, target_commit, fetch_commit, options = Hash.new) click to toggle source

@api private

Safely resets the current branch to a given commit

This method safely resets the current branch to a given commit, not requiring a clean working copy (i.e. it can handle local changes).

It verifies that the current HEAD will not be lost by the operation, either because it is included in the target commit or because it is present remotely

@param [Package] package the package we handle @param [String] target_commit the commit we want to reset HEAD to @param [String] fetch_commit the state of the remote branch. This is

used to avoid losing commits if HEAD is not included in
target_commit

@option options [Boolean] force (false) bypasses checks that verify

that some commits won't be lost by resetting
# File lib/autobuild/import/git.rb, line 1117
def reset_head_to_commit(package, target_commit, fetch_commit, options = Hash.new)
    current_head     = rev_parse(package, 'HEAD')
    head_to_target   = merge_status(package, target_commit, current_head)
    status_to_target = head_to_target.status

    if status_to_target == Status::UP_TO_DATE
        return false
    elsif status_to_target == Status::SIMPLE_UPDATE
        run_git(package, 'merge', target_commit)
    elsif !options[:force]
        # Check whether the current HEAD is present on the remote
        # repository. We'll refuse resetting if there are uncommitted
        # changes
        unless commit_present_in?(package, current_head, fetch_commit)
            raise ImporterCannotReset.new(package, 'import'),
                  "branch #{local_branch} of #{package.name} contains"\
                  " commits that do not seem to be present on the branch"\
                  " #{remote_branch} of the remote repository. I can't"\
                  " go on as it could make you lose some stuff. Update"\
                  " the remote branch in your overrides, push your"\
                  " changes or reset to the remote commit manually"\
                  " before trying again"
        end
    end

    package.message format("  %%s: resetting branch %<branch>s to %<commit>s",
                           branch: local_branch, commit: target_commit)
    # I don't use a reset --hard here as it would add even more
    # restrictions on when we can do the operation (as we would refuse
    # doing it if there are local changes). The checkout creates a
    # detached HEAD, but makes sure that applying uncommitted changes is
    # fine (it would abort otherwise). The rest then updates HEAD and
    # the local_branch ref to match the required target commit
    resolved_target_commit = rev_parse(package, "#{target_commit}^{commit}")
    begin
        run_git(package, 'checkout', target_commit)
        run_git(package, 'update-ref', "refs/heads/#{local_branch}",
                resolved_target_commit)
        run_git(package, 'symbolic-ref', "HEAD", "refs/heads/#{local_branch}")
    rescue ::Exception
        run_git(package, 'symbolic-ref', "HEAD", target_commit)
        run_git(package, 'update-ref', "refs/heads/#{local_branch}", current_head)
        run_git(package, 'checkout', "refs/heads/#{local_branch}")
        raise
    end
    true
end
resolve_all_branches(package, only_local: false) click to toggle source

Resolve branches based on the remote’s HEAD

Since GitHub (and others) decided to change the name of the “default” branch, we can’t assume that master is … well … master.

For this reason, a Git importer does not have a built-in default. If the branch(es) are not provided explicitly, the importer will call this method to guess the name of the default branch instead.

Call {#has_all_branches?} to determine whether it is necessary

# File lib/autobuild/import/git.rb, line 397
def resolve_all_branches(package, only_local: false)
    default_branch = resolve_remote_head(package, only_local: only_local)
    unless default_branch
        raise NetworkAccessNeeded,
              "determining the remote branch would require access to "\
              "the network, and only_local is true"
    end

    relocate(repository, default_branch: default_branch)
end
resolve_remote_head(package, only_local: false) click to toggle source

Return default local branch if exists, if not return the default remote branch

@param [Package] package

# File lib/autobuild/import/git.rb, line 375
def resolve_remote_head(package, only_local: false)
    try_resolve_remote_head_from_local(package) ||
        (!only_local && try_resolve_remote_head_from_server(package))
end
rev_parse(package, name, object_type = "commit") click to toggle source

@api private

Resolves a revision into a commit ID

@param [Package] package @param [String] name the revspec that is to be resolved @param [String] objecT_type the type of git object we want to resolve to @return [String] the commit ID @raise [PackageException] if name cannot be found

# File lib/autobuild/import/git.rb, line 918
def rev_parse(package, name, object_type = "commit")
    name = "#{name}^{#{object_type}}" if object_type
    run_git_bare(package, 'rev-parse', '-q', '--verify', name).first
rescue Autobuild::SubcommandFailed
    raise PackageException.new(package, 'import'),
          "failed to resolve #{name}. "\
          "Are you sure this commit, branch or tag exists ?"
end
run_git(package, *args) click to toggle source

@api private

Run a git command that require a working copy

@param [Package] package @param [Array] args the git arguments, excluding the git command

itself. The last argument can be a hash, in which case it is passed
as an option hash to {Package#run}
# File lib/autobuild/import/git.rb, line 542
def run_git(package, *args)
    self.class.run_git(package, *args)
end
run_git_bare(package, *args) click to toggle source

@api private

Run a git command that only need a git directory

@param (see Git#run_git)

# File lib/autobuild/import/git.rb, line 568
def run_git_bare(package, *args)
    self.class.run_git_bare(package, *args)
end
setup_remote( package, remote_name, repository, push_to = repository, only_local: true ) click to toggle source

@api private

Set a remote up in the repositorie’s configuration

# File lib/autobuild/import/git.rb, line 591
def setup_remote(
    package, remote_name, repository, push_to = repository,
    only_local: true
)
    resolve_all_branches(package, only_local: only_local) unless has_all_branches?

    run_git_bare(package, 'config', '--replace-all',
                 "remote.#{remote_name}.url", repository)
    run_git_bare(package, 'config', '--replace-all',
                 "remote.#{remote_name}.pushurl", push_to || repository)
    run_git_bare(package, 'config', '--replace-all',
                 "remote.#{remote_name}.fetch",
                 "+refs/heads/*:refs/remotes/#{remote_name}/*")

    if remote_branch && local_branch
        remote_ref = remote_branch_to_ref(remote_branch)
        run_git_bare(package, 'config', '--replace-all',
                     "remote.#{remote_name}.push",
                     "refs/heads/#{local_branch}:#{remote_ref}")
    else
        run_git_bare(package, 'config', '--replace-all',
                     "remote.#{remote_name}.push", "refs/heads/*:refs/heads/*")
    end
end
shallow?() click to toggle source

Whether clones should be shallow

# File lib/autobuild/import/git.rb, line 328
def shallow?
    @shallow
end
show(package, commit, path) click to toggle source

Returns the file’s conents at a certain commit

@param [Package] package @param [String] commit @param [String] path @return [String]

# File lib/autobuild/import/git.rb, line 933
def show(package, commit, path)
    run_git_bare(package, 'show', "#{commit}:#{path}").join("\n")
rescue Autobuild::SubcommandFailed
    raise PackageException.new(package, 'import'),
          "failed to either resolve commit #{commit} or file #{path}"
end
single_branch?() click to toggle source

Whether ‘clone’ should fetch only the remote branch, or all the branches

# File lib/autobuild/import/git.rb, line 323
def single_branch?
    @single_branch
end
status(package, options = Hash.new) click to toggle source

Returns a {Status} object that represents the status of this package w.r.t. the expected remote repository and branch

# File lib/autobuild/import/git.rb, line 779
def status(package, options = Hash.new)
    only_local =
        if options.kind_of?(Hash)
            options.fetch(:only_local, false)
        else
            options
        end

    validate_importdir(package)
    resolve_all_branches(package, only_local: only_local) unless has_all_branches?

    _pinned_state, target_commit, = determine_target_state(
        package, only_local: only_local)

    status = merge_status(package, target_commit)
    status.uncommitted_code = self.class.has_uncommitted_changes?(package)
    if (current_branch = self.current_branch(package))
        if current_branch != "refs/heads/#{local_branch}"
            status.unexpected_working_copy_state <<
                "working copy is on branch #{current_branch}, "\
                "the autoproj configuration expected it to be on #{local_branch}"
        end
    else
        status.unexpected_working_copy_state <<
            "working copy is on a detached HEAD"
    end
    status
end
tags(package, options = Hash.new) click to toggle source

The tags of this packages

@param [Package] package @option options [Boolean] only_local (false) whether the tags should

be fetch from the remote first, or if one should only list tags that
are already known locally

@return [Hash<String,String>] a mapping from a tag name to its commit

ID
# File lib/autobuild/import/git.rb, line 521
def tags(package, options = Hash.new)
    unless options.fetch(:only_local, false)
        run_git_bare(package, 'fetch', '--tags')
    end
    tag_list = run_git_bare(package, 'show-ref', '--tags').map(&:strip)
    tags = Hash.new
    tag_list.each do |entry|
        commit_to_tag = entry.split(" ")
        tags[commit_to_tag[1].sub("refs/tags/", "")] = commit_to_tag[0]
    end
    tags
end
try_resolve_remote_head_from_local(package) click to toggle source

Return the remote head branch from local copy if exists, if not return nil

@param [Package] package

# File lib/autobuild/import/git.rb, line 349
def try_resolve_remote_head_from_local(package)
    ls_local_string = run_git_bare(
        package, 'symbolic-ref',
        "refs/remotes/#{@remote_name}/HEAD"
    ).first.strip
    local_remote_head = ls_local_string.match("refs/remotes/#{@remote_name}/(.*)")
    local_remote_head ? local_remote_head[1] : nil
rescue Autobuild::SubcommandFailed # rubocop:disable Lint/SuppressedException
end
try_resolve_remote_head_from_server(package) click to toggle source

Return the remote head branch from server if exists, if not return ‘master’

@param [Package] package

# File lib/autobuild/import/git.rb, line 362
def try_resolve_remote_head_from_server(package)
    ls_remote_string = package.run(
        :import,
        Autobuild.tool('git'), 'ls-remote', '--symref', repository
    ).first.strip
    server_remote_head =
        ls_remote_string.match("ref:[^A-z]refs/heads/(.*)[^A-z]HEAD")
    server_remote_head ? server_remote_head[1] : 'master'
end
update(package, options = Hash.new) click to toggle source

@option (see Package#update)

# File lib/autobuild/import/git.rb, line 1188
def update(package, options = Hash.new)
    validate_importdir(package)

    resolve_all_branches(package) unless has_all_branches?

    only_local = options.fetch(:only_local, false)
    reset = options.fetch(:reset, false)

    # This is really really a hack to workaround how broken the
    # importdir thing is
    update_alternates(package) if package.importdir == package.srcdir

    pinned_state, target_commit, fetch_commit =
        determine_target_state(package, only_local: only_local)

    did_change_branch = ensure_on_local_branch(package, target_commit)

    # Check whether we are already at the requested state
    if pinned_state
        pin_is_uptodate, pin_did_merge =
            handle_pinned_state(package, pinned_state, reset: reset)
    end

    unless pin_is_uptodate
        fetch_commit ||= current_remote_commit(
            package, only_local: only_local,
                     refspec: [remote_branch_to_ref(remote_branch), tag]
        )
        did_update =
            if reset
                reset_head_to_commit(package, target_commit, fetch_commit,
                                     force: (reset == :force))
            else
                merge_if_simple(package, target_commit)
            end
    end

    if !only_local && with_submodules?
        run_git(package, "submodule", "update", '--init', '--recursive')
        did_update = true
    end

    did_update || pin_did_merge || did_change_branch
end
update_alternates(package) click to toggle source

@api private

Updates the git alternates file in the already checked out package to match {#alternates}

@param [Package] package the already checked-out package @return [void]

# File lib/autobuild/import/git.rb, line 1064
def update_alternates(package)
    alternates_path = File.join(git_dir(package, false),
                                'objects', 'info', 'alternates')
    current_alternates =
        if File.file?(alternates_path)
            File.readlines(alternates_path)
                .map(&:strip)
                .find_all { |l| !l.empty? }
        else
            Array.new
        end

    alternates = each_alternate_path(package).map do |path|
        File.join(path, 'objects')
    end

    unless (current_alternates.sort - alternates.sort).empty?
        # Warn that something is fishy, but assume that the user knows
        # what he is doing
        package.warn "%s: the list of git alternates listed in the repository "\
            "differs from the one set up in autobuild."
        package.warn "%s: I will update, but that is dangerous"
        package.warn "%s: using git alternates is for advanced users only, "\
            "who know git very well."
        package.warn "%s: Don't complain if something breaks"
    end
    if alternates.empty?
        FileUtils.rm_f alternates_path
    else
        File.open(alternates_path, 'w') do |io|
            io.write alternates.join("\n")
        end
    end
end
update_remotes_configuration(package, only_local: true) click to toggle source

@api private

Updates the git repository’s configuration for the target remote

# File lib/autobuild/import/git.rb, line 631
def update_remotes_configuration(package, only_local: true)
    each_configured_remote do |*args|
        setup_remote(package, *args, only_local: only_local)
    end

    if local_branch
        run_git_bare(package, 'config', '--replace-all',
                     "branch.#{local_branch}.remote", remote_name)
        run_git_bare(package, 'config', '--replace-all',
                     "branch.#{local_branch}.merge",
                     remote_branch_to_ref(local_branch))
    end
end
uses_lfs?(package) click to toggle source
# File lib/autobuild/import/git.rb, line 1287
def uses_lfs?(package)
    git_files = run_git(package, 'ls-files').join("\n")
    git_attrs = run_git(
        package, 'check-attr', '--all', '--stdin',
        input_streams: [StringIO.new(git_files)]
    ).join("\n")

    /(.*): filter: lfs/.match(git_attrs)
end
validate_importdir(package) click to toggle source

@api private

Verifies that the package’s {Package#importdir} points to a git repository

# File lib/autobuild/import/git.rb, line 342
def validate_importdir(package)
    git_dir(package, true)
end
validate_shallow(package) click to toggle source
# File lib/autobuild/import/git.rb, line 1304
def validate_shallow(package)
    return false unless shallow?

    if commit
        Autoproj.warn "#{package.name}: "\
                      "Cannot pin a commit while doing a shallow clone"
        return false
    end
    if tag && !single_branch?
        Autoproj.warn "#{package.name}: "\
                      "Cannot pin a tag while doing a shallow clone"
        return false
    end
    if @remote_branch
        Autoproj.warn "#{package.name}: "\
                      "Cannot use remote_branch while doing a shallow clone"
        return false
    end
    true
end
vcs_fingerprint(package) click to toggle source
# File lib/autobuild/import/git.rb, line 217
def vcs_fingerprint(package)
    rev_parse(package, "HEAD", @fingerprint_mode)
end
with_submodules?() click to toggle source

Whether the git checkout should be done with submodules

# File lib/autobuild/import/git.rb, line 317
def with_submodules?
    @with_submodules
end

Private Instance Methods

ensure_on_local_branch(package, target_commit) click to toggle source
# File lib/autobuild/import/git.rb, line 1233
        def ensure_on_local_branch(package, target_commit)
    if !has_local_branch?(package)
        package.message format("%%s: checking out branch %<branch>s",
                               branch: local_branch)
        run_git(package, 'checkout', '-b', local_branch, target_commit)
        true
    elsif !on_local_branch?(package)
        package.message format("%%s: switching to branch %<branch>s",
                               branch: local_branch)
        run_git(package, 'checkout', local_branch)
        true
    else
        false
    end
end
handle_pinned_state(package, pinned_state, reset: false) click to toggle source
# File lib/autobuild/import/git.rb, line 1266
        def handle_pinned_state(package, pinned_state, reset: false)
    current_head = rev_parse(package, 'HEAD')
    if reset
        [current_head == pinned_state, false]
    elsif commit_present_in?(package, pinned_state, current_head)
        [true, false]
    elsif merge_if_simple(package, pinned_state)
        [true, true]
    end
end
remote_branch_to_ref(branch) click to toggle source
# File lib/autobuild/import/git.rb, line 839
        def remote_branch_to_ref(branch)
    return unless branch

    if branch.start_with?("refs/")
        branch
    else
        "refs/heads/#{branch}"
    end
end