au 0.1.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 +7 -0
- data/.gitignore +209 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/Rakefile +6 -0
- data/au.gemspec +40 -0
- data/bin/au +170 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/au.rb +14 -0
- data/lib/au/misc/load_pstore.rb +13 -0
- data/lib/au/models/commit.rb +259 -0
- data/lib/au/models/diff.rb +69 -0
- data/lib/au/models/document.rb +104 -0
- data/lib/au/models/repository.rb +254 -0
- data/lib/au/models/staging.rb +91 -0
- data/lib/au/patches/object.rb +12 -0
- data/lib/au/version.rb +3 -0
- metadata +155 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
module Au
|
4
|
+
class Diff
|
5
|
+
|
6
|
+
def self.create(db_name, parent_id, change, md5)
|
7
|
+
db = get_db(db_name)
|
8
|
+
id = Time.now.to_f
|
9
|
+
db.transaction do
|
10
|
+
db[id] = [parent_id, change, md5]
|
11
|
+
end
|
12
|
+
id
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.replay_changes_upto(id, db_name)
|
16
|
+
raise 'Missing block' unless block_given?
|
17
|
+
diffs = [find(id, db_name: db_name)]
|
18
|
+
while diffs.last.has_parent?
|
19
|
+
diffs << diffs.last.parent
|
20
|
+
end
|
21
|
+
# apply the oldest first
|
22
|
+
diffs.reverse.each do |diff|
|
23
|
+
yield(diff)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.find(id, db_name: nil, db: nil)
|
28
|
+
raise 'Missing both db name and db' if db_name.nil? && db.nil?
|
29
|
+
db ||= get_db(db_name)
|
30
|
+
stored_values = db.transaction{ db[id] }
|
31
|
+
|
32
|
+
raise 'Diff not found' unless stored_values
|
33
|
+
parent_id, change, md5 = stored_values
|
34
|
+
new(id, parent_id, change, md5, db)
|
35
|
+
end
|
36
|
+
|
37
|
+
# memoize all encountered dbs
|
38
|
+
# Note, may need to watch memory usage.
|
39
|
+
# Note, depending on PStore initialization, may not need this
|
40
|
+
def self.get_db(db_name)
|
41
|
+
if @dbs
|
42
|
+
@dbs[db_name] ||= PStore.new(db_name)
|
43
|
+
else
|
44
|
+
@dbs = {}
|
45
|
+
get_db(db_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :id, :parent_id, :change, :md5, :db
|
50
|
+
|
51
|
+
def initialize(id, parent_id, change, md5, db)
|
52
|
+
@id = id
|
53
|
+
@parent_id = parent_id
|
54
|
+
@change = change
|
55
|
+
@md5 = md5
|
56
|
+
@db = db
|
57
|
+
end
|
58
|
+
|
59
|
+
def parent
|
60
|
+
return unless parent_id
|
61
|
+
self.class.find(parent_id, db: db)
|
62
|
+
end
|
63
|
+
|
64
|
+
def has_parent?
|
65
|
+
!!parent_id
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'fileutils'
|
5
|
+
require_relative '../models/diff'
|
6
|
+
|
7
|
+
module Au
|
8
|
+
class Document
|
9
|
+
class HasMergeConflict < StandardError
|
10
|
+
attr_reader :document
|
11
|
+
def initialize(document)
|
12
|
+
@document = document
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find(paths)
|
18
|
+
paths.is_a?(Array) ? paths.map{ |path| new(path) } : new(paths)
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :path
|
22
|
+
|
23
|
+
# Do NOT invoke this method directly, always use
|
24
|
+
# Document.find instead.
|
25
|
+
def initialize(path)
|
26
|
+
@path = path
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_diff(parent_diff_id)
|
30
|
+
return nil unless File.exists?(abs_path)
|
31
|
+
changes = diff(parent_diff_id)
|
32
|
+
# if not first diff AND there's no change, don't create new diff
|
33
|
+
return parent_diff_id if changes.empty? && parent_diff_id
|
34
|
+
Diff.create(diff_db_name, parent_diff_id, changes, calculate_md5)
|
35
|
+
end
|
36
|
+
|
37
|
+
def diff_3(ancestor_diff_id:, other_diff_id:)
|
38
|
+
file_in_ancestor_state = content_from(ancestor_diff_id)
|
39
|
+
file_in_other_state = content_from(other_diff_id)
|
40
|
+
diff3_args = "#{abs_path} #{file_in_ancestor_state.path} #{file_in_other_state.path}"
|
41
|
+
|
42
|
+
has_conflict = !`diff3 -x #{diff3_args}`.empty?
|
43
|
+
merged_content = `diff3 --merge #{diff3_args}`
|
44
|
+
File.open(abs_path, 'w'){ |f| f.write(merged_content) }
|
45
|
+
|
46
|
+
raise HasMergeConflict.new(self) if has_conflict
|
47
|
+
end
|
48
|
+
|
49
|
+
def changed?(parent_diff_id)
|
50
|
+
!diff(parent_diff_id).empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def diff(parent_diff_id)
|
54
|
+
`diff -c #{abs_path} #{content_from(parent_diff_id).path}`
|
55
|
+
end
|
56
|
+
|
57
|
+
def content_from(diff_id)
|
58
|
+
output = Tempfile.new
|
59
|
+
return output unless diff_id
|
60
|
+
apply_diffs(output, diff_id)
|
61
|
+
output
|
62
|
+
end
|
63
|
+
|
64
|
+
def checkout(diff_id)
|
65
|
+
# do nothing if file didn't change
|
66
|
+
return if File.exists?(abs_path) && calculate_md5 == md5_at(diff_id)
|
67
|
+
FileUtils.mkdir_p(File.dirname(abs_path)) unless File.exists?(abs_path)
|
68
|
+
|
69
|
+
File.open(abs_path, 'w') do |f|
|
70
|
+
apply_diffs(f, diff_id)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def md5_at(diff_id)
|
75
|
+
Diff.find(diff_id, db_name: diff_db_name).md5
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def calculate_md5
|
81
|
+
Digest::MD5.hexdigest(File.read(abs_path))
|
82
|
+
end
|
83
|
+
|
84
|
+
def apply_diffs(output, last_diff_id)
|
85
|
+
Diff.replay_changes_upto(last_diff_id, diff_db_name) do |diff|
|
86
|
+
Tempfile.create do |f|
|
87
|
+
f.write(diff.change)
|
88
|
+
f.rewind
|
89
|
+
`patch -cR #{output.path} #{f.path}`
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def diff_db_name
|
95
|
+
paths = [Repository.path, "diff_#{path.gsub('/', '.')}.pstore"].compact
|
96
|
+
File.join(*paths)
|
97
|
+
end
|
98
|
+
|
99
|
+
def abs_path
|
100
|
+
File.join(Repository.root, path)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
module Au
|
4
|
+
class Repository
|
5
|
+
# singleton class
|
6
|
+
attr_reader :root, :path, :current, :head
|
7
|
+
|
8
|
+
def self.root(path = nil)
|
9
|
+
instance(path, false).root
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.path(path = nil)
|
13
|
+
instance(path, false).path
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.head(path = nil)
|
17
|
+
Commit.find instance(path, false).head
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.diff(file_path)
|
21
|
+
return nil unless head
|
22
|
+
tracked_version = head.cat(file_path)
|
23
|
+
return nil unless tracked_version
|
24
|
+
`diff -c #{tracked_version.path} #{File.join(root, file_path)}`
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.instance(path = nil, create = false)
|
28
|
+
return @single_instance unless @single_instance.nil?
|
29
|
+
@single_instance = new(path, create)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(path, create = false, remote_path = nil)
|
33
|
+
if path.nil?
|
34
|
+
working_dir = Dir.pwd
|
35
|
+
# little bit different with mercurial 0.1
|
36
|
+
# here we only check whether the current working directory is a repository
|
37
|
+
if not File.directory?(File.join(working_dir, '.au')) and not create
|
38
|
+
raise 'Repository not found in ' + working_dir
|
39
|
+
end
|
40
|
+
path = working_dir
|
41
|
+
end
|
42
|
+
|
43
|
+
@root = path
|
44
|
+
@path = File.join(path, '.au')
|
45
|
+
|
46
|
+
Dir.mkdir(@path) if create && !File.directory?(@path)
|
47
|
+
|
48
|
+
@head_pstore = PStore.new(File.join(@path, 'head.pstore'))
|
49
|
+
|
50
|
+
update_head_var
|
51
|
+
|
52
|
+
@alt_pstore = PStore.new(File.join(@path, 'alt.pstore'))
|
53
|
+
|
54
|
+
update_alt_list
|
55
|
+
|
56
|
+
@leaves_pstore = PStore.new(File.join(@path, 'leaves.pstore'))
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge(commit_to_merge)
|
60
|
+
commit_id = Commit.merge(@head, commit_to_merge)
|
61
|
+
return false if commit_id.nil?
|
62
|
+
# checkout to new commit
|
63
|
+
checkout(commit_id)
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def commit(commit_message)
|
68
|
+
staged_file_paths = Staging.staged_file_paths
|
69
|
+
return if staged_file_paths.empty?
|
70
|
+
commit_created_id =
|
71
|
+
if File.exist?(Repository.resolve_conflict_fname)
|
72
|
+
parent_id1, parent_id2 = File.read(Repository.resolve_conflict_fname).split(',')
|
73
|
+
File.delete(Repository.resolve_conflict_fname)
|
74
|
+
Commit.create_commit_resolve_conflict(staged_file_paths, parent_id1, parent_id2)
|
75
|
+
else
|
76
|
+
Commit.create(staged_file_paths, commit_message, @head)
|
77
|
+
end
|
78
|
+
update_head_pstore(commit_created_id)
|
79
|
+
update_leaves_pstore(commit_created_id)
|
80
|
+
Staging.clear_staged_file_paths
|
81
|
+
commit_created_id
|
82
|
+
end
|
83
|
+
|
84
|
+
def checkout(commit_id)
|
85
|
+
this_commit = Commit.find(commit_id)
|
86
|
+
if this_commit
|
87
|
+
this_commit.checkout(update_head_var)
|
88
|
+
update_alt_pstore(update_head_var)
|
89
|
+
update_head_pstore(commit_id)
|
90
|
+
update_head_var
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def pull(remote_repo)
|
95
|
+
# Initialize local and remote db and commit list
|
96
|
+
remote_commit_db = Commit.db(remote_repo.root)
|
97
|
+
remote_commit_list = remote_commit_db.transaction do
|
98
|
+
remote_commit_db.roots
|
99
|
+
end
|
100
|
+
local_commit_db = Commit.db
|
101
|
+
local_commit_list = local_commit_db.transaction do
|
102
|
+
local_commit_db.roots
|
103
|
+
end
|
104
|
+
# Check if the root commits are the same
|
105
|
+
if local_commit_list[0] != remote_commit_list[0]
|
106
|
+
raise "Error: Root commits do not match."
|
107
|
+
end
|
108
|
+
|
109
|
+
# Find commits that only exist locally
|
110
|
+
commit_id_to_add_list = remote_commit_list.reject{|x| local_commit_list.include?(x)}
|
111
|
+
commits_to_add = {}
|
112
|
+
remote_commit_db.transaction(true) do
|
113
|
+
commit_id_to_add_list.each{|commit_id| commits_to_add[commit_id] = remote_commit_db[commit_id]}
|
114
|
+
end
|
115
|
+
|
116
|
+
# Write those commits to remote commit db
|
117
|
+
local_commit_db.transaction do
|
118
|
+
commits_to_add.each{|commit_id, data| local_commit_db[commit_id] = data}
|
119
|
+
end
|
120
|
+
|
121
|
+
# Find documents and their diff_id in those commits that only existed locally
|
122
|
+
remote_diff_id_to_add_list = {}
|
123
|
+
commits_to_add.each do |commit|
|
124
|
+
commit[:doc_diff_ids].each do |doc_path, diff_id|
|
125
|
+
remote_diff_id_to_add_list[doc_path] = diff_id
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Write these information to remote
|
130
|
+
remote_diff_id_to_add_list.each do |doc_path, diff_id|
|
131
|
+
diff_pstore_remote = PStore.new(diff_db_name_remote(doc_path, remote_repo.path))
|
132
|
+
diff_pstore_remote.transaction(true) do
|
133
|
+
diff_pstore_local = PStore.new(diff_db_name_remote(doc_path, @path))
|
134
|
+
diff_pstore_local.transaction do
|
135
|
+
if diff_pstore_local[doc_path] == nil
|
136
|
+
diff_pstore_local[doc_path] = diff_pstore_remote[doc_path]
|
137
|
+
elsif diff_pstore_local[doc_path] != diff_pstore_remote[doc_path]
|
138
|
+
raise "Error: Remote and local reposiroty have different content in a same diff"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def push(remote_repo)
|
146
|
+
# Initialize local and remote db and commit list
|
147
|
+
remote_commit_db = Commit.db(remote_repo.root)
|
148
|
+
remote_commit_list = remote_commit_db.transaction do
|
149
|
+
remote_commit_db.roots
|
150
|
+
end
|
151
|
+
local_commit_db = Commit.db
|
152
|
+
local_commit_list = local_commit_db.transaction do
|
153
|
+
local_commit_db.roots
|
154
|
+
end
|
155
|
+
# Check if the root commits are the same
|
156
|
+
if local_commit_list[0] != remote_commit_list[0]
|
157
|
+
raise "Error: Root commits do not match."
|
158
|
+
end
|
159
|
+
|
160
|
+
# Find commits that only exist locally
|
161
|
+
commit_id_to_add_list = local_commit_list.reject{|x| remote_commit_list.include?(x)}
|
162
|
+
commits_to_add = {}
|
163
|
+
remote_commit_db.transaction(true) do
|
164
|
+
commit_id_to_add_list.each{|commit_id| commits_to_add[commit_id] = remote_commit_db[commit_id]}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Write those commits to remote commit db
|
168
|
+
remote_commit_db.transaction do
|
169
|
+
commits_to_add.each{|commit_id, data| remote_commit_db[commit_id] = data}
|
170
|
+
end
|
171
|
+
|
172
|
+
# Find documents and their diff_id in those commits that only existed locally
|
173
|
+
local_diff_id_to_add_list = {}
|
174
|
+
commits_to_add.each do |commit|
|
175
|
+
commit[:doc_diff_ids].each do |doc_path, diff_id|
|
176
|
+
local_diff_id_to_add_list[doc_path] = diff_id
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Write these information to remote
|
181
|
+
local_diff_id_to_add_list.each do |doc_path, diff_id|
|
182
|
+
diff_pstore_local = PStore.new(diff_db_name_remote(doc_path, @path))
|
183
|
+
diff_pstore_local.transaction(true) do
|
184
|
+
diff_pstore_remote = PStore.new(diff_db_name_remote(doc_path, remote_repo.path))
|
185
|
+
diff_pstore_remote.transaction do
|
186
|
+
if diff_pstore_remote[doc_path] == nil
|
187
|
+
diff_pstore_remote[doc_path] = diff_pstore_local[doc_path]
|
188
|
+
elsif diff_pstore_local[doc_path] != diff_pstore_remote[doc_path]
|
189
|
+
raise "Error: Remote and local reposiroty have different content in a same diff"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# def clone
|
197
|
+
#
|
198
|
+
# end
|
199
|
+
|
200
|
+
def heads
|
201
|
+
@leaves_pstore.transaction(true) do
|
202
|
+
@leaves_pstore.roots
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def diff_db_name_remote(file_path, remote_path)
|
209
|
+
paths = [Repository(remote_path, false).path, "diff_#{file_path.gsub('/', '.')}.pstore"].compact
|
210
|
+
File.join(*paths)
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
def update_head_var
|
215
|
+
@head = @head_pstore.transaction(true) do
|
216
|
+
@head_pstore[:head]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def update_head_pstore(commit_id)
|
221
|
+
@head = @head_pstore.transaction do
|
222
|
+
@head_pstore[:head] = commit_id
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def update_alt_list
|
227
|
+
@alt_list = @alt_pstore.transaction(true) do
|
228
|
+
@alt_pstore[:commit]
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def update_alt_pstore(commit_id)
|
233
|
+
@alt_pstore.transaction do
|
234
|
+
@alt_pstore[:commit].nil? ? (@alt_pstore[:commit] = [commit_id]) : (@alt_pstore[:commit] << commit_id)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def update_leaves_pstore(commit_id)
|
239
|
+
@leaves_pstore.transaction do
|
240
|
+
commit = Commit.find(commit_id)
|
241
|
+
if commit
|
242
|
+
parent_commit_id_1, parent_commit_id_2 = commit.parent_id_1, commit.parent_id_2
|
243
|
+
@leaves_pstore.delete(parent_commit_id_1)
|
244
|
+
@leaves_pstore.delete(parent_commit_id_2)
|
245
|
+
@leaves_pstore[commit_id] = 1
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.resolve_conflict_fname
|
251
|
+
File.join(Repository.instance.path, 'resolve_conflict')
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'pstore'
|
3
|
+
|
4
|
+
module Au
|
5
|
+
class Staging
|
6
|
+
|
7
|
+
def self.status(last_commit = Repository.head)
|
8
|
+
return project_file_paths unless last_commit
|
9
|
+
changed_doc_paths = []
|
10
|
+
tracked_docs_md5 = last_commit.tracked_docs_md5
|
11
|
+
project_file_paths.each do |relative_path|
|
12
|
+
known_md5 = tracked_docs_md5[relative_path]
|
13
|
+
# file not tracked or tracked but hasn't changed
|
14
|
+
if known_md5.nil? || Digest::MD5.hexdigest(File.read(abs_path(relative_path))) != known_md5
|
15
|
+
changed_doc_paths << relative_path
|
16
|
+
end
|
17
|
+
tracked_docs_md5.delete(relative_path) if known_md5
|
18
|
+
end
|
19
|
+
|
20
|
+
# remaining files in tracked_docs_md5 are no longer in the project
|
21
|
+
changed_doc_paths += tracked_docs_md5.keys unless tracked_docs_md5.empty?
|
22
|
+
changed_doc_paths
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.stage(relative_paths, last_commit = Repository.head)
|
26
|
+
relative_paths = relative_paths.each_with_object([]) do |staged_path, accum|
|
27
|
+
if File.directory?(staged_path)
|
28
|
+
Dir.glob('**/*', base: staged_path) do |sub_path|
|
29
|
+
accum << (File.join(staged_path, sub_path)) unless File.directory?(sub_path)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
accum << staged_path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
stage_db.transaction do
|
37
|
+
filter_stageable(last_commit, relative_paths).each do |relative_path|
|
38
|
+
stage_db[relative_path] = 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
relative_paths
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.filter_stageable(last_commit, relative_paths)
|
45
|
+
tracked_docs_md5 = last_commit&.tracked_docs_md5 || {}
|
46
|
+
relative_paths.reject do |relative_path|
|
47
|
+
known_md5 = tracked_docs_md5[relative_path]
|
48
|
+
abs_path = File.join(Repository.root, relative_path)
|
49
|
+
if File.exists?(abs_path)
|
50
|
+
known_md5 && Digest::MD5.hexdigest(File.read(abs_path)) == known_md5
|
51
|
+
else
|
52
|
+
known_md5.nil?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.staged_file_paths
|
58
|
+
stage_db.transaction(true) do
|
59
|
+
stage_db.roots
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.clear_staged_file_paths
|
64
|
+
File.delete stage_db.path
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.unstage(file_paths)
|
68
|
+
stage_db.transaction do
|
69
|
+
file_paths.each do |file_path|
|
70
|
+
stage_db.delete(file_path)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.abs_path(relative_path)
|
76
|
+
File.join(Repository.root, relative_path)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.project_file_paths
|
80
|
+
Dir.glob('**/*', base: Repository.root).reject do |file_path|
|
81
|
+
File.directory? file_path
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# private
|
86
|
+
def self.stage_db
|
87
|
+
@stage_db ||= PStore.new(File.join(*[Repository.path, 'stage.pstore'].compact))
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|