Module: Actions

Includes:
DiffCalc
Included in:
Stg::CLI
Defined in:
lib/stg/actions.rb

Instance Attribute Summary

Attributes included from DiffCalc

#cnt, #mem, #new_s, #old_s

Instance Method Summary collapse

Methods included from DiffCalc

#build_sequences, #compute_diff, #differencing, #initialize, #print_diff, #print_line

Instance Method Details

#branchObject



460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/stg/actions.rb', line 460

def branch
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg branch [name]'

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg branch [name]'
    puts '  -h, --help    Show this help'
    exit 1
  end

  names = ARGV
  pointer = read_json('.stolen-git/pointer.json')
  pointed_branch = pointer['type'] == 'branch' ? pointer['point_to'] : ''
  if names && names.length.positive?
    names.each do |name|
      first_commit = pointer['type'] == 'branch' ? read_json(".stolen-git/branches/#{pointed_branch}.json")['commit_pointer'] : pointer['point_to']
      id = SecureRandom.uuid
      File.write(".stolen-git/branches/#{id}.json", JSON.pretty_generate({
                                                                           name: name,
                                                                           created_at: Time.now,
                                                                           commit_pointer: first_commit
                                                                         }))
      puts "branch #{name} created"
    end
    return
  end
  Dir.children('.stolen-git/branches').each do |entry|
    branch_content = read_json(".stolen-git/branches/#{entry}")
    branch_id = File.basename(entry, '.*')
    if pointed_branch == branch_id
      print '* '
      puts "#{branch_content['name']}".green
    else
      puts "#{branch_content['name']}"
    end
  end
end

#checkoutObject



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/stg/actions.rb', line 403

def checkout
  options = { commit: false }

  # Getting commit name & description
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg checkout [options]'

      opts.on('-c', '--commit', 'Add a commit id instead') do
        options[:commit] = true
      end

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg checkout [options]'
    puts '  -c, --commit    Add a commit id instead'
    puts '  -h, --help      Show this help'
    exit 1
  end

  inp = ARGV.last
  if !inp
    puts 'Please Enter the name of a branch or commit_id'
  elsif options[:commit]
    commit_history = read_json('.stolen-git/commits.json')
    current_commit_hash = commit_history['commits'].find { |x| x['id'] == inp }['hash']
    revert_to_commit(current_commit_hash)
  else
    # TODO: Handle if user enters branch_id instead of name
    branch_content = {}
    branch_id = ''
    Dir.children('.stolen-git/branches').each do |entry|
      branch_content = read_json(".stolen-git/branches/#{entry}")
      if branch_content['name'] == inp
        branch_id = File.basename(entry, '.*')
        break
      end
    end
    if branch_content.empty?
      puts 'There is no branch with that name.'
      nil
    end
    commit_hash = branch_content['commit_pointer']
    revert_to_commit(commit_hash) if commit_hash&.length&.positive?
    pointer = read_json('.stolen-git/pointer.json')
    pointer['point_to'] = branch_id
    pointer['type'] = 'branch'
    File.write('.stolen-git/pointer.json', JSON.pretty_generate(pointer))

  end
end

#commitObject



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/stg/actions.rb', line 175

def commit
  options = { name: '', description: '' }

  # Getting commit name & description
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg commit [options]'

      opts.on('-n', '--name NAME', 'Add a commit name') do |name|
        options[:name] = name
      end

      opts.on('-d', '--description DESCRIPTION', 'Add a commit description') do |description|
        options[:description] = description
      end

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg commit [options]'
    puts '  -n, --name NAME                Add a commit name'
    puts '  -d, --description DESCRIPTION  Add a commit description'
    puts '  -h, --help                     Show this help'
    exit 1
  end

  options[:name] = ask('Add a commit name: ') if options[:name].empty?

  # Get the index
  index = JSON.parse(File.read('.stolen-git/index.json'))
  ignore = read_json('.stg-ignore')
  original_index_size = index.length
  index.reject! { |key, _value| ignored_path?(key, ignore) }
  File.write('.stolen-git/index.json', JSON.pretty_generate(index)) if index.length != original_index_size

  tree_content = {
    entries: []
  }

  commit_history = JSON.parse(File.read('.stolen-git/commits.json'))

  pointer = read_json('.stolen-git/pointer.json')
  branch_id = pointer['point_to']
  branch_content = read_json(".stolen-git/branches/#{branch_id}.json")
  parent_commit = branch_content['commit_pointer']

  # Getting differences to last commit
  no_insertions = 0
  no_deletions = 0
  no_file_changed = 0

  commit_diff = {}

  getting_diff = lambda do |entries|
    if entries.nil?
      index.each do |key, value|
        key = clean_path(key)
        new_entry_content = File.read(".stolen-git/storage/blobs/#{value['hash']}").lines.to_a
        compute_diff([], new_entry_content)
        diff = build_sequences
        no_insertions += diff[:insertions]
        no_deletions += diff[:deletions]
        no_file_changed += 1
        commit_diff[key] = diff
      end
    else
      parent_map = entries
                   .reject { |entry| ignored_path?(entry['path'], ignore) }
                   .to_h { |e| [e['path'], e['hash']] }

      index.each do |key, value|
        key = clean_path(key)
        new_hash = value['hash']
        old_hash = parent_map[key]

        if old_hash
          next if old_hash == new_hash

          entry_content = File.read(".stolen-git/storage/blobs/#{old_hash}").lines.to_a
          new_entry_content = File.read(".stolen-git/storage/blobs/#{new_hash}").lines.to_a
          compute_diff(entry_content, new_entry_content)
        else
          new_entry_content = File.read(".stolen-git/storage/blobs/#{new_hash}").lines.to_a
          compute_diff([], new_entry_content)
        end

        diff = build_sequences
        no_insertions += diff[:insertions]
        no_deletions += diff[:deletions]
        no_file_changed += 1
        commit_diff[key] = diff
      end

      parent_map.each do |key, old_hash|
        key = clean_path(key)
        next if index.key?(key)

        entry_content = File.read(".stolen-git/storage/blobs/#{old_hash}").lines.to_a
        compute_diff(entry_content, [])
        diff = build_sequences
        no_insertions += diff[:insertions]
        no_deletions += diff[:deletions]
        no_file_changed += 1
        commit_diff[key] = diff
      end
    end
  end

  if parent_commit.empty?
    getting_diff.call(nil)
  else
    parent_commit_content = read_json(".stolen-git/commits/#{parent_commit}.json")
    parent_tree_hash = parent_commit_content['tree_hash']
    parent_tree_content = JSON.parse(File.read(".stolen-git/storage/trees/#{parent_tree_hash}.json"))
    getting_diff.call(parent_tree_content['entries'])
  end

  if no_file_changed <= 0
    puts 'Everything up to date'
    puts "If you have changed please 'stg stage' them first "
    return
  end

  # Making the tree
  index.each do |key, value|
    key = clean_path(key)
    tree_content[:entries].push({
                                  path: key,
                                  type: 'blob',
                                  hash: value['hash'],
                                  diff: commit_diff[key] || {}
                                })
  end
  tree_content[:entries] = tree_content[:entries].sort { |a, b| a['path'] <=> b['path'] }
  tree_content = JSON.pretty_generate(tree_content)
  tree_hash = get_string_hash(tree_content)
  File.write(".stolen-git/storage/trees/#{tree_hash}.json", tree_content)

  # Making the commit
  # TODO: Change to actual values when personal profiles are created
  commit_id = SecureRandom.uuid
  commit_content = {
    tree_hash: tree_hash,
    created_at: Time.now,
    id: commit_id,
    parent_commit: parent_commit,
    branch_id: branch_id,
    author_profile: {
      name: 'Amr',
      email: 'amrbassem218@gmail.com',
      username: 'amrbassem218'
    },
    name: options[:name],
    description: options[:description],

    no_insertions: no_insertions,
    no_deletions: no_deletions,
    no_files_changed: no_file_changed
  }
  commit_content = JSON.pretty_generate(commit_content)
  commit_hash = get_string_hash(commit_content)
  File.write(".stolen-git/commits/#{commit_hash}.json", commit_content)

  # Adding commit to history
  commit_history['commits'].push({ id: commit_id, hash: commit_hash, name: options[:name] })
  File.write('.stolen-git/commits.json', JSON.pretty_generate(commit_history))

  # Adding to branch
  branch_content['commit_pointer'] = commit_hash
  File.write(".stolen-git/branches/#{branch_id}.json", JSON.pretty_generate(branch_content))

  # Print
  puts "#{no_file_changed} files changed, #{no_insertions} insertions(+), #{no_deletions} deletions(-)"
end

#diffObject



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/stg/actions.rb', line 505

def diff
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg diff'

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg diff'
    puts '  -h, --help    Show this help'
    exit 1
  end

  index = read_json('.stolen-git/index.json')
  index.each do |key, value|
    key = clean_path(key)
    new_file_content = File.exist?(key) ? File.read(key) : nil
    file_name = File.basename(key)
    if new_file_content.nil?
      print "@@#{file_name}".cyan
      print " [DELETED]]\n".red
    else
      new_hash = get_file_hash(key)
      next if new_hash == value['hash']

      print "@@#{file_name}".cyan
      blob_content = File.read(".stolen-git/storage/blobs/#{value['hash']}")
      print_diff(blob_content, new_file_content)
    end
  end
end

#logObject



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/stg/actions.rb', line 541

def log
  # Handle if the user wants to -[num]
  ARGV.map! do |arg|
    if arg =~ /^-(\d+)$/
      ['-l', ::Regexp.last_match(1)]
    else
      arg
    end
  end.flatten!

  options = { limit: 0 }
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg log [limit]'

      opts.on('-l', '--limit LIMIT', 'limit showed logs') do |num|
        options[:limit] = num.to_i
      end

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg log [limit]'
    puts '  -[num], --limit   limit showed logs i.e. stg log -5'
    puts '  -l, --limit   limit showed logs'
    puts '  -h, --help    Show this help'
    exit 1
  end

  is_limited = options[:limit] > 0
  pointer = read_json('.stolen-git/pointer.json')
  last_commit = pointer['type'] == 'commit' ? pointer['point_to'] : read_json(".stolen-git/branches/#{pointer['point_to']}.json")['commit_pointer']
  unless last_commit
    puts "Couldn't find last commits. The setup might have been corrupted. If that's the case run 'stg init'"
  end
  i = 0

  print_commit = lambda do |commit_hash, commit_content|
    puts
    puts "commit #{commit_content['id']}".green
    puts "hash:  #{commit_hash[0..5]}...#{commit_hash[-5..]}".green
    puts "Author #{commit_content['author_profile']['username']} <#{commit_content['author_profile']['email']}>"
    puts "Date: #{commit_content['created_at']}"
    puts
    puts "    #{commit_content['name']}"
  end

  while (is_limited && i < options[:limit] || !is_limited) && last_commit && last_commit.length.positive?
    last_commit_content =  read_json(".stolen-git/commits/#{last_commit}.json")
    print_commit.call(last_commit, last_commit_content)
    last_commit = last_commit_content['parent_commit']
    i += 1
    if !is_limited && i >= 5
      q = ask(':')
      break if q == 'q'
    end
  end
end

#p_initializeObject



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/stg/actions.rb', line 11

def p_initialize
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg init'

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg init'
    puts '  -h, --help    Show this help'
    exit 1
  end

  # Check if already initialized
  if File.exist?('.stolen-git')
    if confirm?('An instance of stolen-git is already up here do you want to replace it')
      if confirm?('THIS WILL DELETE ALL COMMITS AND INSTANCES OF stolen-git. ARE YOU SURE')
        FileUtils.rm_rf('.stolen-git')

        if File.exist?('.stolen-git')
          puts 'An error occured during the deletion process of the old directory of stolen-git'
        else
          p_initialize
        end
      else
        puts 'ok'
      end
    else
      puts 'ok'
    end

  else
    # main dot directory
    FileUtils.mkdir_p('.stolen-git')

    # Sub Directories
    FileUtils.mkdir_p('.stolen-git/commits')
    FileUtils.mkdir_p('.stolen-git/branches')

    FileUtils.mkdir_p('.stolen-git/storage')
    FileUtils.mkdir_p('.stolen-git/storage/blobs')
    FileUtils.mkdir_p('.stolen-git/storage/trees')

    # main files
    File.write('.stolen-git/project_info.json', {})
    File.write('.stg-ignore', JSON.pretty_generate(['.*/']))
    File.write('.stolen-git/commits.json', JSON.pretty_generate({ commits: [] }))
    File.write('.stolen-git/index.json', {})

    main_branch_id = SecureRandom.uuid
    File.write(".stolen-git/branches/#{main_branch_id}.json",
               JSON.pretty_generate({ name: 'main', created_at: Time.now, commit_pointer: '' }))

    File.write('.stolen-git/pointer.json', JSON.pretty_generate({ point_to: main_branch_id, type: 'branch' }))
    puts 'Stolen-git initialized Sucessfully :D'
  end
end

#resetObject



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/stg/actions.rb', line 354

def reset
  reset_usage = lambda do
    puts 'Usage: stg reset <commit_id>'
    puts "You can find commit_id by running 'stg log'"
  end

  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg reset <commit_id>'

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    reset_usage.call
    puts '  -h, --help    Show this help'
    exit 1
  end

  if ARGV.length > 1
    reset_usage.call
    return
  end

  commit_id = ARGV.first
  commit_history = read_json('.stolen-git/commits.json')
  pointer = read_json('.stolen-git/pointer.json')
  branch_id = pointer['point_to']
  branch_content = read_json(".stolen-git/branches/#{branch_id}.json")
  if commit_id.nil? || commit_id.empty?
    revert_to_index
    return
  end

  commit = commit_history['commits'].find { |x| x['id'] == commit_id }
  new_commit = commit && commit['hash']
  if new_commit.nil? || new_commit.empty?
    reset_usage.call
    return
  end

  revert_to_commit(new_commit)
  branch_content['commit_pointer'] = new_commit
  File.write(".stolen-git/branches/#{branch_id}.json", JSON.pretty_generate(branch_content))
end

#stageObject



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/stg/actions.rb', line 73

def stage
  begin
    OptionParser.new do |opts|
      opts.banner = 'Usage: stg stage <file> [files...]'

      opts.on_tail('-h', '--help', 'Show this help') do
        puts opts
        exit
      end
    end.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts 'Usage: stg stage <file> [files...]'
    puts '  -h, --help    Show this help'
    exit 1
  end

  inp = ARGV

  if inp.empty?
    puts 'Usage: stg stage <file> [files...]'
    return
  end

  index = read_json('.stolen-git/index.json')
  ignore = read_json('.stg-ignore')
  index.reject! { |key, _value| ignored_path?(key, ignore) }

  path_in_directory = lambda do |path, dir_path|
    dir_path == '.' || path == dir_path || path.start_with?("#{dir_path}/")
  end

  stage_deleted = lambda do |path|
    path = clean_path(path)

    index.keys.each do |indexed_path|
      next unless indexed_path == path || path_in_directory.call(indexed_path, path)
      next if File.exist?(indexed_path)

      index.delete(indexed_path)
    end
  end

  stage_file = lambda do |file_path|
    file_path = clean_path(file_path)
    return if ignored_path?(file_path, ignore)

    file_hash = get_file_hash(file_path)
    file_content = File.read(file_path)

    next if index[file_path] && index[file_path]['hash'] == file_hash

    # Create blob
    File.write(".stolen-git/storage/blobs/#{file_hash}", file_content)

    # Assign index_obj
    default_index_obj = {}
    index[file_path] ||= default_index_obj
    index[file_path]['hash'] = file_hash
  end

  stage_directory = lambda do |dir_path|
    dir_path = clean_path(dir_path)
    return if ignored_path?(dir_path, ignore)

    stage_deleted.call(dir_path)

    Dir.children(dir_path).each do |entry|
      next if entry == '.stolen-git'

      path = File.join(dir_path, entry)
      if File.file?(path)
        stage_file.call(path)
      else
        stage_directory.call(path)
      end
    end
  end

  inp.each do |inp_path|
    inp_path = clean_path(inp_path)
    unless File.exist? inp_path
      if index.key?(inp_path) || index.keys.any? { |key| path_in_directory.call(key, inp_path) }
        stage_deleted.call(inp_path)
      else
        puts "#{inp_path} doesn't exist"
      end
      next
    end

    if File.file?(inp_path)
      stage_file.call(inp_path)
    elsif File.directory? inp_path
      stage_directory.call(inp_path)
    else
      puts "Error logging #{inp_path}. It's neither a file or a directory"
    end
  end
  index = index.sort.to_h
  File.write('.stolen-git/index.json', JSON.pretty_generate(index))
end