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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +118 -86
- data/assets/canopus-editor.png +0 -0
- data/assets/logo.png +0 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/002-bounded-diff-engine.md +4 -4
- data/docs/adr/006-tab-close-lifetime.md +25 -0
- data/docs/adr/007-terminal-key-routing.md +24 -0
- data/lib/canopus/controller.rb +101 -23
- data/lib/canopus/language/syntax_worker.rb +1 -0
- data/lib/canopus/pane.rb +18 -2
- data/lib/canopus/settings.rb +53 -1
- data/lib/canopus/terminal/pty.rb +92 -17
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/workspace/git_aware.rb +25 -20
- data/lib/canopus/workspace/session_persistable.rb +32 -1
- data/lib/canopus/workspace/view/terminal_presentable.rb +57 -17
- data/lib/canopus/workspace/view.rb +22 -10
- data/lib/canopus/workspace.rb +303 -17
- data/lib/canopus.rb +2 -1
- data/sig/canopus.rbs +28 -1
- data/sig/controller.rbs +1 -0
- data/sig/terminal.rbs +7 -2
- data/sig/workspace_services.rbs +4 -2
- data/tools/check_dependencies.rb +1 -1
- metadata +40 -15
- data/lib/canopus/git/blame.rb +0 -50
- data/lib/canopus/git/commit.rb +0 -7
- data/lib/canopus/git/corrupt_object.rb +0 -7
- data/lib/canopus/git/diff.rb +0 -131
- data/lib/canopus/git/index.rb +0 -94
- data/lib/canopus/git/object_database.rb +0 -45
- data/lib/canopus/git/pack.rb +0 -186
- data/lib/canopus/git/repository.rb +0 -313
- data/lib/canopus/git/status.rb +0 -74
- data/lib/canopus/git/tree_entry.rb +0 -7
- data/lib/canopus/git.rb +0 -15
- data/sig/git.rbs +0 -140
|
@@ -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
|
data/lib/canopus/git/status.rb
DELETED
|
@@ -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
|
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
|