thuban 0.2.0 → 0.3.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 +10 -0
- data/README.md +89 -7
- data/lib/thuban/commit_writer.rb +60 -0
- data/lib/thuban/history_writer.rb +317 -0
- data/lib/thuban/index.rb +46 -6
- data/lib/thuban/index_writer.rb +95 -0
- data/lib/thuban/object_writer.rb +139 -0
- data/lib/thuban/pack_writer.rb +76 -0
- data/lib/thuban/ref_lock_error.rb +6 -0
- data/lib/thuban/ref_store.rb +270 -0
- data/lib/thuban/signature.rb +5 -0
- data/lib/thuban/stash.rb +156 -0
- data/lib/thuban/version.rb +1 -1
- data/lib/thuban/worktree_files.rb +1 -7
- data/lib/thuban.rb +9 -0
- data/sig/thuban.rbs +54 -1
- metadata +11 -2
data/lib/thuban/stash.rb
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thuban
|
|
4
|
+
class Repository
|
|
5
|
+
def stash_push(message: nil, include_untracked: false)
|
|
6
|
+
current_head = head
|
|
7
|
+
raise ArgumentError, "cannot stash on an unborn branch" unless current_head
|
|
8
|
+
current_index = index
|
|
9
|
+
raise ArgumentError, "cannot stash unresolved conflicts" unless current_index.conflicts.empty?
|
|
10
|
+
|
|
11
|
+
changes = status
|
|
12
|
+
untracked = changes.select { |entry| entry.index == "?" }.map(&:path)
|
|
13
|
+
return nil unless changes.any? { |entry| entry.index != "?" } || (include_untracked && !untracked.empty?)
|
|
14
|
+
|
|
15
|
+
identity = operation_signature
|
|
16
|
+
description = "#{current_head[0, 7]} #{require_commit(current_head).message.lines.first.to_s.strip}"
|
|
17
|
+
location = branch || "(no branch)"
|
|
18
|
+
stash_message = message ? "On #{location}: #{message}" : "WIP on #{location}: #{description}"
|
|
19
|
+
index_tree = write_tree_from_entries(current_index.entries)
|
|
20
|
+
index_commit = write_commit(tree: index_tree, parents: [current_head], author: identity,
|
|
21
|
+
message: "index on #{location}: #{description}")
|
|
22
|
+
parents = [current_head, index_commit]
|
|
23
|
+
if include_untracked && !untracked.empty?
|
|
24
|
+
untracked_tree = write_tree_from_entries(worktree_entries(untracked))
|
|
25
|
+
parents << write_commit(tree: untracked_tree, author: identity,
|
|
26
|
+
message: "untracked files on #{location}: #{description}")
|
|
27
|
+
end
|
|
28
|
+
worktree_tree = write_tree_from_entries(worktree_entries((tree(current_head).keys | current_index.entries.map(&:path))))
|
|
29
|
+
oid = write_commit(tree: worktree_tree, parents: parents, author: identity, message: stash_message)
|
|
30
|
+
RefStore.new(self).update("refs/stash", oid, old_oid: resolve("refs/stash"), message: stash_message) do
|
|
31
|
+
replace_repository_state(tree(current_head), tree(current_head), remove_paths: include_untracked ? untracked : [])
|
|
32
|
+
end
|
|
33
|
+
oid
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stash_list
|
|
37
|
+
stash_records.reverse.map { |record| require_commit(record_oid(record)) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stash_pop(index = 0)
|
|
41
|
+
raise ArgumentError, "stash index must be a non-negative Integer" unless index.is_a?(Integer) && index >= 0
|
|
42
|
+
ensure_clean_tracked_state!
|
|
43
|
+
records = stash_records
|
|
44
|
+
position = records.length - index - 1
|
|
45
|
+
raise ArgumentError, "stash not found: #{index}" if position.negative?
|
|
46
|
+
|
|
47
|
+
oid = record_oid(records.fetch(position))
|
|
48
|
+
stored = require_commit(oid)
|
|
49
|
+
raise CorruptObject, "invalid stash commit" unless stored.parents.length >= 2
|
|
50
|
+
current_head = head
|
|
51
|
+
raise ArgumentError, "cannot pop a stash on an unborn branch" unless current_head
|
|
52
|
+
|
|
53
|
+
base_tree = tree(stored.parents[0])
|
|
54
|
+
stored_index = tree(stored.parents[1])
|
|
55
|
+
index_tree = apply_tree_change(base_tree, stored_index, tree(current_head), "stash pop")
|
|
56
|
+
worktree_tree = apply_tree_change(stored_index, tree(stored.oid), index_tree, "stash pop")
|
|
57
|
+
if stored.parents[2]
|
|
58
|
+
untracked_tree = tree(stored.parents[2])
|
|
59
|
+
collisions = untracked_tree.keys & worktree_tree.keys
|
|
60
|
+
raise ArgumentError, "stash pop conflicts: #{collisions.sort.join(', ')}" unless collisions.empty?
|
|
61
|
+
|
|
62
|
+
worktree_tree = worktree_tree.merge(untracked_tree)
|
|
63
|
+
end
|
|
64
|
+
drop_stash(records, position) { replace_repository_state(worktree_tree, index_tree) }
|
|
65
|
+
oid
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def worktree_entries(paths)
|
|
71
|
+
paths.sort.filter_map do |path|
|
|
72
|
+
absolute = worktree_path(path)
|
|
73
|
+
stat = File.lstat(absolute)
|
|
74
|
+
next unless stat.file? || stat.symlink?
|
|
75
|
+
|
|
76
|
+
contents = stat.symlink? ? File.readlink(absolute).b : File.binread(absolute)
|
|
77
|
+
mode = stat.symlink? ? 0o120000 : (stat.mode & 0o100).positive? ? 0o100755 : 0o100644
|
|
78
|
+
Index::Entry.new(path: path, oid: write_blob(contents), mode: mode, size: stat.size,
|
|
79
|
+
mtime: stat.mtime.to_i, mtime_nsec: stat.mtime.nsec, ctime: stat.ctime.to_i, ctime_nsec: stat.ctime.nsec,
|
|
80
|
+
dev: stat.dev, ino: stat.ino, uid: stat.uid, gid: stat.gid, stage: 0, flags: 0, extended_flags: 0)
|
|
81
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def stash_records
|
|
87
|
+
path = File.join(common_dir, "logs", "refs", "stash")
|
|
88
|
+
return [] unless File.file?(path)
|
|
89
|
+
|
|
90
|
+
RefStore.new(self).send(:validate_storage_path, path)
|
|
91
|
+
File.readlines(path, chomp: true, encoding: Encoding::BINARY)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def record_oid(record)
|
|
95
|
+
oid = record.split(" ", 3)[1]
|
|
96
|
+
raise CorruptObject, "invalid stash reflog" unless oid&.match?(/\A[0-9a-f]{40}\z/)
|
|
97
|
+
|
|
98
|
+
oid
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def drop_stash(records, position)
|
|
102
|
+
kept = records.dup
|
|
103
|
+
kept.delete_at(position)
|
|
104
|
+
ref_path = File.join(common_dir, "refs", "stash")
|
|
105
|
+
log_path = File.join(common_dir, "logs", "refs", "stash")
|
|
106
|
+
locks = {}
|
|
107
|
+
[ref_path, log_path].sort.each do |path|
|
|
108
|
+
RefStore.new(self).send(:prepare_storage_path, path)
|
|
109
|
+
locks[path] = File.open(path + ".lock", File::WRONLY | File::CREAT | File::EXCL | File::BINARY, 0o644)
|
|
110
|
+
end
|
|
111
|
+
unchanged = resolve("refs/stash") == record_oid(records.last) && stash_records == records
|
|
112
|
+
raise RefLockError, "stash changed" unless unchanged
|
|
113
|
+
metadata = [ref_path, log_path].to_h do |path|
|
|
114
|
+
[path, File.file?(path) ? [File.binread(path), File.stat(path).mode & 0o777] : nil]
|
|
115
|
+
end
|
|
116
|
+
unless kept.empty?
|
|
117
|
+
locks.fetch(ref_path).write("#{record_oid(kept.last)}\n")
|
|
118
|
+
locks.fetch(log_path).write(kept.join("\n") + "\n")
|
|
119
|
+
locks.each_value { |file| file.flush; file.fsync; file.close }
|
|
120
|
+
end
|
|
121
|
+
rollback = yield
|
|
122
|
+
begin
|
|
123
|
+
if kept.empty?
|
|
124
|
+
File.unlink(ref_path) if File.file?(ref_path)
|
|
125
|
+
File.unlink(log_path) if File.file?(log_path)
|
|
126
|
+
else
|
|
127
|
+
[log_path, ref_path].each do |path|
|
|
128
|
+
File.rename(locks.fetch(path).path, path)
|
|
129
|
+
locks.delete(path)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
rescue StandardError
|
|
133
|
+
restore_stash_metadata(metadata)
|
|
134
|
+
rollback.call if rollback.respond_to?(:call)
|
|
135
|
+
raise
|
|
136
|
+
end
|
|
137
|
+
rescue Errno::EEXIST => error
|
|
138
|
+
raise RefLockError, "stash is locked: #{error.message}"
|
|
139
|
+
ensure
|
|
140
|
+
locks&.each_value do |file|
|
|
141
|
+
file.close unless file.closed?
|
|
142
|
+
File.unlink(file.path) if File.exist?(file.path)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def restore_stash_metadata(metadata)
|
|
147
|
+
metadata.each do |path, backup|
|
|
148
|
+
if backup
|
|
149
|
+
atomic_write(path, *backup)
|
|
150
|
+
elsif File.file?(path) || File.symlink?(path)
|
|
151
|
+
File.unlink(path)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/thuban/version.rb
CHANGED
|
@@ -12,9 +12,7 @@ module Thuban
|
|
|
12
12
|
def each(&block)
|
|
13
13
|
return enum_for(__method__) unless block
|
|
14
14
|
|
|
15
|
-
rules = IgnoreMatcher.
|
|
16
|
-
exclude = File.join(@common_dir, "info", "exclude")
|
|
17
|
-
rules = rules.add(File.read(exclude), base: "") if File.file?(exclude)
|
|
15
|
+
rules = IgnoreMatcher.load(@root)
|
|
18
16
|
walk("", rules, {}, &block)
|
|
19
17
|
self
|
|
20
18
|
end
|
|
@@ -26,10 +24,6 @@ module Thuban
|
|
|
26
24
|
return if visited[[stat.dev, stat.ino]]
|
|
27
25
|
|
|
28
26
|
visited[[stat.dev, stat.ino]] = true
|
|
29
|
-
%w[.gitignore .ignore].each do |name|
|
|
30
|
-
source = path(directory.empty? ? name : File.join(directory, name))
|
|
31
|
-
rules = rules.add(File.read(source, encoding: "UTF-8"), base: directory) if File.file?(source)
|
|
32
|
-
end
|
|
33
27
|
Dir.children(path(directory)).sort.each do |name|
|
|
34
28
|
next if name == ".git"
|
|
35
29
|
|
data/lib/thuban.rb
CHANGED
|
@@ -11,8 +11,17 @@ require_relative "thuban/ignore_matcher"
|
|
|
11
11
|
require_relative "thuban/worktree_files"
|
|
12
12
|
require_relative "thuban/object_database"
|
|
13
13
|
require_relative "thuban/index"
|
|
14
|
+
require_relative "thuban/index_writer"
|
|
14
15
|
require_relative "thuban/status"
|
|
15
16
|
require_relative "thuban/blame"
|
|
16
17
|
require_relative "thuban/commit"
|
|
17
18
|
require_relative "thuban/tree_entry"
|
|
19
|
+
require_relative "thuban/signature"
|
|
18
20
|
require_relative "thuban/repository"
|
|
21
|
+
require_relative "thuban/object_writer"
|
|
22
|
+
require_relative "thuban/ref_lock_error"
|
|
23
|
+
require_relative "thuban/ref_store"
|
|
24
|
+
require_relative "thuban/commit_writer"
|
|
25
|
+
require_relative "thuban/history_writer"
|
|
26
|
+
require_relative "thuban/stash"
|
|
27
|
+
require_relative "thuban/pack_writer"
|
data/sig/thuban.rbs
CHANGED
|
@@ -6,9 +6,16 @@ module Thuban
|
|
|
6
6
|
def edits: (Porrima::lines, Porrima::lines) -> Array[Porrima::Edit]
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
interface _PackOutput
|
|
10
|
+
def write: (String) -> Integer
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
class CorruptObject < StandardError
|
|
10
14
|
end
|
|
11
15
|
|
|
16
|
+
class RefLockError < StandardError
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
class Commit < Struct[untyped]
|
|
13
20
|
attr_accessor oid: String
|
|
14
21
|
attr_accessor tree: String?
|
|
@@ -26,6 +33,14 @@ module Thuban
|
|
|
26
33
|
def self.new: (path: String, oid: String, mode: Integer) -> instance
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
class Signature < Struct[untyped]
|
|
37
|
+
attr_accessor name: String
|
|
38
|
+
attr_accessor email: String
|
|
39
|
+
attr_accessor time: Time | Integer | nil
|
|
40
|
+
attr_accessor offset: String | Integer | nil
|
|
41
|
+
def self.new: (name: String, email: String, ?time: Time | Integer, ?offset: String | Integer?) -> instance
|
|
42
|
+
end
|
|
43
|
+
|
|
29
44
|
class IgnoreMatcher
|
|
30
45
|
class Rule < Struct[untyped]
|
|
31
46
|
attr_accessor base: String
|
|
@@ -63,6 +78,24 @@ module Thuban
|
|
|
63
78
|
def status: () -> Array[Status::Entry]
|
|
64
79
|
def blame: (String path, ?reference: String, ?differ: _Differ) -> Array[Blame::Line]
|
|
65
80
|
def checkout: (String name) -> String
|
|
81
|
+
def write_blob: (String content) -> String
|
|
82
|
+
def write_tree: (Array[TreeEntry] entries) -> String
|
|
83
|
+
def write_commit: (tree: String, ?parents: Array[String], author: Signature, ?committer: Signature?, message: String) -> String
|
|
84
|
+
def update_ref: (String name, String new_oid, ?old_oid: String | Symbol | nil, ?message: String?) -> String
|
|
85
|
+
def delete_ref: (String name, ?old_oid: String | Symbol | nil) -> String
|
|
86
|
+
def create_branch: (String name, String oid) -> String
|
|
87
|
+
def delete_branch: (String name) -> String
|
|
88
|
+
def symbolic_ref: (String name, String target) -> String
|
|
89
|
+
def reflog: (String name) -> Array[String]
|
|
90
|
+
def write_tree_from_index: () -> String
|
|
91
|
+
def commit!: (message: String, author: Signature, ?amend: bool) -> String
|
|
92
|
+
def merge_base: (String a, String b) -> String?
|
|
93
|
+
def reset: (String oid, ?mode: :soft | :mixed | :hard) -> String
|
|
94
|
+
def cherry_pick: (String oid) -> String
|
|
95
|
+
def revert: (String oid) -> String
|
|
96
|
+
def stash_push: (?message: String?, ?include_untracked: bool) -> String?
|
|
97
|
+
def stash_pop: (?Integer index) -> String
|
|
98
|
+
def stash_list: () -> Array[Commit]
|
|
66
99
|
end
|
|
67
100
|
|
|
68
101
|
class ObjectDatabase
|
|
@@ -71,6 +104,18 @@ module Thuban
|
|
|
71
104
|
def self.hash: (String type, String data) -> String
|
|
72
105
|
def read: (String oid, ?Array[String] seen) -> object
|
|
73
106
|
def packs: () -> Array[Pack]
|
|
107
|
+
def exist?: (String oid) -> bool
|
|
108
|
+
def write: (String type, String data) -> String
|
|
109
|
+
def write_loose: (String type, String data) -> String
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class RefStore
|
|
113
|
+
ZERO_OID: String
|
|
114
|
+
def initialize: (Repository repository) -> void
|
|
115
|
+
def update: (String name, String new_oid, ?old_oid: String | Symbol | nil, ?message: String?) -> String
|
|
116
|
+
def delete: (String name, ?old_oid: String | Symbol | nil) -> String
|
|
117
|
+
def symbolic: (String name, String target) -> String
|
|
118
|
+
def reflog: (String name) -> Array[String]
|
|
74
119
|
end
|
|
75
120
|
|
|
76
121
|
class Pack
|
|
@@ -82,6 +127,7 @@ module Thuban
|
|
|
82
127
|
def include?: (String oid) -> bool
|
|
83
128
|
def read: (String oid) ?{ (String) -> object } -> object
|
|
84
129
|
def self.apply_delta: (String base, String delta) -> String
|
|
130
|
+
def self.write: (_PackOutput io, Enumerable[object] objects) ?{ (Integer current, Integer total) -> void } -> String
|
|
85
131
|
end
|
|
86
132
|
|
|
87
133
|
class Index
|
|
@@ -105,13 +151,20 @@ module Thuban
|
|
|
105
151
|
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
|
|
106
152
|
end
|
|
107
153
|
attr_reader entries: Array[Entry]
|
|
154
|
+
attr_reader extensions: Array[String]
|
|
108
155
|
attr_reader version: Integer
|
|
109
156
|
attr_reader path: String
|
|
110
157
|
def initialize: (String path) -> void
|
|
111
158
|
def each: () -> Enumerator[Entry, Array[Entry]]
|
|
112
159
|
| () { (Entry) -> void } -> Array[Entry]
|
|
113
160
|
def []: (String path) -> Entry?
|
|
114
|
-
def self.encode: (Array[Entry] entries) -> String
|
|
161
|
+
def self.encode: (Array[Entry] entries, ?extensions: Array[String], ?version: Integer) -> String
|
|
162
|
+
def write: () -> Index
|
|
163
|
+
def stage: (String path, String oid, Integer mode, ?stat: File::Stat | Hash[Symbol, untyped]?) -> Entry
|
|
164
|
+
def unstage: (String path) -> Array[Entry]
|
|
165
|
+
def remove: (String path) -> Array[Entry]
|
|
166
|
+
def conflicts: () -> Array[Entry]
|
|
167
|
+
def resolve: (String path, String oid, Integer mode) -> Entry
|
|
115
168
|
end
|
|
116
169
|
|
|
117
170
|
class Status
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: thuban
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -35,12 +35,21 @@ files:
|
|
|
35
35
|
- lib/thuban.rb
|
|
36
36
|
- lib/thuban/blame.rb
|
|
37
37
|
- lib/thuban/commit.rb
|
|
38
|
+
- lib/thuban/commit_writer.rb
|
|
38
39
|
- lib/thuban/corrupt_object.rb
|
|
40
|
+
- lib/thuban/history_writer.rb
|
|
39
41
|
- lib/thuban/ignore_matcher.rb
|
|
40
42
|
- lib/thuban/index.rb
|
|
43
|
+
- lib/thuban/index_writer.rb
|
|
41
44
|
- lib/thuban/object_database.rb
|
|
45
|
+
- lib/thuban/object_writer.rb
|
|
42
46
|
- lib/thuban/pack.rb
|
|
47
|
+
- lib/thuban/pack_writer.rb
|
|
48
|
+
- lib/thuban/ref_lock_error.rb
|
|
49
|
+
- lib/thuban/ref_store.rb
|
|
43
50
|
- lib/thuban/repository.rb
|
|
51
|
+
- lib/thuban/signature.rb
|
|
52
|
+
- lib/thuban/stash.rb
|
|
44
53
|
- lib/thuban/status.rb
|
|
45
54
|
- lib/thuban/tree_entry.rb
|
|
46
55
|
- lib/thuban/version.rb
|
|
@@ -70,5 +79,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
79
|
requirements: []
|
|
71
80
|
rubygems_version: 3.6.9
|
|
72
81
|
specification_version: 4
|
|
73
|
-
summary: A pure Ruby Git
|
|
82
|
+
summary: A pure Ruby implementation for local Git repositories
|
|
74
83
|
test_files: []
|