canopus 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,313 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Canopus
4
- module Git
5
- class Repository
6
- attr_reader :root, :git_dir, :common_dir, :odb
7
-
8
- def initialize(path)
9
- directory = File.expand_path(path)
10
- directory = File.dirname(directory) unless File.directory?(directory)
11
- loop do
12
- location = File.join(directory, ".git")
13
- if File.directory?(location)
14
- @root, @git_dir = directory, location
15
- break
16
- elsif File.file?(location)
17
- value = File.read(location).strip
18
- raise ArgumentError, "invalid gitdir file" unless value.start_with?("gitdir: ")
19
- @root, @git_dir = directory, File.expand_path(value.delete_prefix("gitdir: "), directory)
20
- break
21
- elsif File.file?(File.join(directory, "HEAD")) && File.directory?(File.join(directory, "objects"))
22
- @root, @git_dir = nil, directory
23
- break
24
- end
25
- parent = File.dirname(directory)
26
- raise ArgumentError, "not a Git repository: #{path}" if parent == directory
27
- directory = parent
28
- end
29
- common = File.join(git_dir, "commondir")
30
- @common_dir = File.file?(common) ? File.expand_path(File.read(common).strip, git_dir) : git_dir
31
- config = File.join(common_dir, "config")
32
- raise ArgumentError, "SHA-256 repositories are not supported" if File.file?(config) && File.read(config).match?(/objectformat\s*=\s*sha256/i)
33
- @odb = ObjectDatabase.new(File.join(common_dir, "objects"))
34
- end
35
-
36
- def object(oid) = odb.read(resolve(oid) || oid)
37
- def index = Index.new(File.join(git_dir, "index"))
38
- def head = resolve("HEAD")
39
-
40
- def branch
41
- text = File.read(File.join(git_dir, "HEAD")).strip
42
- text.start_with?("ref: refs/heads/") ? text.delete_prefix("ref: refs/heads/") : nil
43
- end
44
-
45
- def refs
46
- result = {}
47
- packed = File.join(common_dir, "packed-refs")
48
- if File.file?(packed)
49
- File.foreach(packed) do |line|
50
- next if line.start_with?("#", "^")
51
- oid, name = line.strip.split(" ", 2)
52
- result[name] = oid if name && /\A[0-9a-f]{40}\z/.match?(oid)
53
- end
54
- end
55
- Dir[File.join(common_dir, "refs", "**", "*")].sort.each do |path|
56
- next unless File.file?(path)
57
- name = path.delete_prefix(common_dir + "/")
58
- result[name] = resolve(name)
59
- end
60
- result
61
- end
62
-
63
- def branches = refs.keys.grep(%r{\Arefs/heads/}).map { |ref| ref.delete_prefix("refs/heads/") }.sort
64
-
65
- def resolve(name, seen = [])
66
- return name if /\A[0-9a-f]{40}\z/.match?(name.to_s)
67
- raise ArgumentError, "invalid Git reference" unless name.is_a?(String) && !name.empty? && !name.start_with?("/") && !name.split("/").any? { |piece| piece == ".." || piece.empty? } && !name.match?(/[\x00-\x20\\]/)
68
- raise CorruptObject, "cyclic symbolic reference" if seen.include?(name) || seen.length > 32
69
- candidates = name == "HEAD" || name.start_with?("refs/") ? [name] : ["refs/heads/#{name}", "refs/tags/#{name}", "refs/remotes/#{name}"]
70
- candidates.each do |candidate|
71
- directory = candidate == "HEAD" ? git_dir : common_dir
72
- path = File.join(directory, candidate)
73
- if File.file?(path)
74
- value = File.read(path).strip
75
- return resolve(value.delete_prefix("ref: "), seen + [name]) if value.start_with?("ref: ")
76
- raise CorruptObject, "invalid reference #{candidate}" unless value.match?(/\A[0-9a-f]{40}\z/)
77
- return value
78
- end
79
- packed = File.join(common_dir, "packed-refs")
80
- next unless File.file?(packed)
81
- File.foreach(packed) do |line|
82
- oid, reference = line.strip.split(" ", 2)
83
- return oid if reference == candidate && oid.match?(/\A[0-9a-f]{40}\z/)
84
- end
85
- end
86
- nil
87
- end
88
-
89
- def commit(reference = "HEAD")
90
- oid = resolve(reference)
91
- return nil unless oid
92
- seen = []
93
- loop do
94
- type, data = odb.read(oid)
95
- if type == "tag"
96
- raise CorruptObject, "cyclic annotated tag" if seen.include?(oid)
97
- seen << oid
98
- oid = data.lines.find { |line| line.start_with?("object ") }&.split&.last
99
- next
100
- end
101
- raise ArgumentError, "reference is not a commit" unless type == "commit"
102
- headers, message = data.split("\n\n", 2)
103
- values = headers.lines.reject { |line| line.start_with?(" ") }.map { |line| line.chomp.split(" ", 2) }
104
- return Commit.new(oid: oid, tree: values.assoc("tree")&.last,
105
- parents: values.select { |key, _| key == "parent" }.map(&:last),
106
- author: values.assoc("author")&.last, committer: values.assoc("committer")&.last,
107
- message: message.to_s.force_encoding(Encoding::UTF_8))
108
- end
109
- end
110
-
111
- def tree(reference = "HEAD")
112
- revision = commit(reference)
113
- revision ? read_tree(revision.tree) : {}
114
- end
115
-
116
- def blob(path, reference: "HEAD")
117
- entry = tree(reference)[path]
118
- entry && entry.mode != 0o160000 ? odb.read(entry.oid).last : nil
119
- end
120
-
121
- def status = Status.new(self).call
122
- def blame(path, reference: "HEAD") = Blame.new(self).call(path, reference: reference)
123
-
124
- def diff(path, staged: false, context: 3)
125
- entry = index[path]
126
- staged_content = entry ? odb.read(entry.oid).last : ""
127
- before = staged ? blob(path).to_s : staged_content
128
- after = staged ? staged_content : worktree_content(path).to_s
129
- Diff.hunks(before, after, context: context)
130
- end
131
-
132
- def worktree_content(path)
133
- absolute = worktree_path(path)
134
- return File.readlink(absolute).b if File.symlink?(absolute)
135
- File.file?(absolute) ? File.binread(absolute) : nil
136
- end
137
-
138
- def worktree_path(path, replacing: [])
139
- raise ArgumentError, "bare repository has no worktree" unless root
140
- raise ArgumentError, "unsafe worktree path" if path.empty? || path.start_with?("/") || path.split("/").any? { |part| ["..", ".git", ""].include?(part) }
141
- absolute = File.expand_path(path, root)
142
- raise ArgumentError, "path outside worktree" unless absolute.start_with?(root + "/")
143
- parent = File.dirname(absolute)
144
- while parent != root
145
- raise ArgumentError, "worktree parent is a symlink" if File.symlink?(parent) && !replacing.include?(parent.delete_prefix(root + "/"))
146
- parent = File.dirname(parent)
147
- end
148
- absolute
149
- end
150
-
151
- def revert_hunk(path, hunk)
152
- absolute = worktree_path(path)
153
- raise ArgumentError, "cannot revert a symlink hunk" if File.symlink?(absolute)
154
- content = File.binread(absolute)
155
- updated = Diff.revert(content, hunk)
156
- atomic_write(absolute, updated, File.stat(absolute).mode & 0o777)
157
- updated
158
- end
159
-
160
- # Checkout is deliberately limited to a clean index and tracked worktree.
161
- # Untracked files are retained and any collision aborts before the first write.
162
- def checkout(name)
163
- raise ArgumentError, "unknown branch: #{name}" unless branches.include?(name)
164
- raise ArgumentError, "worktree/index must be clean" if status.any? { |entry| entry.index != "?" }
165
- target = tree("refs/heads/#{name}")
166
- current = index.entries.to_h { |entry| [entry.path, entry] }
167
- raise ArgumentError, "submodule checkout requires separate worktree handling" if (target.values + current.values).any? { |entry| entry.mode == 0o160000 }
168
- removed = current.keys - target.keys
169
- target.each_key do |path|
170
- absolute = worktree_path(path, replacing: removed)
171
- unless current.key?(path)
172
- if File.directory?(absolute) && !File.symlink?(absolute)
173
- Find.find(absolute) do |entry|
174
- next if File.directory?(entry) && !File.symlink?(entry)
175
- raise ArgumentError, "untracked checkout collision: #{path}" unless removed.include?(entry.delete_prefix(root + "/"))
176
- end
177
- elsif File.exist?(absolute) || File.symlink?(absolute)
178
- raise ArgumentError, "untracked checkout collision: #{path}"
179
- end
180
- end
181
- parent = File.dirname(absolute)
182
- until parent == root
183
- if (File.file?(parent) || File.symlink?(parent)) && !removed.include?(parent.delete_prefix(root + "/"))
184
- raise ArgumentError, "untracked checkout collision: #{parent}"
185
- end
186
- parent = File.dirname(parent)
187
- end
188
- end
189
- contents = target.to_h { |path, entry| [path, odb.read(entry.oid).last] }
190
- backups = current.to_h { |path, entry| [path, [worktree_content(path), entry.mode]] }
191
- backups.each do |path, (data, _)|
192
- raise ArgumentError, "worktree changed before checkout: #{path}" unless data && ObjectDatabase.hash("blob", data) == current[path].oid
193
- end
194
- index_path = File.join(git_dir, "index")
195
- head_path = File.join(git_dir, "HEAD")
196
- old_index = File.binread(index_path) if File.exist?(index_path)
197
- old_head = File.binread(head_path)
198
- locks = []
199
- changed = false
200
- begin
201
- [index_path, head_path].each { |path| locks << File.open(path + ".lock", File::WRONLY | File::CREAT | File::EXCL | File::BINARY, 0o644) }
202
- changed = true
203
- removed.sort_by { |path| -path.count("/") }.each { |path| File.unlink(worktree_path(path)) }
204
- prune_empty_directories(removed)
205
- target.each do |path, entry|
206
- absolute = worktree_path(path)
207
- Dir.rmdir(absolute) if File.directory?(absolute) && !File.symlink?(absolute)
208
- FileUtils.mkdir_p(File.dirname(absolute))
209
- File.unlink(absolute) if File.symlink?(absolute)
210
- if entry.mode == 0o120000
211
- File.unlink(absolute) if File.exist?(absolute)
212
- File.symlink(contents[path], absolute)
213
- else
214
- atomic_write(absolute, contents[path], entry.mode & 0o777)
215
- end
216
- end
217
- entries = target.map do |path, entry|
218
- stat = File.lstat(worktree_path(path))
219
- Index::Entry.new(path: path, oid: entry.oid, mode: entry.mode, size: stat.size,
220
- mtime: stat.mtime.to_i, mtime_nsec: stat.mtime.nsec, ctime: stat.ctime.to_i, ctime_nsec: stat.ctime.nsec,
221
- dev: stat.dev, ino: stat.ino, uid: stat.uid, gid: stat.gid, stage: 0)
222
- end
223
- locks[0].write(Index.encode(entries))
224
- locks[1].write("ref: refs/heads/#{name}\n")
225
- locks.each { |file| file.flush; file.fsync; file.close }
226
- File.rename(index_path + ".lock", index_path)
227
- File.rename(head_path + ".lock", head_path)
228
- rescue StandardError
229
- if changed
230
- (target.keys - current.keys).each do |path|
231
- absolute = worktree_path(path)
232
- File.unlink(absolute) if File.file?(absolute) || File.symlink?(absolute)
233
- end
234
- prune_empty_directories(target.keys - current.keys)
235
- backups.each do |path, (data, mode)|
236
- absolute = worktree_path(path)
237
- Dir.rmdir(absolute) if File.directory?(absolute) && !File.symlink?(absolute)
238
- FileUtils.mkdir_p(File.dirname(absolute))
239
- File.unlink(absolute) if File.symlink?(absolute)
240
- if mode == 0o120000
241
- File.unlink(absolute) if File.exist?(absolute)
242
- File.symlink(data, absolute)
243
- else
244
- atomic_write(absolute, data, mode & 0o777)
245
- end
246
- end
247
- old_index ? atomic_write(index_path, old_index, 0o644) : File.unlink(index_path) if old_index || File.exist?(index_path)
248
- atomic_write(head_path, old_head, 0o644)
249
- end
250
- raise
251
- ensure
252
- locks.each do |file|
253
- file.close unless file.closed?
254
- File.unlink(file.path) if File.exist?(file.path)
255
- end
256
- end
257
- name
258
- end
259
-
260
- private
261
-
262
- def prune_empty_directories(paths)
263
- paths.sort_by { |path| -path.count("/") }.each do |path|
264
- parent = File.dirname(File.join(root, path))
265
- until parent == root
266
- begin
267
- Dir.rmdir(parent)
268
- rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT, Errno::ENOTDIR
269
- break
270
- end
271
- parent = File.dirname(parent)
272
- end
273
- end
274
- end
275
-
276
- def atomic_write(path, data, mode)
277
- Tempfile.create([".canopus-", ".tmp"], File.dirname(path)) do |file|
278
- file.binmode
279
- file.chmod(mode)
280
- file.write(data)
281
- file.flush
282
- file.fsync
283
- file.close
284
- File.rename(file.path, path)
285
- end
286
- end
287
-
288
- def read_tree(oid, prefix = "", stack = [])
289
- raise CorruptObject, "tree nesting exceeds limit" if stack.length > 256 || stack.include?(oid)
290
- type, data = odb.read(oid)
291
- raise CorruptObject, "expected tree object" unless type == "tree"
292
- result = {}
293
- offset = 0
294
- while offset < data.bytesize
295
- ending = data.index("\0", offset)
296
- raise CorruptObject, "truncated tree entry" unless ending && ending + 21 <= data.bytesize
297
- mode, name = data[offset...ending].split(" ", 2)
298
- raise CorruptObject, "unsafe tree entry" unless mode&.match?(/\A[0-7]+\z/) && name && !["", ".", "..", ".git"].include?(name) && !name.include?("/")
299
- path = (prefix + name).force_encoding(Encoding::UTF_8)
300
- object = data[ending + 1, 20].unpack1("H*")
301
- mode = mode.to_i(8)
302
- if mode == 0o40000
303
- result.merge!(read_tree(object, path + "/", stack + [oid]))
304
- else
305
- result[path] = TreeEntry.new(path: path, oid: object, mode: mode)
306
- end
307
- offset = ending + 21
308
- end
309
- result
310
- end
311
- end
312
- end
313
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Canopus
4
- module Git
5
- class Status
6
- Entry = Struct.new(:path, :index, :worktree, keyword_init: true) do
7
- def code = index + worktree
8
- end
9
-
10
- def initialize(repository)
11
- @repository = repository
12
- end
13
-
14
- def call
15
- repository = @repository
16
- index = repository.index
17
- head = repository.tree
18
- tracked = index.entries.group_by(&:path)
19
- result = []
20
- (head.keys | tracked.keys).sort.each do |path|
21
- entries = tracked[path]
22
- if entries&.any? { |entry| entry.stage != 0 }
23
- stages = entries.map(&:stage)
24
- code = {[1] => "DD", [2] => "AU", [3] => "UA", [1, 2] => "UD", [1, 3] => "DU", [2, 3] => "AA", [1, 2, 3] => "UU"}.fetch(stages.sort, "UU")
25
- result << Entry.new(path: path, index: code[0], worktree: code[1])
26
- next
27
- end
28
- staged = entries&.first
29
- original = head[path]
30
- x = if !original then "A"
31
- elsif !staged then "D"
32
- elsif original.mode != staged.mode || original.oid != staged.oid then "M"
33
- else " " end
34
- y = staged ? worktree_status(staged, index.path) : " "
35
- result << Entry.new(path: path, index: x, worktree: y) unless x == " " && y == " "
36
- end
37
- submodules = index.entries.select { |entry| entry.mode == 0o160000 }.map { |entry| entry.path + "/" }
38
- Project.new(repository.root).files(include_symlinks: true).each do |path|
39
- next if submodules.any? { |prefix| path.start_with?(prefix) }
40
- result << Entry.new(path: path, index: "?", worktree: "?") unless tracked.key?(path)
41
- end
42
- result.sort_by(&:path)
43
- end
44
-
45
- private
46
-
47
- def worktree_status(entry, index_path)
48
- absolute = @repository.worktree_path(entry.path)
49
- stat = File.lstat(absolute)
50
- if entry.mode == 0o160000
51
- return "T" unless stat.directory?
52
- begin
53
- return Repository.new(absolute).head == entry.oid ? " " : "M"
54
- rescue ArgumentError
55
- return "M"
56
- end
57
- end
58
- mode = stat.symlink? ? 0o120000 : stat.file? ? 0o100000 | ((stat.mode & 0o100).positive? ? 0o755 : 0o644) : 0
59
- return "T" if (mode & 0o170000) != (entry.mode & 0o170000)
60
- return "M" if mode != entry.mode
61
- return " " if (entry.extended_flags.to_i & 0x4000).positive? # skip-worktree
62
- index_time = File.mtime(index_path)
63
- if stat.size == entry.size && stat.mtime.to_i == entry.mtime && stat.mtime.nsec == entry.mtime_nsec &&
64
- stat.ctime.to_i == entry.ctime && stat.ctime.nsec == entry.ctime_nsec && stat.mtime.to_i < index_time.to_i
65
- return " "
66
- end
67
- content = stat.symlink? ? File.readlink(absolute).b : File.binread(absolute)
68
- ObjectDatabase.hash("blob", content) == entry.oid ? " " : "M"
69
- rescue Errno::ENOENT, Errno::ENOTDIR
70
- (entry.extended_flags.to_i & 0x4000).positive? ? " " : "D"
71
- end
72
- end
73
- end
74
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Canopus
4
- module Git
5
- TreeEntry = Struct.new(:path, :oid, :mode, keyword_init: true)
6
- end
7
- end
data/lib/canopus/git.rb DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "find"
5
- require "tempfile"
6
- require_relative "project"
7
- require_relative "git/object_database"
8
- require_relative "git/index"
9
- require_relative "git/diff"
10
- require_relative "git/status"
11
- require_relative "git/blame"
12
- require_relative "git/commit"
13
- require_relative "git/tree_entry"
14
-
15
- require_relative "git/repository"
data/sig/git.rbs DELETED
@@ -1,140 +0,0 @@
1
- module Canopus
2
- module Git
3
- type object = [String, String]
4
- type lines = String | Array[String]
5
- class CorruptObject < StandardError
6
- end
7
- class Commit < Struct[untyped]
8
- attr_accessor oid: String
9
- attr_accessor tree: String?
10
- attr_accessor parents: Array[String]
11
- attr_accessor author: String?
12
- attr_accessor committer: String?
13
- attr_accessor message: String
14
- def self.new: (oid: String, ?tree: String?, parents: Array[String], ?author: String?, ?committer: String?, message: String) -> instance
15
- end
16
- class TreeEntry < Struct[untyped]
17
- attr_accessor path: String
18
- attr_accessor oid: String
19
- attr_accessor mode: Integer
20
- def self.new: (path: String, oid: String, mode: Integer) -> instance
21
- end
22
- class Repository
23
- attr_reader root: String?
24
- attr_reader git_dir: String
25
- attr_reader common_dir: String
26
- attr_reader odb: ObjectDatabase
27
- def initialize: (String path) -> void
28
- def object: (String oid) -> object
29
- def index: () -> Index
30
- def head: () -> String?
31
- def branch: () -> String?
32
- def refs: () -> Hash[String, String?]
33
- def branches: () -> Array[String]
34
- def resolve: (String name, ?Array[String] seen) -> String?
35
- def commit: (?String reference) -> Commit?
36
- def tree: (?String reference) -> Hash[String, TreeEntry]
37
- def blob: (String path, ?reference: String) -> String?
38
- def status: () -> Array[Status::Entry]
39
- def blame: (String path, ?reference: String) -> Array[Blame::Line]
40
- def diff: (String path, ?staged: bool, ?context: Integer) -> Array[Diff::Hunk]
41
- def worktree_content: (String path) -> String?
42
- def worktree_path: (String path, ?replacing: Array[String]) -> String
43
- def revert_hunk: (String path, Diff::Hunk hunk) -> String
44
- def checkout: (String name) -> String
45
- end
46
- class ObjectDatabase
47
- attr_reader directory: String
48
- def initialize: (String directory) -> void
49
- def self.hash: (String type, String data) -> String
50
- def read: (String oid, ?Array[String] seen) -> object
51
- def packs: () -> Array[Pack]
52
- end
53
- class Pack
54
- TYPES: Hash[Integer, String]
55
- MAX_OBJECT_SIZE: Integer
56
- attr_reader path: String
57
- attr_reader offsets: Hash[String, Integer]
58
- def initialize: (String index_path) -> void
59
- def include?: (String oid) -> bool
60
- def read: (String oid) ?{ (String) -> object } -> object
61
- def self.apply_delta: (String base, String delta) -> String
62
- end
63
- class Index
64
- include Enumerable[Entry]
65
- class Entry < Struct[untyped]
66
- attr_accessor path: String
67
- attr_accessor oid: String
68
- attr_accessor mode: Integer
69
- attr_accessor size: Integer
70
- attr_accessor mtime: Integer
71
- attr_accessor mtime_nsec: Integer
72
- attr_accessor ctime: Integer
73
- attr_accessor ctime_nsec: Integer
74
- attr_accessor dev: Integer
75
- attr_accessor ino: Integer
76
- attr_accessor uid: Integer
77
- attr_accessor gid: Integer
78
- attr_accessor stage: Integer
79
- attr_accessor flags: Integer?
80
- attr_accessor extended_flags: Integer?
81
- def self.new: (path: String, oid: String, mode: Integer, size: Integer, mtime: Integer, mtime_nsec: Integer, ctime: Integer, ctime_nsec: Integer, dev: Integer, ino: Integer, uid: Integer, gid: Integer, stage: Integer, ?flags: Integer?, ?extended_flags: Integer?) -> instance
82
- end
83
- attr_reader entries: Array[Entry]
84
- attr_reader version: Integer
85
- attr_reader path: String
86
- def initialize: (String path) -> void
87
- def each: () -> Enumerator[Entry, Array[Entry]]
88
- | () { (Entry) -> void } -> Array[Entry]
89
- def []: (String path) -> Entry?
90
- def self.encode: (Array[Entry] entries) -> String
91
- end
92
- module Diff
93
- class Edit < Struct[untyped]
94
- attr_accessor kind: :equal | :insert | :delete
95
- attr_accessor old_line: Integer
96
- attr_accessor new_line: Integer
97
- attr_accessor text: String
98
- def self.new: (kind: :equal | :insert | :delete, old_line: Integer, new_line: Integer, text: String) -> instance
99
- end
100
- class Hunk < Struct[untyped]
101
- attr_accessor old_start: Integer
102
- attr_accessor old_count: Integer
103
- attr_accessor new_start: Integer
104
- attr_accessor new_count: Integer
105
- attr_accessor edits: Array[Edit]
106
- def self.new: (old_start: Integer, old_count: Integer, new_start: Integer, new_count: Integer, edits: Array[Edit]) -> instance
107
- def old_text: () -> String
108
- def new_text: () -> String
109
- end
110
- def self.edits: (lines before, lines after) -> Array[Edit]
111
- def self.hunks: (lines before, lines after, ?context: Integer) -> Array[Hunk]
112
- def self.revert: (String text, Hunk hunk) -> String
113
- def self.unified: (lines before, lines after, ?old_name: String, ?new_name: String, ?context: Integer) -> String
114
- end
115
- class Status
116
- class Entry < Struct[untyped]
117
- attr_accessor path: String
118
- attr_accessor index: String
119
- attr_accessor worktree: String
120
- def self.new: (path: String, index: String, worktree: String) -> instance
121
- def code: () -> String
122
- end
123
- def initialize: (Repository repository) -> void
124
- def call: () -> Array[Entry]
125
- end
126
- class Blame
127
- class Line < Struct[untyped]
128
- attr_accessor line: Integer
129
- attr_accessor text: String
130
- attr_accessor commit: String
131
- attr_accessor author: String?
132
- attr_accessor original_line: Integer
133
- attr_accessor path: String
134
- def self.new: (line: Integer, text: String, commit: String, author: String?, original_line: Integer, path: String) -> instance
135
- end
136
- def initialize: (Repository repository) -> void
137
- def call: (String path, ?reference: String) -> Array[Line]
138
- end
139
- end
140
- end