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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Thuban
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -12,9 +12,7 @@ module Thuban
12
12
  def each(&block)
13
13
  return enum_for(__method__) unless block
14
14
 
15
- rules = IgnoreMatcher.new
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.2.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 repository reader
82
+ summary: A pure Ruby implementation for local Git repositories
74
83
  test_files: []