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 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
4
+
5
+ require 'stg'
6
+
7
+ Stg::CLI.start
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
@@ -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: []