stg 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/README.md +27 -0
- data/bin/stg +7 -0
- data/lib/actions.rb +349 -0
- data/lib/differencing.rb +219 -0
- data/lib/help.rb +21 -0
- data/lib/stg.rb +47 -0
- data/lib/utils.rb +63 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e9c7c1bdc57ae80ecb2f59b5104e4c2e2be91521044a8921e0a801879843543d
|
|
4
|
+
data.tar.gz: ea29aa8292c71cfc198232b977eca56a971698e2de7efed838ea78427c92ce79
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f57259e0e823d045aedef0004c892c9009f8ae5bdd6ad5820500402f0663e36b24125a1815fa0e6d2d3f59efc8db4540c5582ff0ebcb8ef57c425e4a3c4c2681
|
|
7
|
+
data.tar.gz: e2368f884e3fad925e0bd9d0716f663189a9cbd8e772f7a32b6ad567e3451dd9ec82052fb29c30e21caffec5f9ba22103f56bdc666a405ba928f701af92bb349
|
data/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Stolen Git</h1>
|
|
3
|
+
<span>
|
|
4
|
+
Have you ever wanted to use Git but it's too good?
|
|
5
|
+
</span>
|
|
6
|
+
</div>
|
|
7
|
+
<br>
|
|
8
|
+
|
|
9
|
+
## What is stolen git? \ Why?
|
|
10
|
+
|
|
11
|
+
Stolen git is well... you get it. I'm building a mini git clone to learn version control and get comfortable with ruby.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
| Command | description | Flags |
|
|
16
|
+
| -------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| init | Initialize the project | N/A |
|
|
18
|
+
| stage | add a file or directory to be tracked | N/A |
|
|
19
|
+
| commit | Save the current tracked state | `-n [--name]` <br> `-d [--description]` |
|
|
20
|
+
| diff | get the difference between working directory and the last commit | N/A |
|
|
21
|
+
| log | print out commit history (limit print by a number `log <number>`) | N/A |
|
|
22
|
+
| checkout | check a commit or a branch without loss in data | `-c [--commit] <commit_id>` to check out a commit instaed of a branch wihout changing HEAD pointer |
|
|
23
|
+
| branch | List all branches. (or creating a branch by `branch <name>`) | N/A |
|
|
24
|
+
|
|
25
|
+
## Tech stack
|
|
26
|
+
|
|
27
|
+
Just ruby... (for now at least)
|
data/bin/stg
ADDED
data/lib/actions.rb
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require 'optparse'
|
|
3
|
+
require_relative 'differencing'
|
|
4
|
+
require_relative 'utils'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'colorize'
|
|
7
|
+
require 'json'
|
|
8
|
+
module Actions
|
|
9
|
+
include DiffCalc
|
|
10
|
+
|
|
11
|
+
def p_initialize
|
|
12
|
+
# Check if already initialized
|
|
13
|
+
if File.exist?('.stolen-git')
|
|
14
|
+
if confirm?('An instance of stolen-git is already up here do you want to replace it')
|
|
15
|
+
if confirm?('THIS WILL DELETE ALL COMMITS AND INSTANCES OF stolen-git. ARE YOU SURE')
|
|
16
|
+
FileUtils.rm_rf('.stolen-git')
|
|
17
|
+
|
|
18
|
+
if File.exist?('.stolen-git')
|
|
19
|
+
puts 'An error occured during the deletion process of the old directory of stolen-git'
|
|
20
|
+
else
|
|
21
|
+
p_initialize
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
puts 'ok'
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
puts 'ok'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
else
|
|
31
|
+
# main dot directory
|
|
32
|
+
FileUtils.mkdir_p('.stolen-git')
|
|
33
|
+
|
|
34
|
+
# Sub Directories
|
|
35
|
+
FileUtils.mkdir_p('.stolen-git/commits')
|
|
36
|
+
FileUtils.mkdir_p('.stolen-git/branches')
|
|
37
|
+
|
|
38
|
+
FileUtils.mkdir_p('.stolen-git/storage')
|
|
39
|
+
FileUtils.mkdir_p('.stolen-git/storage/blobs')
|
|
40
|
+
FileUtils.mkdir_p('.stolen-git/storage/trees')
|
|
41
|
+
|
|
42
|
+
# main files
|
|
43
|
+
File.write('.stolen-git/project_info.json', {})
|
|
44
|
+
File.write('.stolen-git/commits.json', JSON.pretty_generate({ commits: [] }))
|
|
45
|
+
File.write('.stolen-git/index.json', {})
|
|
46
|
+
|
|
47
|
+
main_branch_id = SecureRandom.uuid
|
|
48
|
+
File.write(".stolen-git/branches/#{main_branch_id}.json",
|
|
49
|
+
JSON.pretty_generate({ name: 'main', created_at: Time.now, commit_pointer: '' }))
|
|
50
|
+
|
|
51
|
+
File.write('.stolen-git/pointer.json', JSON.pretty_generate({ point_to: main_branch_id, type: 'branch' }))
|
|
52
|
+
puts 'Stolen-git initialized Sucessfully :D'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stage
|
|
57
|
+
files = ARGV
|
|
58
|
+
if files.empty?
|
|
59
|
+
puts 'Usage: stolen-git stage <files..>'
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
index = JSON.parse(File.read('.stolen-git/index.json'))
|
|
64
|
+
files.each do |file_path|
|
|
65
|
+
file_hash = get_file_hash(file_path)
|
|
66
|
+
file_content = File.read(file_path)
|
|
67
|
+
|
|
68
|
+
next if index[file_path] && index[file_path]['hash'] == file_hash
|
|
69
|
+
|
|
70
|
+
# Create blob
|
|
71
|
+
File.write(".stolen-git/storage/blobs/#{file_hash}", file_content)
|
|
72
|
+
|
|
73
|
+
# Assign index_obj
|
|
74
|
+
default_index_obj = {}
|
|
75
|
+
index[file_path] ||= default_index_obj
|
|
76
|
+
index[file_path]['hash'] = file_hash
|
|
77
|
+
end
|
|
78
|
+
index = index.sort.to_h
|
|
79
|
+
File.write('.stolen-git/index.json', JSON.pretty_generate(index))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def commit
|
|
83
|
+
options = { name: '', description: '' }
|
|
84
|
+
|
|
85
|
+
# Getting commit name & description
|
|
86
|
+
OptionParser.new do |opts|
|
|
87
|
+
opts.banner = 'Usage: stolen-git commit [options]'
|
|
88
|
+
|
|
89
|
+
opts.on('-n', '--name NAME', 'Add a commit name') do |name|
|
|
90
|
+
options[:name] = name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
opts.on('-d', '--description DESCRIPTION', 'Add a commit description') do |description|
|
|
94
|
+
options[:description] = description
|
|
95
|
+
end
|
|
96
|
+
end.parse!
|
|
97
|
+
|
|
98
|
+
options[:name] = ask('Add a commit name: ') if options[:name].empty?
|
|
99
|
+
|
|
100
|
+
# Get the index
|
|
101
|
+
index = JSON.parse(File.read('.stolen-git/index.json'))
|
|
102
|
+
tree_content = {
|
|
103
|
+
entries: []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
commit_history = JSON.parse(File.read('.stolen-git/commits.json'))
|
|
107
|
+
|
|
108
|
+
pointer = read_json('.stolen-git/pointer.json')
|
|
109
|
+
branch_id = pointer['point_to']
|
|
110
|
+
branch_content = read_json(".stolen-git/branches/#{branch_id}.json")
|
|
111
|
+
parent_commit = branch_content['commit_pointer']
|
|
112
|
+
|
|
113
|
+
# Getting differences to last commit
|
|
114
|
+
|
|
115
|
+
no_insertions = 0
|
|
116
|
+
no_deletions = 0
|
|
117
|
+
no_file_changed = 0
|
|
118
|
+
commit_diff = {}
|
|
119
|
+
|
|
120
|
+
getting_diff = lambda do |entries|
|
|
121
|
+
parent_index = 0
|
|
122
|
+
|
|
123
|
+
index.each do |key, value|
|
|
124
|
+
if entries && entries[parent_index] && key == entries[parent_index]['path']
|
|
125
|
+
new_hash = value['hash']
|
|
126
|
+
old_hash = entries[parent_index]['hash']
|
|
127
|
+
parent_index += 1
|
|
128
|
+
next if value['hash'] == new_hash
|
|
129
|
+
|
|
130
|
+
entry_content = File.read(".stolen-git/storage/blobs/#{old_hash}").lines.to_a
|
|
131
|
+
new_entry_content = File.read(".stolen-git/storage/blobs/#{new_hash}").lines.to_a
|
|
132
|
+
compute_diff(entry_content, new_entry_content)
|
|
133
|
+
diff = build_sequences
|
|
134
|
+
no_insertions += diff[:insertions]
|
|
135
|
+
no_deletions += diff[:deletions]
|
|
136
|
+
no_file_changed += 1
|
|
137
|
+
commit_diff[key] = diff
|
|
138
|
+
else
|
|
139
|
+
new_entry_content = File.read(".stolen-git/storage/blobs/#{value['hash']}").lines.to_a
|
|
140
|
+
compute_diff('', new_entry_content)
|
|
141
|
+
diff = build_sequences
|
|
142
|
+
|
|
143
|
+
no_insertions += diff[:insertions]
|
|
144
|
+
no_deletions += diff[:deletions]
|
|
145
|
+
no_file_changed += 1
|
|
146
|
+
commit_diff[key] = diff
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if parent_commit.empty?
|
|
152
|
+
getting_diff.call(nil)
|
|
153
|
+
else
|
|
154
|
+
parent_commit_content = read_json(".stolen-git/commits/#{parent_commit}.json")
|
|
155
|
+
parent_tree_hash = parent_commit_content['tree_hash']
|
|
156
|
+
parent_tree_content = JSON.parse(File.read(".stolen-git/storage/trees/#{parent_tree_hash}.json"))
|
|
157
|
+
getting_diff.call(parent_tree_content['entries'])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if no_file_changed <= 0
|
|
161
|
+
puts 'Everything up to date'
|
|
162
|
+
puts "If you have changed please 'stolen-git stage' them first "
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Making the tree
|
|
167
|
+
index.each do |key, value|
|
|
168
|
+
tree_content[:entries].push({
|
|
169
|
+
path: key,
|
|
170
|
+
type: 'blob',
|
|
171
|
+
hash: value['hash'],
|
|
172
|
+
diff: commit_diff[key] || {}
|
|
173
|
+
})
|
|
174
|
+
end
|
|
175
|
+
tree_content[:entries] = tree_content[:entries].sort { |a, b| a['path'] <=> b['path'] }
|
|
176
|
+
tree_content = JSON.pretty_generate(tree_content)
|
|
177
|
+
tree_hash = get_string_hash(tree_content)
|
|
178
|
+
File.write(".stolen-git/storage/trees/#{tree_hash}.json", tree_content)
|
|
179
|
+
|
|
180
|
+
# Making the commit
|
|
181
|
+
# TODO: Change to actual values when personal profiles are created
|
|
182
|
+
commit_id = SecureRandom.uuid
|
|
183
|
+
commit_content = {
|
|
184
|
+
tree_hash: tree_hash,
|
|
185
|
+
created_at: Time.now,
|
|
186
|
+
id: commit_id,
|
|
187
|
+
parent_commit: parent_commit,
|
|
188
|
+
branch_id: branch_id,
|
|
189
|
+
author_profile: {
|
|
190
|
+
name: 'Amr',
|
|
191
|
+
email: 'amrbassem218@gmail.com',
|
|
192
|
+
username: 'amrbassem218'
|
|
193
|
+
},
|
|
194
|
+
name: options[:name],
|
|
195
|
+
description: options[:description],
|
|
196
|
+
|
|
197
|
+
no_insertions: no_insertions,
|
|
198
|
+
no_deletions: no_deletions,
|
|
199
|
+
no_files_changed: no_file_changed
|
|
200
|
+
}
|
|
201
|
+
commit_content = JSON.pretty_generate(commit_content)
|
|
202
|
+
commit_hash = get_string_hash(commit_content)
|
|
203
|
+
File.write(".stolen-git/commits/#{commit_hash}.json", commit_content)
|
|
204
|
+
|
|
205
|
+
# Adding commit to history
|
|
206
|
+
commit_history['commits'].push({ id: commit_id, hash: commit_hash, name: options[:name] })
|
|
207
|
+
File.write('.stolen-git/commits.json', JSON.pretty_generate(commit_history))
|
|
208
|
+
|
|
209
|
+
# Adding to branch
|
|
210
|
+
branch_content['commit_pointer'] = commit_hash
|
|
211
|
+
File.write(".stolen-git/branches/#{branch_id}.json", JSON.pretty_generate(branch_content))
|
|
212
|
+
|
|
213
|
+
# Print
|
|
214
|
+
puts "#{no_file_changed} files changed, #{no_insertions} insertions(+), #{no_deletions} deletions(-)"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def reset
|
|
218
|
+
commit_id = ARGV.first
|
|
219
|
+
commit_history = read_json('.stolen-git/commits.json')
|
|
220
|
+
pointer = read_json('.stolen-git/pointer.json')
|
|
221
|
+
branch_id = pointer['point_to']
|
|
222
|
+
branch_content = read_json(".stolen-git/branches/#{branch_id}.json")
|
|
223
|
+
current_commit_hash = branch_content['commit_pointer']
|
|
224
|
+
parent_commit_hash = if !commit_id || commit_id.empty?
|
|
225
|
+
read_json(".stolen-git/commits/#{current_commit_hash}.json")['parent_commit']
|
|
226
|
+
else
|
|
227
|
+
commit_history['commits'].find { |x| x['id'] == commit_id }['hash']
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if parent_commit_hash.empty?
|
|
231
|
+
puts "The current commit is the earliest in the project. Can't reset behind that."
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
revert_to_commit(parent_commit_hash)
|
|
235
|
+
branch_content['commit_pointer'] = parent_commit_hash
|
|
236
|
+
File.write(".stolen-git/branches/#{branch_id}.json", JSON.pretty_generate(branch_content))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def checkout
|
|
240
|
+
options = { commit: false }
|
|
241
|
+
|
|
242
|
+
# Getting commit name & description
|
|
243
|
+
OptionParser.new do |opts|
|
|
244
|
+
opts.banner = 'Usage: stolen-git checkout [options]'
|
|
245
|
+
|
|
246
|
+
opts.on('-c', '--commit', 'Add a commit id instead') do
|
|
247
|
+
options[:commit] = true
|
|
248
|
+
end
|
|
249
|
+
end.parse!
|
|
250
|
+
inp = ARGV.last
|
|
251
|
+
if !inp
|
|
252
|
+
puts 'Please Enter the name of a branch or commit_id'
|
|
253
|
+
elsif options[:commit]
|
|
254
|
+
commit_history = read_json('.stolen-git/commits.json')
|
|
255
|
+
current_commit_hash = commit_history['commits'].find { |x| x['id'] == inp }['hash']
|
|
256
|
+
revert_to_commit(current_commit_hash)
|
|
257
|
+
else
|
|
258
|
+
# TODO: Handle if user enters branch_id instead
|
|
259
|
+
branch_content = {}
|
|
260
|
+
branch_id = ''
|
|
261
|
+
Dir.children('.stolen-git/branches').each do |entry|
|
|
262
|
+
branch_content = read_json(".stolen-git/branches/#{entry}")
|
|
263
|
+
if branch_content['name'] == inp
|
|
264
|
+
branch_id = File.basename(entry, '.*')
|
|
265
|
+
break
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
if branch_content.empty?
|
|
269
|
+
puts 'There is no branch with that name.'
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
commit_hash = branch_content['commit_pointer']
|
|
273
|
+
revert_to_commit(commit_hash)
|
|
274
|
+
pointer = read_json('.stolen-git/pointer.json')
|
|
275
|
+
pointer['point_to'] = branch_id
|
|
276
|
+
pointer['type'] = 'branch'
|
|
277
|
+
File.write('.stolen-git/pointer.json', JSON.pretty_generate(pointer))
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def branch
|
|
282
|
+
name = ARGV.last
|
|
283
|
+
pointer = read_json('.stolen-git/pointer.json')
|
|
284
|
+
pointed_branch = pointer['type'] == 'branch' ? pointer['point_to'] : ''
|
|
285
|
+
if name
|
|
286
|
+
first_commit = pointer['type'] == 'branch' ? read_json(".stolen-git/branches/#{pointed_branch}.json")['commit_pointer'] : pointer['point_to']
|
|
287
|
+
id = SecureRandom.uuid
|
|
288
|
+
File.write(".stolen-git/branches/#{id}.json", JSON.pretty_generate({
|
|
289
|
+
name: name,
|
|
290
|
+
created_at: Time.now,
|
|
291
|
+
commit_pointer: first_commit
|
|
292
|
+
}))
|
|
293
|
+
return
|
|
294
|
+
puts "branch #{name} created"
|
|
295
|
+
end
|
|
296
|
+
Dir.children('.stolen-git/branches').each do |entry|
|
|
297
|
+
branch_content = read_json(".stolen-git/branches/#{entry}")
|
|
298
|
+
branch_id = File.basename(entry, '.*')
|
|
299
|
+
if pointed_branch == branch_id
|
|
300
|
+
print '* '
|
|
301
|
+
puts "#{branch_content['name']}".green
|
|
302
|
+
else
|
|
303
|
+
puts "#{branch_content['name']}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def diff
|
|
309
|
+
index = read_json('.stolen-git/index.json')
|
|
310
|
+
index.each do |key, value|
|
|
311
|
+
new_file_content = File.exist?(key) ? File.read(key) : nil
|
|
312
|
+
file_name = File.basename(key)
|
|
313
|
+
if new_file_content.nil?
|
|
314
|
+
print "@@#{file_name}".cyan
|
|
315
|
+
print " [DELETED]]\n".red
|
|
316
|
+
else
|
|
317
|
+
new_hash = get_file_hash(key)
|
|
318
|
+
next if new_hash == value['hash']
|
|
319
|
+
|
|
320
|
+
print "@@#{file_name}".cyan
|
|
321
|
+
blob_content = File.read(".stolen-git/storage/blobs/#{value['hash']}")
|
|
322
|
+
print_diff(blob_content, new_file_content)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def log
|
|
328
|
+
limit = ARGV.last
|
|
329
|
+
limit = limit && !limit.empty? ? limit.to_i : 0
|
|
330
|
+
commit_history_content = read_json('.stolen-git/commits.json')
|
|
331
|
+
|
|
332
|
+
commit_history_content['commits'].each_with_index do |commit, i|
|
|
333
|
+
break if limit > 0 && i >= limit
|
|
334
|
+
|
|
335
|
+
if limit == 0 && i >= 5
|
|
336
|
+
q = ask(':')
|
|
337
|
+
break if q == 'q'
|
|
338
|
+
end
|
|
339
|
+
commit_content = read_json(".stolen-git/commits/#{commit['hash']}.json")
|
|
340
|
+
puts
|
|
341
|
+
puts "commit #{commit_content['id']}".green
|
|
342
|
+
puts "Author #{commit_content['author_profile']['username']} <#{commit_content['author_profile']['email']}>"
|
|
343
|
+
puts "Date: #{commit_content['created_at']}"
|
|
344
|
+
puts
|
|
345
|
+
puts " #{commit_content['name']}"
|
|
346
|
+
puts
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
data/lib/differencing.rb
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
require 'colorize'
|
|
2
|
+
module DiffCalc
|
|
3
|
+
attr_reader :mem, :cnt, :old_s, :new_s
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@mem = {}
|
|
7
|
+
@cnt = 0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def compute_diff(old_arr, new_arr)
|
|
11
|
+
@mem = {}
|
|
12
|
+
@old_s = old_arr.is_a?(String) ? old_arr.split("\n").to_a : old_arr
|
|
13
|
+
@new_s = new_arr.is_a?(String) ? new_arr.split("\n").to_a : new_arr
|
|
14
|
+
differencing(0, 0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def differencing(i1 = 0, i2 = 0)
|
|
18
|
+
cached = @mem[[i1, i2]]
|
|
19
|
+
return cached if cached
|
|
20
|
+
|
|
21
|
+
if i1 >= @old_s.length && i2 >= @new_s.length
|
|
22
|
+
result = { diff_cnt: 0, insertions: 0, deletions: 0, action: :done }
|
|
23
|
+
@mem[[i1, i2]] = result
|
|
24
|
+
return result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if i1 >= @old_s.length
|
|
28
|
+
result = { diff_cnt: @new_s.length - i2, insertions: @new_s.length - i2, deletions: 0, action: :insert }
|
|
29
|
+
@mem[[i1, i2]] = result
|
|
30
|
+
differencing(i1, i2 + 1) if i2 < @new_s.length
|
|
31
|
+
return result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if i2 >= @new_s.length
|
|
35
|
+
result = { diff_cnt: @old_s.length - i1, insertions: 0, deletions: @old_s.length - i1, action: :delete }
|
|
36
|
+
@mem[[i1, i2]] = result
|
|
37
|
+
differencing(i1 + 1, i2) if i1 < @old_s.length
|
|
38
|
+
return result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if @old_s[i1] == @new_s[i2]
|
|
42
|
+
result = differencing(i1 + 1, i2 + 1)
|
|
43
|
+
result = { diff_cnt: result[:diff_cnt], insertions: result[:insertions], deletions: result[:deletions],
|
|
44
|
+
action: :keep }
|
|
45
|
+
@mem[[i1, i2]] = result
|
|
46
|
+
return result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
del = differencing(i1 + 1, i2)
|
|
50
|
+
add = differencing(i1, i2 + 1)
|
|
51
|
+
|
|
52
|
+
del_cost = del[:diff_cnt] + 1
|
|
53
|
+
add_cost = add[:diff_cnt] + 1
|
|
54
|
+
|
|
55
|
+
result = if del_cost <= add_cost
|
|
56
|
+
{ diff_cnt: del_cost, insertions: del[:insertions], deletions: del[:deletions] + 1, action: :delete }
|
|
57
|
+
else
|
|
58
|
+
{ diff_cnt: add_cost, insertions: add[:insertions] + 1, deletions: add[:deletions], action: :insert }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@mem[[i1, i2]] = result
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_sequences
|
|
66
|
+
i1 = 0
|
|
67
|
+
i2 = 0
|
|
68
|
+
insertion_seq = {}
|
|
69
|
+
deletion_seq = {}
|
|
70
|
+
|
|
71
|
+
while i1 < @old_s.length || i2 < @new_s.length
|
|
72
|
+
action = @mem[[i1, i2]][:action]
|
|
73
|
+
|
|
74
|
+
case action
|
|
75
|
+
when :done
|
|
76
|
+
break
|
|
77
|
+
when :keep
|
|
78
|
+
i1 += 1
|
|
79
|
+
i2 += 1
|
|
80
|
+
when :delete
|
|
81
|
+
deletion_seq[i2] ||= []
|
|
82
|
+
deletion_seq[i2].push({ old_index: i1, ma_type: 'bs' })
|
|
83
|
+
i1 += 1
|
|
84
|
+
when :insert
|
|
85
|
+
insertion_seq[i2] = { value: @new_s[i2], old_index: i1 }
|
|
86
|
+
i2 += 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
result = @mem[[0, 0]]
|
|
91
|
+
{
|
|
92
|
+
insertion_seq: insertion_seq,
|
|
93
|
+
deletion_seq: deletion_seq,
|
|
94
|
+
insertions: result[:insertions],
|
|
95
|
+
deletions: result[:deletions],
|
|
96
|
+
diff_cnt: result[:diff_cnt]
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def print_diff(file_a, file_b)
|
|
101
|
+
compute_diff(file_a, file_b)
|
|
102
|
+
diff = build_sequences
|
|
103
|
+
insertions, deletions, insertion_seq, deletion_seq = diff.values_at(:insertions, :deletions, :insertion_seq,
|
|
104
|
+
:deletion_seq)
|
|
105
|
+
# printing diff
|
|
106
|
+
print " (+): #{insertions}".green
|
|
107
|
+
print " (-): #{deletions}\n".red
|
|
108
|
+
|
|
109
|
+
# sorting by keys
|
|
110
|
+
insertion_seq = insertion_seq.sort.to_h
|
|
111
|
+
insertion_seq_keys = insertion_seq.keys
|
|
112
|
+
|
|
113
|
+
deletion_seq = deletion_seq.sort.to_h
|
|
114
|
+
deletion_seq_keys = deletion_seq.keys
|
|
115
|
+
|
|
116
|
+
# index (old or new)
|
|
117
|
+
# value (styled text to be printed)
|
|
118
|
+
# type (-1 => deletion, 0 => insertion, 1 => not changed)
|
|
119
|
+
edit_list = []
|
|
120
|
+
|
|
121
|
+
# Getting a list of all the changes
|
|
122
|
+
insertion_poniter = 0
|
|
123
|
+
deletion_pointer = 0
|
|
124
|
+
while insertion_poniter < insertions || deletion_pointer < deletion_seq_keys.length
|
|
125
|
+
is_insertion = if insertion_poniter >= insertions
|
|
126
|
+
false
|
|
127
|
+
elsif deletion_pointer >= deletion_seq_keys.length
|
|
128
|
+
true
|
|
129
|
+
else
|
|
130
|
+
insertion_seq_keys[insertion_poniter] < deletion_seq_keys[deletion_pointer]
|
|
131
|
+
end
|
|
132
|
+
index = is_insertion ? insertion_seq_keys[insertion_poniter] : deletion_seq_keys[deletion_pointer]
|
|
133
|
+
if is_insertion
|
|
134
|
+
edit_list.push({ index: index, value: "#{insertion_seq[index][:value]}", type: 0,
|
|
135
|
+
old_index: insertion_seq[index][:old_index] })
|
|
136
|
+
insertion_poniter += 1
|
|
137
|
+
else
|
|
138
|
+
deletion_seq[index] = deletion_seq[index].sort_by { |del| del[:old_index] }
|
|
139
|
+
deletion_seq[index].each do |del|
|
|
140
|
+
old_index = del[:old_index]
|
|
141
|
+
edit_list.push({ index: index, value: "#{@old_s[old_index]}", type: -1,
|
|
142
|
+
old_index: old_index })
|
|
143
|
+
end
|
|
144
|
+
deletion_pointer += 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
max_space_diff = 3
|
|
150
|
+
print_queue = {}
|
|
151
|
+
# Getting all the lines to print (including context ones)
|
|
152
|
+
edit_list.each_with_index do |order, _i|
|
|
153
|
+
(order[:index] - max_space_diff..order[:index] + max_space_diff).each do |j|
|
|
154
|
+
next unless j >= 0 && j < @new_s.length
|
|
155
|
+
|
|
156
|
+
line = order[:index] == j ? order : { index: j, value: @new_s[j], type: 1, old_index: j }
|
|
157
|
+
print_queue[j] ||= []
|
|
158
|
+
print_queue[j].push(line) unless line[:type] == 1 && print_queue[j].length.positive?
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
print_queue = print_queue.sort.to_h
|
|
163
|
+
print_queue_keys = print_queue.keys
|
|
164
|
+
printed = {}
|
|
165
|
+
|
|
166
|
+
# Stylizing & printing lines
|
|
167
|
+
def print_line(line)
|
|
168
|
+
sign = if line[:type] == -1
|
|
169
|
+
'-'
|
|
170
|
+
else
|
|
171
|
+
line[:type] == 0 ? '+' : ''
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
index = if line[:type] == -1
|
|
175
|
+
line[:old_index]
|
|
176
|
+
else
|
|
177
|
+
line[:index]
|
|
178
|
+
end
|
|
179
|
+
print_text = "(#{index}) #{sign}#{line[:value]}"
|
|
180
|
+
if line[:type] == -1
|
|
181
|
+
print_text = print_text.red
|
|
182
|
+
elsif line[:type] == 0
|
|
183
|
+
print_text = print_text.green
|
|
184
|
+
end
|
|
185
|
+
puts print_text
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Printing all the lines
|
|
189
|
+
print_queue.each_with_index do |(index, lines), queue_i|
|
|
190
|
+
lines = lines.sort_by { |line| line[:type] }
|
|
191
|
+
is_changed = false
|
|
192
|
+
lines.each do |line, _i|
|
|
193
|
+
next if printed[[index, line[:type]]] == 1
|
|
194
|
+
|
|
195
|
+
print_line(line) unless is_changed == true && line[:type] == 1
|
|
196
|
+
is_changed = true
|
|
197
|
+
|
|
198
|
+
next unless line[:type] == -1
|
|
199
|
+
|
|
200
|
+
j = queue_i + 1
|
|
201
|
+
|
|
202
|
+
define_singleton_method :is_deletion? do
|
|
203
|
+
return false if j >= print_queue_keys.length
|
|
204
|
+
|
|
205
|
+
print_queue[print_queue_keys[j]]&.any? { |x| x[:type] == -1 }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
while is_deletion?
|
|
209
|
+
q_i = print_queue_keys[j]
|
|
210
|
+
cur_line = print_queue[q_i].find { |x| x[:type] == -1 }
|
|
211
|
+
cur_line ||= print_queue[q_i].find { |x| x[:type] == 1 }
|
|
212
|
+
print_line(cur_line)
|
|
213
|
+
printed[[q_i, cur_line[:type]]] = 1
|
|
214
|
+
j += 1
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/help.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Help
|
|
2
|
+
def print_usage
|
|
3
|
+
usage_error_message = "Usage: stolen-git <command> [options]
|
|
4
|
+
|
|
5
|
+
Commands:\n"
|
|
6
|
+
commands_docs = {
|
|
7
|
+
commit: 'Save changes',
|
|
8
|
+
reset: 'Revert to commit',
|
|
9
|
+
init: 'Initialize stolen-git to start tracking'
|
|
10
|
+
}
|
|
11
|
+
max_len = 0
|
|
12
|
+
commands_docs.each_key do |command|
|
|
13
|
+
max_len = max_len > command.length ? max_len : command.length
|
|
14
|
+
end
|
|
15
|
+
commands_docs.each do |key, value|
|
|
16
|
+
command = key.to_s
|
|
17
|
+
usage_error_message << ' ' << command << ' ' * (max_len - command.length) << ' ' << value << "\n"
|
|
18
|
+
end
|
|
19
|
+
puts usage_error_message
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/stg.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative 'help'
|
|
2
|
+
require_relative 'actions'
|
|
3
|
+
|
|
4
|
+
command = ARGV.shift
|
|
5
|
+
NAME = 'stolen-git'
|
|
6
|
+
include Actions
|
|
7
|
+
include Help
|
|
8
|
+
case command
|
|
9
|
+
when 'init'
|
|
10
|
+
p_initialize
|
|
11
|
+
|
|
12
|
+
when 'commit'
|
|
13
|
+
commit
|
|
14
|
+
|
|
15
|
+
when 'diff'
|
|
16
|
+
diff
|
|
17
|
+
when 'test'
|
|
18
|
+
test
|
|
19
|
+
|
|
20
|
+
when 'stage'
|
|
21
|
+
stage
|
|
22
|
+
|
|
23
|
+
when 'check_router'
|
|
24
|
+
check_router
|
|
25
|
+
when 'reset'
|
|
26
|
+
reset
|
|
27
|
+
when 'log'
|
|
28
|
+
log
|
|
29
|
+
|
|
30
|
+
when 'checkout'
|
|
31
|
+
checkout
|
|
32
|
+
|
|
33
|
+
when 'branch'
|
|
34
|
+
branch
|
|
35
|
+
|
|
36
|
+
when 'help'
|
|
37
|
+
puts print_usage
|
|
38
|
+
exit 1
|
|
39
|
+
|
|
40
|
+
when nil
|
|
41
|
+
puts print_usage
|
|
42
|
+
exit 1
|
|
43
|
+
|
|
44
|
+
else
|
|
45
|
+
puts "Unknown command: #{command}"
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
data/lib/utils.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'optparse'
|
|
3
|
+
require 'digest'
|
|
4
|
+
module Utils
|
|
5
|
+
def confirm?(prompt)
|
|
6
|
+
loop do
|
|
7
|
+
print "#{prompt} (y/n): "
|
|
8
|
+
input = gets.chomp.downcase
|
|
9
|
+
case input
|
|
10
|
+
when 'y'
|
|
11
|
+
return true
|
|
12
|
+
when 'n'
|
|
13
|
+
return false
|
|
14
|
+
else
|
|
15
|
+
puts 'wrong input only enter y/n'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ask(prompt)
|
|
21
|
+
print prompt
|
|
22
|
+
gets.chomp
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get_string_hash(str)
|
|
26
|
+
Digest::SHA256.hexdigest(str)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read_json(path)
|
|
30
|
+
JSON.parse(File.read(path))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_file_hash(path)
|
|
34
|
+
Digest::SHA256.file(path).hexdigest
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_file_from_hash(hash, dir_path)
|
|
38
|
+
Dir.glob("#{dir_path}/**/*").each do |file_path|
|
|
39
|
+
next unless File.file?(file_path)
|
|
40
|
+
|
|
41
|
+
file_hash = get_file_hash(file_path)
|
|
42
|
+
return file_path if file_hash == hash
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def revert_to_commit(commit_hash)
|
|
48
|
+
commit_content = read_json(".stolen-git/commits/#{commit_hash}.json")
|
|
49
|
+
commit_tree = read_json(".stolen-git/storage/trees/#{commit_content['tree_hash']}.json")
|
|
50
|
+
index = {}
|
|
51
|
+
commit_tree['entries'].each do |entry|
|
|
52
|
+
blob = File.read(".stolen-git/storage/blobs/#{entry['hash']}")
|
|
53
|
+
# TODO: Figure out what to do when path changes
|
|
54
|
+
File.write(entry['path'], blob)
|
|
55
|
+
index[entry['path']] ||= {}
|
|
56
|
+
index[entry['path']]['hash'] = entry['hash']
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
File.write('.stolen-git/index.json', JSON.pretty_generate(index))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
include Utils
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stg
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Amr ElTaweel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: colorize
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.1'
|
|
27
|
+
description: Stg is a version control system built on an architecture inspired by
|
|
28
|
+
git's but in the great language of ruby
|
|
29
|
+
email:
|
|
30
|
+
- amrbeducation@gmail.com
|
|
31
|
+
executables:
|
|
32
|
+
- stg
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- README.md
|
|
37
|
+
- bin/stg
|
|
38
|
+
- lib/actions.rb
|
|
39
|
+
- lib/differencing.rb
|
|
40
|
+
- lib/help.rb
|
|
41
|
+
- lib/stg.rb
|
|
42
|
+
- lib/utils.rb
|
|
43
|
+
homepage: https://github.com/amrbassem218/stolen-git
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata: {}
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.5.3
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Stg is like git but worse.
|
|
66
|
+
test_files: []
|