Class: RBS::Collection::Sources::Git

Inherits:
Object
  • Object
show all
Includes:
Base
Defined in:
lib/rbs/collection/sources/git.rb,
sig/collection/sources.rbs

Defined Under Namespace

Classes: CommandError

Constant Summary collapse

METADATA_FILENAME =

Returns:

  • (String)
'.rbs_meta.yaml'

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Base

#dependencies_of

Constructor Details

#initialize(name:, revision:, remote:, repo_dir:) ⇒ Git

Returns a new instance of Git.

Parameters:

  • name: (String)
  • revision: (String)
  • remote: (String)
  • repo_dir: (String, nil)


19
20
21
22
23
24
25
# File 'lib/rbs/collection/sources/git.rb', line 19

def initialize(name:, revision:, remote:, repo_dir:)
  @name = name
  @remote = remote
  @repo_dir = repo_dir || 'gems'
  @revision = revision
  @need_setup = true
end

Instance Attribute Details

#nameString (readonly)

Returns the value of attribute name.

Returns:

  • (String)


17
18
19
# File 'lib/rbs/collection/sources/git.rb', line 17

def name
  @name
end

#remoteString (readonly)

Returns the value of attribute remote.

Returns:

  • (String)


17
18
19
# File 'lib/rbs/collection/sources/git.rb', line 17

def remote
  @remote
end

#repo_dirString (readonly)

Returns the value of attribute repo_dir.

Returns:

  • (String)


17
18
19
# File 'lib/rbs/collection/sources/git.rb', line 17

def repo_dir
  @repo_dir
end

#revisionString (readonly)

Returns the value of attribute revision.

Returns:

  • (String)


17
18
19
# File 'lib/rbs/collection/sources/git.rb', line 17

def revision
  @revision
end

Instance Method Details

#_install(dest:, name:, version:) ⇒ void

This method returns an undefined value.

Parameters:

  • dest: (Pathname)
  • name: (String)
  • version: (String)


88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rbs/collection/sources/git.rb', line 88

private def _install(dest:, name:, version:)
  # Should checkout that revision to support symlinks
  git("reset", "--hard", resolved_revision)

  dir = dest.join(name, version)
  dir.mkpath
  src = gem_repo_dir.join(name, version)

  cp_r(src, dir)
  (dir: dir, name: name, version: version)
end

#commit_hash?Boolean

Returns true if revision looks like a commit hash

Returns:

  • (Boolean)


104
105
106
# File 'sig/collection/sources.rbs', line 104

private def commit_hash?
  revision.match?(/\A[a-f0-9]{40}\z/)
end

#cp_r(src, dest) ⇒ void

This method returns an undefined value.

Parameters:

  • (Pathname)
  • (Pathname)


100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/rbs/collection/sources/git.rb', line 100

private def cp_r(src, dest)
  Find.find(src) do |file_src|
    file_src = Pathname(file_src)

    # Skip file if it starts with _, such as _test/
    Find.prune if file_src.basename.to_s.start_with?('_')

    file_src_relative = file_src.relative_path_from(src)
    file_dest = dest.join(file_src_relative)
    file_dest.dirname.mkpath
    FileUtils.copy_entry(file_src, file_dest, false, true) unless file_src.directory?
  end
end

#format_config_entry(name, version) ⇒ String

Parameters:

  • name (String)
  • version (String)

Returns:

  • (String)


124
125
126
127
128
129
# File 'lib/rbs/collection/sources/git.rb', line 124

private def format_config_entry(name, version)
  rev = resolved_revision[0..10]
  desc = "#{name}@#{rev}"

  "#{name}:#{version} (#{desc})"
end

#gem_repo_dirPathname

The full path of repo_dir in the local git repository

Returns:

  • (Pathname)


101
102
103
# File 'sig/collection/sources.rbs', line 101

private def gem_repo_dir
  git_dir.join @repo_dir
end

#gems_versionsHash[String, Set[String]]

Returns:

  • (Hash[String, Set[String]])


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/rbs/collection/sources/git.rb', line 236

private def gems_versions
  @gems_versions ||= begin
    repo_path = Pathname(repo_dir)

    paths = git('ls-tree', '--full-tree', '-dr', '--name-only', '-z', resolved_revision, File.join(repo_dir, "")).split("\0").map {|line| Pathname(line) }

    # @type var versions: Hash[String, Set[String]]
    versions = {}

    paths.each do |full_path|
      path = full_path.relative_path_from(repo_path)

      gem_name, version = path.descend.take(2)

      if gem_name
        versions[gem_name.to_s] ||= Set[]

        if version && !version.basename.to_s.start_with?('_')
          versions[gem_name.to_s] << version.basename.to_s
        end
      end
    end

    versions
  end
end

#git(*cmd, **opt) ⇒ String

Executes a git command, raises an error if failed

Parameters:

  • cmd (String)
  • opt (Object)

Returns:

  • (String)


107
108
109
# File 'sig/collection/sources.rbs', line 107

private def git(*cmd, **opt)
  sh! 'git', *cmd, **opt
end

#git?(*cmd, **opt) ⇒ String?

Executes a git command, returns nil if failed

Parameters:

  • cmd (String)
  • opt (Object)

Returns:

  • (String, nil)


110
111
112
113
114
# File 'sig/collection/sources.rbs', line 110

private def git?(*cmd, **opt)
  git(*cmd, **opt)
rescue CommandError
  nil
end

#git_dirPathname

The full path of local git repository

Returns:

  • (Pathname)


98
99
100
101
102
103
104
105
106
# File 'sig/collection/sources.rbs', line 98

private def git_dir
  @git_dir ||= (
    base = Pathname(ENV['XDG_CACHE_HOME'] || File.expand_path("~/.cache"))
    cache_key = remote.start_with?('.') ? "#{remote}\0#{Dir.pwd}" : remote
    dir = base.join('rbs', Digest::SHA256.hexdigest(cache_key))
    dir.mkpath
    dir
  )
end

#has?(name, version) ⇒ Boolean

Parameters:

  • name (String)
  • version (String, nil)

Returns:

  • (Boolean)


27
28
29
30
31
32
33
34
35
# File 'lib/rbs/collection/sources/git.rb', line 27

def has?(name, version)
  setup! do
    if version
      (gems_versions[name] || Set[]).include?(version)
    else
      gems_versions.key?(name)
    end
  end
end

#install(dest:, name:, version:, stdout:) ⇒ void

This method returns an undefined value.

Parameters:

  • dest: (Pathname)
  • name: (String)
  • version: (String)
  • stdout: (CLI::_IO)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/rbs/collection/sources/git.rb', line 44

def install(dest:, name:, version:, stdout:)
  setup!()

  gem_dir = dest.join(name, version)

  colored_io = CLI::ColoredIO.new(stdout: stdout)

  case
  when gem_dir.symlink?
    colored_io.puts_green("Updating to #{format_config_entry(name, version)} from a local source")
    gem_dir.unlink
    _install(dest: dest, name: name, version: version)
  when gem_dir.directory?
    prev = (dir: gem_dir)

    if prev == (name: name, version: version)
      colored_io.puts "Using #{format_config_entry(name, version)}"
    else
      colored_io.puts_green("Updating to #{format_config_entry(name, version)} from #{format_config_entry(prev["name"], prev["version"])}")
      FileUtils.remove_entry_secure(gem_dir.to_s)
      _install(dest: dest, name: name, version: version)
    end
  when !gem_dir.exist?
    colored_io.puts_green("Installing #{format_config_entry(name, version)}")
    _install(dest: dest, name: name, version: version)
  else
    raise
  end
end

#load_metadata(dir:) ⇒ metadata

Load .rbs_meta.yaml from the dir, where is the destination of the RBS file installation, and return the cleaned up content

Parameters:

  • dir: (Pathname)

Returns:

  • (metadata)


125
126
127
128
129
# File 'sig/collection/sources.rbs', line 125

def (dir:)
  # @type var content: Hash[String, untyped]
  content = YAML.load_file(dir.join(METADATA_FILENAME).to_s)
  _ = content.slice("name", "version", "source")
end

#manifest_of(name, version) ⇒ manifest_entry?

Parameters:

  • name (String)
  • version (String)

Returns:

  • (manifest_entry, nil)


74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/rbs/collection/sources/git.rb', line 74

def manifest_of(name, version)
  setup! do
    path = File.join(repo_dir, name, version, 'manifest.yaml')
    content = git('cat-file', '-p', "#{resolved_revision}:#{path}")
    YAML.safe_load(content)
  rescue CommandError
    if has?(name, version)
      nil
    else
      raise
    end
  end
end

#metadata_content(name:, version:) ⇒ metadata

Parameters:

  • name: (String)
  • version: (String)

Returns:

  • (metadata)


214
215
216
217
218
219
220
# File 'lib/rbs/collection/sources/git.rb', line 214

def (name:, version:)
  {
    "name" => name,
    "version" => version,
    "source" => to_lockfile
  }
end

#need_to_fetch?(revision) ⇒ Boolean

Parameters:

  • revision (String)

Returns:

  • (Boolean)


159
160
161
162
163
# File 'lib/rbs/collection/sources/git.rb', line 159

private def need_to_fetch?(revision)
  return true unless commit_hash?

  !git?('cat-file', '-e', revision)
end

#resolved_revisionString

Returns:

  • (String)


179
180
181
182
183
184
185
186
187
188
# File 'lib/rbs/collection/sources/git.rb', line 179

def resolved_revision
  @resolved_revision ||=
    begin
      if commit_hash?
        revision
      else
        setup! { git('rev-parse', "refs/remotes/origin/#{revision}").chomp }
      end
    end
end

#setup!void #setup!void

Ensure the git repository exists, and

  • When revision is a commit hash, the commit exists in the local repository, or
  • When revision is a branch name, the latest version is fetched from origin

It may require a network connection to fetch or clone the repository from remote.

  • If revision is a commit hash and the commit doesn't exists in the local repository, it runs git fetch
  • If revision is a branch name, it runs git fetch once per instance

Overloads:

  • #setup!void

    This method returns an undefined value.

  • #setup!void

    This method returns an undefined value.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'sig/collection/sources.rbs', line 92

private def setup!
  if @need_setup
    git_dir.mkpath
    if git_dir.join('.git').directory?
      if need_to_fetch?(revision)
        git 'fetch', 'origin'
      end
    else
      begin
        # git v2.27.0 or greater
        git 'clone', '--filter=blob:none', remote, git_dir.to_s, chdir: nil
      rescue CommandError
        # The failed `--filter=blob:none` clone may leave behind a
        # partial repository (e.g. when the server returned an error
        # mid-clone), which would cause the fallback `git clone` to
        # fail with "destination path ... already exists and is not
        # an empty directory". Clean it up before retrying.
        FileUtils.rm_rf(git_dir)
        git 'clone', remote, git_dir.to_s, chdir: nil
      end
    end

    @need_setup = false
  end

  yield if block_given?
end

#sh!(*cmd, **opt) ⇒ String

Parameters:

  • cmd (String)
  • opt (Object)

Returns:

  • (String)


204
205
206
207
208
209
210
211
212
# File 'lib/rbs/collection/sources/git.rb', line 204

private def sh!(*cmd, **opt)
  RBS.logger.debug "$ #{cmd.join(' ')}"
  opt = { chdir: git_dir }.merge(opt).compact
  (__skip__ = Open3.capture3(*cmd, **opt)).then do |out, err, status|
    raise CommandError, "Unexpected status #{status.exitstatus}\n\n#{err}" unless status.success?

    out
  end
end

#to_lockfilesource_entry

Returns:

  • (source_entry)


114
115
116
117
118
119
120
121
122
# File 'lib/rbs/collection/sources/git.rb', line 114

def to_lockfile
  {
    'type' => 'git',
    'name' => name,
    'revision' => resolved_revision,
    'remote' => remote,
    'repo_dir' => repo_dir,
  }
end

#versions(name) ⇒ Array[String]

Parameters:

  • name (String)

Returns:

  • (Array[String])


37
38
39
40
41
42
# File 'lib/rbs/collection/sources/git.rb', line 37

def versions(name)
  setup! do
    versions = gems_versions[name] or raise "Git source `#{name}` doesn't have `#{name}`"
    versions.sort
  end
end

#write_metadata(dir:, name:, version:) ⇒ void

This method returns an undefined value.

Write .rbs_meta.yaml

Parameters:

  • dir: (Pathname)
  • name: (String)
  • version: (String)


121
122
123
124
125
126
127
# File 'sig/collection/sources.rbs', line 121

def (dir:, name:, version:)
  dir.join(METADATA_FILENAME).write(
    YAML.dump(
      (name: name, version: version)
    )
  )
end