stg 0.1.3 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a24fa114435be573b5b125dc7631052ba02e9b87c071d900aef8be5dd3c771e3
4
- data.tar.gz: ae2e3189797eb7f737bf2e6f44502458dbf0567d69c8659a1adf18c5a7619a0b
3
+ metadata.gz: 1eb93a1f37a9ec874a195189c9047b322ef7eb823f2c4af4219dad4aba1bdcb0
4
+ data.tar.gz: 7795502739c3503dcc0fa241cecc65093c711eed608d633ef32813c96c0f4d80
5
5
  SHA512:
6
- metadata.gz: e9b91105669be9f1b50129e3c347af9c7b9dc6adfd1d476d7c015005db0990927848956587961f16f6e3cd8a134edfc4cf4707ea720bd78423110d807e197e7e
7
- data.tar.gz: c19cea532598c24f46e8743f6f5af524d087780730b19e1081cc0318dea0ec2a68757effdd440429384e17107abf714161f302f4d9daf90f861a7e0206029fd8
6
+ metadata.gz: 60c435caaec0c696b269a1cd014a96539fa646eb6c204a9b53d67cb55dda4f2de16813202e3e1c6e5a3a12d774acf1c43cb26eb418c1817b03cca7d242b2aded
7
+ data.tar.gz: e1b69a714e519862c46d4264648504f3699b94fc7046f9900dbe780e36f3db8fa255cc9dfe40323b201bcb6f18ea7a8a0f01409b23724eedda67d7fa665e8d5e
data/README.md CHANGED
@@ -12,17 +12,22 @@ Stolen git is well... you get it. I'm building a mini git clone to learn version
12
12
 
13
13
  ## Usage
14
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
-
23
- | reset | Reset to the last commit | N/A |
24
- | 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 |
25
- | branch | List all branches. (or creating a branch by `branch <name>`) | N/A |
15
+ Run `stg init` once in a project before using the other commands. Stolen Git stores its data in `.stolen-git/` and tracks files through its own index.
16
+
17
+ | Command | Description | Options |
18
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
19
+ | `init` | Initialize `.stolen-git/` in the current directory. | N/A |
20
+ | `stage <file...>` | Add files or directories to the Stolen Git index. Directories are staged recursively. | N/A |
21
+ | `commit` | Save the current indexed state as a commit. | `-n, --name NAME` <br> `-d, --description DESCRIPTION` |
22
+ | `diff` | Show differences between the working directory and the last commit. | N/A |
23
+ | `log [limit]` | Print commit history. Pass a limit to show only the latest entries. | N/A |
24
+ | `reset [commit_id]` | With no id, restore working files from the index. With an id, restore that commit and move the current branch pointer. | N/A |
25
+ | `checkout <name>` | Check out a branch by name. | N/A |
26
+ | `checkout -c <commit_id>` | Check out a commit without moving the current branch pointer. | `-c, --commit` |
27
+ | `branch [name]` | List all branches, or create a branch when a name is provided. | N/A |
28
+ | `help` | Print the command list. | N/A |
29
+
30
+ For more detail on staging, committing, and reset behavior, see [COMMANDS.md](COMMANDS.md).
26
31
 
27
32
  ## Installation
28
33
 
@@ -39,6 +44,9 @@ Stolen git is well... you get it. I'm building a mini git clone to learn version
39
44
 
40
45
  ```
41
46
  gem install stg
47
+
48
+ # you may need administrative permission, in this case
49
+ sudo gem install stg
42
50
  ```
43
51
 
44
52
  Run `stg` to verify your installation
@@ -48,47 +56,51 @@ Run `stg` to verify your installation
48
56
  > [!NOTE]
49
57
  > You have to initialize with `stg init` for any of the other commands to work
50
58
 
51
- - Initialization
52
-
53
- ```
54
- stg init
55
- ```
59
+ ### Start a new project
56
60
 
57
- - Staging
58
-
59
- ```
60
- stg stage src/page.ts
61
- ```
61
+ ```sh
62
+ stg init
63
+ stg stage .
64
+ stg commit -n "Initial commit"
65
+ ```
62
66
 
63
- - Committing
67
+ ### Save a file change
64
68
 
65
- ```
66
- stg commit -n "Adding main page"
67
- ```
69
+ ```sh
70
+ stg stage lib/stg/actions.rb
71
+ stg commit -n "Improve reset validation"
72
+ ```
68
73
 
69
- - Logging all commits
74
+ ### Inspect history
70
75
 
71
- ```
72
- stg log
73
- ```
76
+ ```sh
77
+ stg log # Show 5 logs and waits for user confirmation to continue
78
+ stg log 3 # Shows last 3 commits in the branch and closes
79
+ ```
74
80
 
75
- Logging last 5 commits
81
+ ### Discard unstaged working changes
76
82
 
77
- ```
78
- stg log 5
79
- ```
83
+ ```sh
84
+ stg stage README.md
85
+ # edit README.md again
86
+ stg reset
87
+ ```
80
88
 
81
- - Help
89
+ `README.md` is restored to the version stored in the index (a.k.a last version of the file you staged).
82
90
 
83
- ```
84
- stg
85
- or
86
- stg help
87
- ```
91
+ ### Reset to a previous commit
88
92
 
89
- - Hard reset
93
+ > [!WARNING]
94
+ > `stg reset <commit_id>` is destructive. It moves the branch pointer back and can make later Stolen Git commits unreachable. Use `stg checkout -c <commit_id>` if you only want to inspect an older commit.
90
95
 
96
+ ```sh
97
+ stg log # shows all commits with commit_id next to the word commit in green
98
+ stg reset <commit_id>
91
99
  ```
92
100
 
101
+ ### Help
102
+
103
+ ```sh
93
104
  stg
105
+ stg help
94
106
  ```
@@ -9,6 +9,22 @@ module Actions
9
9
  include DiffCalc
10
10
 
11
11
  def p_initialize
12
+ begin
13
+ OptionParser.new do |opts|
14
+ opts.banner = 'Usage: stg init'
15
+
16
+ opts.on_tail('-h', '--help', 'Show this help') do
17
+ puts opts
18
+ exit
19
+ end
20
+ end.parse!
21
+ rescue OptionParser::ParseError => e
22
+ puts e.message
23
+ puts 'Usage: stg init'
24
+ puts ' -h, --help Show this help'
25
+ exit 1
26
+ end
27
+
12
28
  # Check if already initialized
13
29
  if File.exist?('.stolen-git')
14
30
  if confirm?('An instance of stolen-git is already up here do you want to replace it')
@@ -41,6 +57,7 @@ module Actions
41
57
 
42
58
  # main files
43
59
  File.write('.stolen-git/project_info.json', {})
60
+ File.write('.stg-ignore', JSON.pretty_generate(['.*/']))
44
61
  File.write('.stolen-git/commits.json', JSON.pretty_generate({ commits: [] }))
45
62
  File.write('.stolen-git/index.json', {})
46
63
 
@@ -54,16 +71,52 @@ module Actions
54
71
  end
55
72
 
56
73
  def stage
74
+ begin
75
+ OptionParser.new do |opts|
76
+ opts.banner = 'Usage: stg stage <file> [files...]'
77
+
78
+ opts.on_tail('-h', '--help', 'Show this help') do
79
+ puts opts
80
+ exit
81
+ end
82
+ end.parse!
83
+ rescue OptionParser::ParseError => e
84
+ puts e.message
85
+ puts 'Usage: stg stage <file> [files...]'
86
+ puts ' -h, --help Show this help'
87
+ exit 1
88
+ end
89
+
57
90
  inp = ARGV
58
91
 
59
92
  if inp.empty?
60
- puts 'Usage: stg stage <directory/> <file.xy>'
93
+ puts 'Usage: stg stage <file> [files...]'
61
94
  return
62
95
  end
63
96
 
64
- index = JSON.parse(File.read('.stolen-git/index.json'))
97
+ index = read_json('.stolen-git/index.json')
98
+ ignore = read_json('.stg-ignore')
99
+ index.reject! { |key, _value| ignored_path?(key, ignore) }
100
+
101
+ path_in_directory = lambda do |path, dir_path|
102
+ dir_path == '.' || path == dir_path || path.start_with?("#{dir_path}/")
103
+ end
104
+
105
+ stage_deleted = lambda do |path|
106
+ path = clean_path(path)
107
+
108
+ index.keys.each do |indexed_path|
109
+ next unless indexed_path == path || path_in_directory.call(indexed_path, path)
110
+ next if File.exist?(indexed_path)
111
+
112
+ index.delete(indexed_path)
113
+ end
114
+ end
65
115
 
66
116
  stage_file = lambda do |file_path|
117
+ file_path = clean_path(file_path)
118
+ return if ignored_path?(file_path, ignore)
119
+
67
120
  file_hash = get_file_hash(file_path)
68
121
  file_content = File.read(file_path)
69
122
 
@@ -79,7 +132,14 @@ module Actions
79
132
  end
80
133
 
81
134
  stage_directory = lambda do |dir_path|
135
+ dir_path = clean_path(dir_path)
136
+ return if ignored_path?(dir_path, ignore)
137
+
138
+ stage_deleted.call(dir_path)
139
+
82
140
  Dir.children(dir_path).each do |entry|
141
+ next if entry == '.stolen-git'
142
+
83
143
  path = File.join(dir_path, entry)
84
144
  if File.file?(path)
85
145
  stage_file.call(path)
@@ -90,8 +150,13 @@ module Actions
90
150
  end
91
151
 
92
152
  inp.each do |inp_path|
153
+ inp_path = clean_path(inp_path)
93
154
  unless File.exist? inp_path
94
- puts "#{inp_path} doesn't exist"
155
+ if index.key?(inp_path) || index.keys.any? { |key| path_in_directory.call(key, inp_path) }
156
+ stage_deleted.call(inp_path)
157
+ else
158
+ puts "#{inp_path} doesn't exist"
159
+ end
95
160
  next
96
161
  end
97
162
 
@@ -111,22 +176,41 @@ module Actions
111
176
  options = { name: '', description: '' }
112
177
 
113
178
  # Getting commit name & description
114
- OptionParser.new do |opts|
115
- opts.banner = 'Usage: stg commit [options]'
179
+ begin
180
+ OptionParser.new do |opts|
181
+ opts.banner = 'Usage: stg commit [options]'
116
182
 
117
- opts.on('-n', '--name NAME', 'Add a commit name') do |name|
118
- options[:name] = name
119
- end
183
+ opts.on('-n', '--name NAME', 'Add a commit name') do |name|
184
+ options[:name] = name
185
+ end
120
186
 
121
- opts.on('-d', '--description DESCRIPTION', 'Add a commit description') do |description|
122
- options[:description] = description
123
- end
124
- end.parse!
187
+ opts.on('-d', '--description DESCRIPTION', 'Add a commit description') do |description|
188
+ options[:description] = description
189
+ end
190
+
191
+ opts.on_tail('-h', '--help', 'Show this help') do
192
+ puts opts
193
+ exit
194
+ end
195
+ end.parse!
196
+ rescue OptionParser::ParseError => e
197
+ puts e.message
198
+ puts 'Usage: stg commit [options]'
199
+ puts ' -n, --name NAME Add a commit name'
200
+ puts ' -d, --description DESCRIPTION Add a commit description'
201
+ puts ' -h, --help Show this help'
202
+ exit 1
203
+ end
125
204
 
126
205
  options[:name] = ask('Add a commit name: ') if options[:name].empty?
127
206
 
128
207
  # Get the index
129
208
  index = JSON.parse(File.read('.stolen-git/index.json'))
209
+ ignore = read_json('.stg-ignore')
210
+ original_index_size = index.length
211
+ index.reject! { |key, _value| ignored_path?(key, ignore) }
212
+ File.write('.stolen-git/index.json', JSON.pretty_generate(index)) if index.length != original_index_size
213
+
130
214
  tree_content = {
131
215
  entries: []
132
216
  }
@@ -139,35 +223,59 @@ module Actions
139
223
  parent_commit = branch_content['commit_pointer']
140
224
 
141
225
  # Getting differences to last commit
142
-
143
226
  no_insertions = 0
144
227
  no_deletions = 0
145
228
  no_file_changed = 0
229
+
146
230
  commit_diff = {}
147
231
 
148
232
  getting_diff = lambda do |entries|
149
- parent_index = 0
233
+ if entries.nil?
234
+ index.each do |key, value|
235
+ key = clean_path(key)
236
+ new_entry_content = File.read(".stolen-git/storage/blobs/#{value['hash']}").lines.to_a
237
+ compute_diff([], new_entry_content)
238
+ diff = build_sequences
239
+ no_insertions += diff[:insertions]
240
+ no_deletions += diff[:deletions]
241
+ no_file_changed += 1
242
+ commit_diff[key] = diff
243
+ end
244
+ else
245
+ parent_map = entries
246
+ .reject { |entry| ignored_path?(entry['path'], ignore) }
247
+ .to_h { |e| [e['path'], e['hash']] }
150
248
 
151
- index.each do |key, value|
152
- if entries && entries[parent_index] && key == entries[parent_index]['path']
249
+ index.each do |key, value|
250
+ key = clean_path(key)
153
251
  new_hash = value['hash']
154
- old_hash = entries[parent_index]['hash']
155
- parent_index += 1
156
- next if value['hash'] == new_hash
252
+ old_hash = parent_map[key]
253
+
254
+ if old_hash
255
+ next if old_hash == new_hash
256
+
257
+ entry_content = File.read(".stolen-git/storage/blobs/#{old_hash}").lines.to_a
258
+ new_entry_content = File.read(".stolen-git/storage/blobs/#{new_hash}").lines.to_a
259
+ compute_diff(entry_content, new_entry_content)
260
+ else
261
+ new_entry_content = File.read(".stolen-git/storage/blobs/#{new_hash}").lines.to_a
262
+ compute_diff([], new_entry_content)
263
+ end
157
264
 
158
- entry_content = File.read(".stolen-git/storage/blobs/#{old_hash}").lines.to_a
159
- new_entry_content = File.read(".stolen-git/storage/blobs/#{new_hash}").lines.to_a
160
- compute_diff(entry_content, new_entry_content)
161
265
  diff = build_sequences
162
266
  no_insertions += diff[:insertions]
163
267
  no_deletions += diff[:deletions]
164
268
  no_file_changed += 1
165
269
  commit_diff[key] = diff
166
- else
167
- new_entry_content = File.read(".stolen-git/storage/blobs/#{value['hash']}").lines.to_a
168
- compute_diff('', new_entry_content)
169
- diff = build_sequences
270
+ end
170
271
 
272
+ parent_map.each do |key, old_hash|
273
+ key = clean_path(key)
274
+ next if index.key?(key)
275
+
276
+ entry_content = File.read(".stolen-git/storage/blobs/#{old_hash}").lines.to_a
277
+ compute_diff(entry_content, [])
278
+ diff = build_sequences
171
279
  no_insertions += diff[:insertions]
172
280
  no_deletions += diff[:deletions]
173
281
  no_file_changed += 1
@@ -193,6 +301,7 @@ module Actions
193
301
 
194
302
  # Making the tree
195
303
  index.each do |key, value|
304
+ key = clean_path(key)
196
305
  tree_content[:entries].push({
197
306
  path: key,
198
307
  type: 'blob',
@@ -243,23 +352,46 @@ module Actions
243
352
  end
244
353
 
245
354
  def reset
355
+ reset_usage = lambda do
356
+ puts 'Usage: stg reset <commit_id>'
357
+ puts "You can find commit_id by running 'stg log'"
358
+ end
359
+
360
+ begin
361
+ OptionParser.new do |opts|
362
+ opts.banner = 'Usage: stg reset <commit_id>'
363
+
364
+ opts.on_tail('-h', '--help', 'Show this help') do
365
+ puts opts
366
+ exit
367
+ end
368
+ end.parse!
369
+ rescue OptionParser::ParseError => e
370
+ puts e.message
371
+ reset_usage.call
372
+ puts ' -h, --help Show this help'
373
+ exit 1
374
+ end
375
+
376
+ if ARGV.length > 1
377
+ reset_usage.call
378
+ return
379
+ end
380
+
246
381
  commit_id = ARGV.first
247
382
  commit_history = read_json('.stolen-git/commits.json')
248
383
  pointer = read_json('.stolen-git/pointer.json')
249
384
  branch_id = pointer['point_to']
250
385
  branch_content = read_json(".stolen-git/branches/#{branch_id}.json")
251
- current_commit_hash = branch_content['commit_pointer']
252
-
253
- new_commit = if !commit_id || commit_id.empty?
254
- current_commit_hash
255
- else
256
- commit_history['commits'].find do |x|
257
- x['id'] == commit_id
258
- end['hash']
259
- end
260
-
261
- if new_commit.empty?
262
- puts "This commit doesn't exit"
386
+ if commit_id.nil? || commit_id.empty?
387
+ revert_to_index
388
+ return
389
+ end
390
+
391
+ commit = commit_history['commits'].find { |x| x['id'] == commit_id }
392
+ new_commit = commit && commit['hash']
393
+ if new_commit.nil? || new_commit.empty?
394
+ reset_usage.call
263
395
  return
264
396
  end
265
397
 
@@ -272,13 +404,27 @@ module Actions
272
404
  options = { commit: false }
273
405
 
274
406
  # Getting commit name & description
275
- OptionParser.new do |opts|
276
- opts.banner = 'Usage: stg checkout [options]'
407
+ begin
408
+ OptionParser.new do |opts|
409
+ opts.banner = 'Usage: stg checkout [options]'
410
+
411
+ opts.on('-c', '--commit', 'Add a commit id instead') do
412
+ options[:commit] = true
413
+ end
414
+
415
+ opts.on_tail('-h', '--help', 'Show this help') do
416
+ puts opts
417
+ exit
418
+ end
419
+ end.parse!
420
+ rescue OptionParser::ParseError => e
421
+ puts e.message
422
+ puts 'Usage: stg checkout [options]'
423
+ puts ' -c, --commit Add a commit id instead'
424
+ puts ' -h, --help Show this help'
425
+ exit 1
426
+ end
277
427
 
278
- opts.on('-c', '--commit', 'Add a commit id instead') do
279
- options[:commit] = true
280
- end
281
- end.parse!
282
428
  inp = ARGV.last
283
429
  if !inp
284
430
  puts 'Please Enter the name of a branch or commit_id'
@@ -287,7 +433,7 @@ module Actions
287
433
  current_commit_hash = commit_history['commits'].find { |x| x['id'] == inp }['hash']
288
434
  revert_to_commit(current_commit_hash)
289
435
  else
290
- # TODO: Handle if user enters branch_id instead
436
+ # TODO: Handle if user enters branch_id instead of name
291
437
  branch_content = {}
292
438
  branch_id = ''
293
439
  Dir.children('.stolen-git/branches').each do |entry|
@@ -302,28 +448,47 @@ module Actions
302
448
  nil
303
449
  end
304
450
  commit_hash = branch_content['commit_pointer']
305
- revert_to_commit(commit_hash)
451
+ revert_to_commit(commit_hash) if commit_hash&.length&.positive?
306
452
  pointer = read_json('.stolen-git/pointer.json')
307
453
  pointer['point_to'] = branch_id
308
454
  pointer['type'] = 'branch'
309
455
  File.write('.stolen-git/pointer.json', JSON.pretty_generate(pointer))
456
+
310
457
  end
311
458
  end
312
459
 
313
460
  def branch
314
- name = ARGV.last
461
+ begin
462
+ OptionParser.new do |opts|
463
+ opts.banner = 'Usage: stg branch [name]'
464
+
465
+ opts.on_tail('-h', '--help', 'Show this help') do
466
+ puts opts
467
+ exit
468
+ end
469
+ end.parse!
470
+ rescue OptionParser::ParseError => e
471
+ puts e.message
472
+ puts 'Usage: stg branch [name]'
473
+ puts ' -h, --help Show this help'
474
+ exit 1
475
+ end
476
+
477
+ names = ARGV
315
478
  pointer = read_json('.stolen-git/pointer.json')
316
479
  pointed_branch = pointer['type'] == 'branch' ? pointer['point_to'] : ''
317
- if name
318
- first_commit = pointer['type'] == 'branch' ? read_json(".stolen-git/branches/#{pointed_branch}.json")['commit_pointer'] : pointer['point_to']
319
- id = SecureRandom.uuid
320
- File.write(".stolen-git/branches/#{id}.json", JSON.pretty_generate({
321
- name: name,
322
- created_at: Time.now,
323
- commit_pointer: first_commit
324
- }))
480
+ if names && names.length.positive?
481
+ names.each do |name|
482
+ first_commit = pointer['type'] == 'branch' ? read_json(".stolen-git/branches/#{pointed_branch}.json")['commit_pointer'] : pointer['point_to']
483
+ id = SecureRandom.uuid
484
+ File.write(".stolen-git/branches/#{id}.json", JSON.pretty_generate({
485
+ name: name,
486
+ created_at: Time.now,
487
+ commit_pointer: first_commit
488
+ }))
489
+ puts "branch #{name} created"
490
+ end
325
491
  return
326
- puts "branch #{name} created"
327
492
  end
328
493
  Dir.children('.stolen-git/branches').each do |entry|
329
494
  branch_content = read_json(".stolen-git/branches/#{entry}")
@@ -338,8 +503,25 @@ module Actions
338
503
  end
339
504
 
340
505
  def diff
506
+ begin
507
+ OptionParser.new do |opts|
508
+ opts.banner = 'Usage: stg diff'
509
+
510
+ opts.on_tail('-h', '--help', 'Show this help') do
511
+ puts opts
512
+ exit
513
+ end
514
+ end.parse!
515
+ rescue OptionParser::ParseError => e
516
+ puts e.message
517
+ puts 'Usage: stg diff'
518
+ puts ' -h, --help Show this help'
519
+ exit 1
520
+ end
521
+
341
522
  index = read_json('.stolen-git/index.json')
342
523
  index.each do |key, value|
524
+ key = clean_path(key)
343
525
  new_file_content = File.exist?(key) ? File.read(key) : nil
344
526
  file_name = File.basename(key)
345
527
  if new_file_content.nil?
@@ -357,25 +539,65 @@ module Actions
357
539
  end
358
540
 
359
541
  def log
360
- limit = ARGV.last
361
- limit = limit && !limit.empty? ? limit.to_i : 0
362
- commit_history_content = read_json('.stolen-git/commits.json')
542
+ # Handle if the user wants to -[num]
543
+ ARGV.map! do |arg|
544
+ if arg =~ /^-(\d+)$/
545
+ ['-l', ::Regexp.last_match(1)]
546
+ else
547
+ arg
548
+ end
549
+ end.flatten!
363
550
 
364
- commit_history_content['commits'].each_with_index do |commit, i|
365
- break if limit > 0 && i >= limit
551
+ options = { limit: 0 }
552
+ begin
553
+ OptionParser.new do |opts|
554
+ opts.banner = 'Usage: stg log [limit]'
366
555
 
367
- if limit == 0 && i >= 5
368
- q = ask(':')
369
- break if q == 'q'
370
- end
371
- commit_content = read_json(".stolen-git/commits/#{commit['hash']}.json")
556
+ opts.on('-l', '--limit LIMIT', 'limit showed logs') do |num|
557
+ options[:limit] = num.to_i
558
+ end
559
+
560
+ opts.on_tail('-h', '--help', 'Show this help') do
561
+ puts opts
562
+ exit
563
+ end
564
+ end.parse!
565
+ rescue OptionParser::ParseError => e
566
+ puts e.message
567
+ puts 'Usage: stg log [limit]'
568
+ puts ' -[num], --limit limit showed logs i.e. stg log -5'
569
+ puts ' -l, --limit limit showed logs'
570
+ puts ' -h, --help Show this help'
571
+ exit 1
572
+ end
573
+
574
+ is_limited = options[:limit] > 0
575
+ pointer = read_json('.stolen-git/pointer.json')
576
+ last_commit = pointer['type'] == 'commit' ? pointer['point_to'] : read_json(".stolen-git/branches/#{pointer['point_to']}.json")['commit_pointer']
577
+ unless last_commit
578
+ puts "Couldn't find last commits. The setup might have been corrupted. If that's the case run 'stg init'"
579
+ end
580
+ i = 0
581
+
582
+ print_commit = lambda do |commit_hash, commit_content|
372
583
  puts
373
584
  puts "commit #{commit_content['id']}".green
585
+ puts "hash: #{commit_hash[0..5]}...#{commit_hash[-5..]}".green
374
586
  puts "Author #{commit_content['author_profile']['username']} <#{commit_content['author_profile']['email']}>"
375
587
  puts "Date: #{commit_content['created_at']}"
376
588
  puts
377
589
  puts " #{commit_content['name']}"
378
- puts
590
+ end
591
+
592
+ while (is_limited && i < options[:limit] || !is_limited) && last_commit && last_commit.length.positive?
593
+ last_commit_content = read_json(".stolen-git/commits/#{last_commit}.json")
594
+ print_commit.call(last_commit, last_commit_content)
595
+ last_commit = last_commit_content['parent_commit']
596
+ i += 1
597
+ if !is_limited && i >= 5
598
+ q = ask(':')
599
+ break if q == 'q'
600
+ end
379
601
  end
380
602
  end
381
603
  end
@@ -27,14 +27,12 @@ module DiffCalc
27
27
  if i1 >= @old_s.length
28
28
  result = { diff_cnt: @new_s.length - i2, insertions: @new_s.length - i2, deletions: 0, action: :insert }
29
29
  @mem[[i1, i2]] = result
30
- differencing(i1, i2 + 1) if i2 < @new_s.length
31
30
  return result
32
31
  end
33
32
 
34
33
  if i2 >= @new_s.length
35
34
  result = { diff_cnt: @old_s.length - i1, insertions: 0, deletions: @old_s.length - i1, action: :delete }
36
35
  @mem[[i1, i2]] = result
37
- differencing(i1 + 1, i2) if i1 < @old_s.length
38
36
  return result
39
37
  end
40
38
 
@@ -69,7 +67,10 @@ module DiffCalc
69
67
  deletion_seq = {}
70
68
 
71
69
  while i1 < @old_s.length || i2 < @new_s.length
72
- action = @mem[[i1, i2]][:action]
70
+ entry = @mem[[i1, i2]]
71
+ break unless entry
72
+
73
+ action = entry[:action]
73
74
 
74
75
  case action
75
76
  when :done
@@ -82,12 +83,13 @@ module DiffCalc
82
83
  deletion_seq[i2].push({ old_index: i1, ma_type: 'bs' })
83
84
  i1 += 1
84
85
  when :insert
85
- insertion_seq[i2] = { value: @new_s[i2], old_index: i1 }
86
+ insertion_seq[i2] =
87
+ { value: @new_s[i2].encode('UTF-8', invalid: :replace, undef: :replace, replace: ''), old_index: i1 }
86
88
  i2 += 1
87
89
  end
88
90
  end
89
91
 
90
- result = @mem[[0, 0]]
92
+ result = @mem[[0, 0]] || { insertions: 0, deletions: 0, diff_cnt: 0 }
91
93
  {
92
94
  insertion_seq: insertion_seq,
93
95
  deletion_seq: deletion_seq,
@@ -5,14 +5,14 @@ module Help
5
5
  Commands:\n"
6
6
  commands_docs = {
7
7
  init: 'Initialize the project',
8
- stage: 'add a file or directory to be tracked ',
9
- commit: 'Save the current tracked state ',
10
- diff: 'get the difference between working directory and the last commit ',
11
- log: 'print out commit history (limit print by a number `log <number>`) ',
12
- reset: 'Revert to commit',
13
- checkout: 'check a commit or a branch without loss in data',
14
- branch: 'List all branches. (or creating a branch by `branch <name>`) '
15
-
8
+ stage: 'Add files or directories to the index',
9
+ commit: 'Save the current indexed state',
10
+ diff: 'Show differences between the working directory and the last commit',
11
+ log: 'Print commit history',
12
+ reset: 'Restore from the index, or restore a commit when an id is provided',
13
+ checkout: 'Check out a branch, or a commit with -c',
14
+ branch: 'List branches, or create a branch when a name is provided',
15
+ help: 'Show this list'
16
16
  }
17
17
  max_len = 0
18
18
  commands_docs.each_key do |command|
@@ -1,6 +1,7 @@
1
1
  require 'fileutils'
2
2
  require 'optparse'
3
3
  require 'digest'
4
+ require 'pathname'
4
5
  module Utils
5
6
  def confirm?(prompt)
6
7
  loop do
@@ -28,6 +29,8 @@ module Utils
28
29
 
29
30
  def read_json(path)
30
31
  JSON.parse(File.read(path))
32
+ rescue StandardError
33
+ nil
31
34
  end
32
35
 
33
36
  def get_file_hash(path)
@@ -47,17 +50,58 @@ module Utils
47
50
  def revert_to_commit(commit_hash)
48
51
  commit_content = read_json(".stolen-git/commits/#{commit_hash}.json")
49
52
  commit_tree = read_json(".stolen-git/storage/trees/#{commit_content['tree_hash']}.json")
53
+ current_index = read_json('.stolen-git/index.json') || {}
50
54
  index = {}
55
+
51
56
  commit_tree['entries'].each do |entry|
52
57
  blob = File.read(".stolen-git/storage/blobs/#{entry['hash']}")
53
58
  # TODO: Figure out what to do when path changes
59
+ dir = File.dirname(entry['path'])
60
+ FileUtils.mkdir_p(dir) unless dir == '.'
54
61
  File.write(entry['path'], blob)
55
62
  index[entry['path']] ||= {}
56
63
  index[entry['path']]['hash'] = entry['hash']
57
64
  end
58
65
 
66
+ (current_index.keys - index.keys).each do |path|
67
+ FileUtils.rm_f(path)
68
+ end
69
+
59
70
  File.write('.stolen-git/index.json', JSON.pretty_generate(index))
60
71
  end
72
+
73
+ def revert_to_index
74
+ index = read_json('.stolen-git/index.json') || {}
75
+
76
+ index.each do |path, entry|
77
+ blob = File.read(".stolen-git/storage/blobs/#{entry['hash']}")
78
+ File.write(path, blob)
79
+ end
80
+ end
81
+
82
+ def clean_path(path)
83
+ Pathname.new(path).cleanpath.to_s
84
+ end
85
+
86
+ def ignored_path?(path, ignore_patterns)
87
+ path = clean_path(path)
88
+ return false if path == '.'
89
+
90
+ path_as_dir = path.end_with?('/') ? path : "#{path}/"
91
+
92
+ ignore_patterns&.any? do |pattern|
93
+ File.fnmatch(pattern, path) ||
94
+ File.fnmatch(pattern, path_as_dir) ||
95
+ (pattern.end_with?('/') && File.fnmatch("#{pattern}**", path))
96
+ end
97
+ end
98
+
99
+ def check_program_exists
100
+ return true if File.exist? '.stolen-git'
101
+
102
+ puts "There is no instance of stolen-git found. Please run 'stg init' first."
103
+ false
104
+ end
61
105
  end
62
106
 
63
107
  include Utils
@@ -0,0 +1,3 @@
1
+ module Stg
2
+ VERSION = '0.1.6'
3
+ end
data/lib/stg.rb CHANGED
@@ -1,5 +1,7 @@
1
- require_relative 'help'
2
- require_relative 'actions'
1
+ require 'optparse'
2
+ require_relative 'stg/help'
3
+ require_relative 'stg/actions'
4
+ require_relative 'stg/version'
3
5
 
4
6
  module Stg
5
7
  class CLI
@@ -7,10 +9,36 @@ module Stg
7
9
  extend Help
8
10
 
9
11
  def self.start
12
+ begin
13
+ OptionParser.new do |opts|
14
+ opts.banner = 'Usage: stg <command> [options]'
15
+
16
+ opts.on('-v', '--version', 'Show version') do
17
+ puts "stg version #{VERSION}"
18
+ exit
19
+ end
20
+
21
+ opts.on('-h', '--help', 'Show this help') do
22
+ puts print_usage
23
+ exit
24
+ end
25
+ end.order!
26
+ rescue OptionParser::ParseError
27
+ # Ignore unknown options, let command handlers deal with them
28
+ end
29
+
10
30
  command = ARGV.shift
11
- case command
12
- when 'init'
31
+ if !command || command.empty?
32
+ puts print_usage
33
+ return
34
+ elsif command == 'init'
13
35
  p_initialize
36
+ return
37
+ else
38
+ return unless check_program_exists
39
+ end
40
+
41
+ case command
14
42
  when 'commit'
15
43
  commit
16
44
  when 'diff'
@@ -42,3 +70,5 @@ module Stg
42
70
  end
43
71
  end
44
72
  end
73
+
74
+ Stg::CLI.start if __FILE__ == $PROGRAM_NAME
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amr ElTaweel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-02 00:00:00.000000000 Z
11
+ date: 2026-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -35,11 +35,12 @@ extra_rdoc_files: []
35
35
  files:
36
36
  - README.md
37
37
  - bin/stg
38
- - lib/actions.rb
39
- - lib/differencing.rb
40
- - lib/help.rb
41
38
  - lib/stg.rb
42
- - lib/utils.rb
39
+ - lib/stg/actions.rb
40
+ - lib/stg/differencing.rb
41
+ - lib/stg/help.rb
42
+ - lib/stg/utils.rb
43
+ - lib/stg/version.rb
43
44
  homepage: https://github.com/amrbassem218/stolen-git
44
45
  licenses:
45
46
  - MIT