git-crecord 1.1.1 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75901ead4962d50c7cc5a3e838a1da7ded9e9b69c1b01ae4297ded807ff51000
4
- data.tar.gz: b01fd7ae94ea47f59464d0c2f293a0c97f19995ab2bb18f86e8c9e9884e3c9a7
3
+ metadata.gz: 301253d2b7495fed7ae9dc2c148b04bee0bb9241419b99d863c872e40e95eec9
4
+ data.tar.gz: 1b5aef42231723add9c71296d88bdc39a899133c7ec561bf6704a2edd2bd17c8
5
5
  SHA512:
6
- metadata.gz: d840cdce7161d29f8025698a13e4e469506e80332b6ffa47a49b0cb85c8106fd7dae42a92929931e2ab45b7e3596d4ce2d3978a974b909d87c7b341385b4222d
7
- data.tar.gz: 74deb70c563068660856b8783f90967561947935d19dc955b463ea2f2dcfa155c5d8a5ff84c4488ac70f8ce1d6507f9e6cbaed179c7cd2f5645521005a1d3200
6
+ metadata.gz: d0803b8dd1714df6b4b7e2382e2cd688bbd71739706eab5560b2683a7c4d2214dede1a265a24aee70e5667a605bc05c73224c47714b1c63967e9980fbcc7d43d
7
+ data.tar.gz: b563990aaaea1b8feff2e2e7d1403d230d5e048e3bb9c3584e8506b6a1b52ff031753e495e816e63f6b05d25ea861bb0cf86363733ae5a523b882b3e47848d6e
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ ---
2
+ require: rubocop-performance
3
+
1
4
  Documentation:
2
5
  Enabled: false
3
6
 
data/git-crecord.gemspec CHANGED
@@ -30,4 +30,5 @@ GemSpec = Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency 'minitest', '~> 5.8', '>= 5.8.4'
31
31
  spec.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
32
32
  spec.add_development_dependency 'rubocop', '>= 0.56.0'
33
+ spec.add_development_dependency 'rubocop-performance'
33
34
  end
data/lib/git_crecord.rb CHANGED
@@ -34,10 +34,10 @@ module GitCrecord
34
34
  return false if toplevel_dir.empty?
35
35
 
36
36
  Dir.chdir(toplevel_dir) do
37
- files = Diff.parse(Git.diff(staged: reverse), reverse)
38
- files.concat(Diff.untracked_files(Git.status)) if with_untracked_files
37
+ files = Diff.create(reverse: reverse, untracked: with_untracked_files)
39
38
  return false if files.empty?
40
39
 
40
+ UI::StatusBar.reverse = reverse
41
41
  result = UI.run(files)
42
42
  return result.call(reverse) == true if result.respond_to?(:call)
43
43
 
@@ -1,73 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'diff/file'
4
+ require_relative 'git'
4
5
 
5
6
  module GitCrecord
6
7
  module Diff
7
- def self.parse(diff, reverse = false)
8
- files = []
9
- enum = diff.lines.each
10
- loop do
11
- line = enum.next
12
- line.chomp!
13
- next files << parse_file_head(line, enum, reverse) if file_start?(line)
14
- next files[-1] << line if hunk_start?(line)
8
+ def self.create(reverse: false, untracked: false)
9
+ GitCrecord::Git.status.lines.map do |file_status|
10
+ status = file_status[reverse ? 0 : 1].downcase
11
+ filename = file_status.chomp[3..-1]
12
+ next if status == ' ' || status == '?' && !untracked
15
13
 
16
- files[-1].add_hunk_line(line)
17
- end
18
- files
19
- end
20
-
21
- def self.file_start?(line)
22
- line.start_with?('diff')
23
- end
24
-
25
- def self.hunk_start?(line)
26
- line.start_with?('@@')
27
- end
28
-
29
- def self.parse_file_head(line, enum, reverse)
30
- index_line = enum.next # index ... or new ...
31
- is_new_file = index_line.start_with?('new')
32
- enum.next if is_new_file
33
- enum.next # --- ...
34
- enum.next # +++ ...
35
- type = is_new_file ? :untracked : :modified
36
- File.new(*parse_filenames(line), type: type, reverse: reverse)
14
+ method = "handle_status_#{status}"
15
+ send(method, filename, reverse: reverse) if respond_to?(method)
16
+ end.compact
37
17
  end
38
18
 
39
- def self.parse_filenames(line)
40
- line.match(%r{a/(.*) b/(.*)$})[1..2]
19
+ def self.handle_status_m(filename, reverse: false)
20
+ file = File.new(filename, filename, type: :modified, reverse: reverse)
21
+ diff_lines = Git.diff(filename: filename, staged: reverse).lines[4..-1]
22
+ diff_lines.each do |line|
23
+ handle_line(file, line)
24
+ end
25
+ file
41
26
  end
42
27
 
43
- def self.untracked_files(git_status)
44
- git_status.lines.select { |l| l.start_with?('??') }.flat_map do |path|
45
- path = path.chomp[3..-1]
46
- ::File.directory?(path) ? untracked_dir(path) : untracked_file(path)
47
- end.compact
28
+ def self.handle_status_a(filename, reverse: false)
29
+ file = File.new(filename, filename, type: :new, reverse: reverse)
30
+ diff_lines = Git.diff(filename: filename, staged: reverse).lines[5..-1]
31
+ file.make_empty if diff_lines.nil?
32
+ (diff_lines || []).each do |line|
33
+ handle_line(file, line)
34
+ end
35
+ file
48
36
  end
49
37
 
50
- def self.untracked_file(filename)
38
+ def self.handle_status_?(filename, **_)
51
39
  File.new(filename, filename, type: :untracked).tap do |file|
52
40
  lines, err = file_lines(filename)
53
- file << "@@ -0,0 +1,#{lines.size} @@"
54
- file.subs[0].subs << PseudoLine.new(err) if lines.empty?
55
- lines.each { |line| file.add_hunk_line("+#{line.chomp}") }
41
+ if lines.empty?
42
+ file.make_empty(err)
43
+ else
44
+ file << "@@ -0,0 +1,#{lines.size} @@"
45
+ lines.each { |line| file.add_hunk_line("+#{line.chomp}") }
46
+ end
56
47
  file.selected = false
57
48
  end
58
49
  end
59
50
 
60
- def self.untracked_dir(path)
61
- Dir.glob(::File.join(path, '**/*')).map do |filename|
62
- untracked_file(filename) unless ::File.directory?(filename)
51
+ # o ' ' = unmodified
52
+ # o M = modified
53
+ # o A = added
54
+ # o D = deleted
55
+ # o R = renamed
56
+ # o C = copied
57
+ # o U = updated but unmerged
58
+
59
+ def self.handle_line(file, line)
60
+ line.chomp!
61
+ if hunk_start?(line)
62
+ file << line
63
+ else
64
+ file.add_hunk_line(line)
63
65
  end
64
66
  end
65
67
 
68
+ def self.hunk_start?(line)
69
+ line.start_with?('@@')
70
+ end
71
+
66
72
  def self.file_encoding(filename)
67
73
  `file --mime-encoding #{filename}`.split(': ', 2)[1].chomp
68
74
  end
69
75
 
70
76
  def self.file_lines(filename)
77
+ return [[], 'empty'] if ::File.size(filename).zero?
78
+
71
79
  encoding = file_encoding(filename)
72
80
  return [[], 'binary'] if encoding == 'binary'
73
81
 
@@ -27,6 +27,7 @@ module GitCrecord
27
27
  @reverse = reverse
28
28
  @selection_marker_map = reverse ? REVERSE_SELECTED_MAP : SELECTED_MAP
29
29
  @subs = []
30
+ @selected = true
30
31
  end
31
32
 
32
33
  def strings(width)
@@ -53,6 +54,8 @@ module GitCrecord
53
54
  end
54
55
 
55
56
  def selected
57
+ return @selected if selectable_subs.empty?
58
+
56
59
  s = selectable_subs.map(&:selected).uniq
57
60
  return s[0] if s.size == 1
58
61
 
@@ -60,7 +63,11 @@ module GitCrecord
60
63
  end
61
64
 
62
65
  def selected=(value)
63
- selectable_subs.each { |sub| sub.selected = value }
66
+ if selectable_subs.empty?
67
+ @selected = value
68
+ else
69
+ selectable_subs.each { |sub| sub.selected = value }
70
+ end
64
71
  end
65
72
 
66
73
  def style(is_highlighted)
@@ -19,7 +19,7 @@ module GitCrecord
19
19
  end
20
20
 
21
21
  def to_s
22
- prefix = { modified: 'M', untracked: '?' }.fetch(type)
22
+ prefix = { modified: 'M', new: 'A', untracked: '?' }.fetch(type)
23
23
  return "#{prefix} #{@filename_a}" if @filename_a == @filename_b
24
24
 
25
25
  "#{prefix} #{filename_a} -> #{filename_b}"
@@ -68,6 +68,31 @@ module GitCrecord
68
68
  end
69
69
 
70
70
  alias prefix_style style
71
+
72
+ def make_empty(type = 'empty')
73
+ subs << PseudoLine.new(type)
74
+ end
75
+
76
+ def empty?
77
+ selectable_subs.empty?
78
+ end
79
+
80
+ def stage_steps
81
+ case type
82
+ when :modified then %i[stage]
83
+ when :new then empty? ? %i[add_file_full] : %i[stage]
84
+ when :untracked then empty? ? %i[add_file_full] : %i[add_file stage]
85
+ else raise "unknown file type - #{type.inspect}"
86
+ end
87
+ end
88
+
89
+ def unstage_steps
90
+ case type
91
+ when :modified then %i[unstage]
92
+ when :new then %i[unstage]
93
+ else raise "unknown file type - #{type.inspect}"
94
+ end
95
+ end
71
96
  end
72
97
  end
73
98
  end
@@ -6,12 +6,10 @@ require_relative '../ui/color'
6
6
  module GitCrecord
7
7
  module Diff
8
8
  class PseudoLine < Difference
9
- attr_accessor :selected
10
-
11
9
  def initialize(line)
12
10
  @line = line || 'file is empty'
13
- @selected = false
14
11
  super()
12
+ @selected = false
15
13
  end
16
14
 
17
15
  def to_s
@@ -23,7 +21,7 @@ module GitCrecord
23
21
  end
24
22
 
25
23
  def selectable?
26
- true
24
+ false
27
25
  end
28
26
 
29
27
  def expanded
@@ -40,11 +38,8 @@ module GitCrecord
40
38
  end
41
39
 
42
40
  class Line < Difference
43
- attr_reader :selected
44
-
45
41
  def initialize(line, reverse: false)
46
42
  @line = line
47
- @selected = true
48
43
  super(reverse: reverse)
49
44
  end
50
45
 
@@ -5,16 +5,29 @@ require 'open3'
5
5
 
6
6
  module GitCrecord
7
7
  module Git
8
- def self.stage(files, reverse = false)
9
- selected_files = files.select(&:selected)
10
- untracked_files = selected_files.select { |file| file.type == :untracked }
11
- add_files(untracked_files) unless reverse
12
- diff = selected_files.map(&:generate_diff).join("\n")
13
- status = _stage(diff, reverse).success?
14
- return status unless reverse
15
-
16
- reset_files(untracked_files.select { |file| file.selected == true })
17
- true
8
+ def self.stage_files(files, reverse = false)
9
+ method_name = reverse ? :unstage_steps : :stage_steps
10
+ success = true
11
+ files.each do |file|
12
+ next unless file.selected
13
+
14
+ file.send(method_name).each do |step|
15
+ success &&= send(step, file)
16
+ end
17
+ end
18
+ success
19
+ end
20
+
21
+ def self.stage(file)
22
+ _stage(file.generate_diff, false)
23
+ end
24
+
25
+ def self.unstage(file)
26
+ success = _stage(file.generate_diff, true)
27
+ return false unless success
28
+ return true unless file.type == :new && file.selected == true
29
+
30
+ reset_file(file)
18
31
  end
19
32
 
20
33
  def self._stage(diff, reverse = false)
@@ -26,47 +39,43 @@ module GitCrecord
26
39
  LOGGER.info('stdout/stderr:')
27
40
  LOGGER.info(content)
28
41
  LOGGER.info("return code: #{status}")
29
- status
42
+ status.success?
30
43
  end
31
44
 
32
- def self.add_files(files)
33
- files.each do |file|
34
- success = add_file(file.filename_a)
35
- raise "could not add file #{file.filename_a}" unless success
36
- end
37
- end
38
-
39
- def self.add_file(filename)
40
- system("git add -N #{filename}")
45
+ def self.add_file(file)
46
+ system("git add -N #{file.filename_a}")
41
47
  end
42
48
 
43
- def self.reset_files(files)
44
- files.each do |file|
45
- success = reset_file(file.filename_a)
46
- raise "could not reset file #{file.filename_a}" unless success
47
- end
49
+ def self.add_file_full(file)
50
+ system("git add #{file.filename_a}")
48
51
  end
49
52
 
50
- def self.reset_file(filename)
51
- system("git reset -q #{filename}")
53
+ def self.reset_file(file)
54
+ system("git reset -q #{file.filename_a}")
52
55
  end
53
56
 
54
57
  def self.status
55
- `git status --porcelain`
58
+ `git status --porcelain --untracked-files=all`
56
59
  end
57
60
 
58
61
  def self.commit
59
62
  exec('git commit')
60
63
  end
61
64
 
62
- def self.diff(staged: false)
63
- `git diff --no-ext-diff --no-color -D #{staged ? '--staged' : ''}`
65
+ def self.diff(filename: nil, staged: false)
66
+ filename = "'#{filename}'" if filename
67
+ staged_option = staged ? '--staged' : ''
68
+ `git diff --no-ext-diff --no-color -D #{staged_option} #{filename}`
64
69
  end
65
70
 
66
71
  def self.toplevel_dir
67
72
  `git rev-parse --show-toplevel`.chomp
68
73
  end
69
74
 
75
+ def self.branch
76
+ `git rev-parse --abbrev-ref HEAD`.chomp
77
+ end
78
+
70
79
  def self.tab
71
80
  @tab ||= ' ' * [2, `git config crecord.tabwidth`.to_i].max
72
81
  end
@@ -3,6 +3,7 @@
3
3
  require 'curses'
4
4
  require_relative 'ui/color'
5
5
  require_relative 'ui/hunks_window'
6
+ require_relative 'ui/status_bar'
6
7
 
7
8
  module GitCrecord
8
9
  module UI
@@ -44,6 +45,7 @@ module GitCrecord
44
45
 
45
46
  def self.run_loop(win)
46
47
  loop do
48
+ StatusBar.refresh(win)
47
49
  c = win.getch
48
50
  next if ACTIONS[c].nil?
49
51
 
@@ -9,7 +9,8 @@ module GitCrecord
9
9
  normal: 1,
10
10
  green: 2,
11
11
  red: 3,
12
- hl: 4
12
+ hl: 4,
13
+ status_bar: 5
13
14
  }.freeze
14
15
 
15
16
  def self.init
@@ -19,6 +20,9 @@ module GitCrecord
19
20
  Curses.init_pair(MAP[:green], Curses::COLOR_GREEN, -1)
20
21
  Curses.init_pair(MAP[:red], Curses::COLOR_RED, -1)
21
22
  Curses.init_pair(MAP[:hl], Curses::COLOR_BLACK, Curses::COLOR_GREEN)
23
+ Curses.init_pair(
24
+ MAP[:status_bar], Curses::COLOR_BLACK, Curses::COLOR_BLUE
25
+ )
22
26
  end
23
27
 
24
28
  MAP.each_pair do |name, number|
@@ -25,8 +25,12 @@ module GitCrecord
25
25
  delegate getch: :@win
26
26
  def_delegator :@win, :maxx, :width
27
27
 
28
+ def highlight_position
29
+ "#{@visibles.index(@highlighted) + 1}/#{@visibles.size}"
30
+ end
31
+
28
32
  def refresh
29
- @win.refresh(scroll_position, 0, 0, 0, Curses.lines - 1, width)
33
+ @win.refresh(scroll_position, 0, 1, 0, Curses.lines - 1, width)
30
34
  end
31
35
 
32
36
  def redraw
@@ -37,7 +41,7 @@ module GitCrecord
37
41
 
38
42
  def resize
39
43
  new_width = Curses.cols
40
- new_height = [Curses.lines, content_height(new_width)].max
44
+ new_height = [Curses.lines - 1, content_height(new_width)].max
41
45
  return if width == new_width && @win.maxy == new_height
42
46
 
43
47
  @win.resize(new_height, new_width)
@@ -106,11 +110,13 @@ module GitCrecord
106
110
  end
107
111
 
108
112
  def stage
109
- QuitAction.new { |reverse| Git.stage(@files, reverse) }
113
+ QuitAction.new { |reverse| Git.stage_files(@files, reverse) }
110
114
  end
111
115
 
112
116
  def commit
113
- QuitAction.new { |reverse| Git.stage(@files, reverse) && Git.commit }
117
+ QuitAction.new do |reverse|
118
+ Git.stage_files(@files, reverse) && Git.commit
119
+ end
114
120
  end
115
121
 
116
122
  def highlight_next
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'curses'
4
+ require_relative 'color'
5
+ require_relative '../git'
6
+
7
+ module GitCrecord
8
+ module UI
9
+ module StatusBar
10
+ def self.refresh(main_win)
11
+ write_left(main_win)
12
+ fill_to_eol
13
+ write_right(main_win)
14
+ win.refresh
15
+ end
16
+
17
+ def self.write_left(_main_win)
18
+ win.setpos(0, 0)
19
+ win.addstr(" #{branch}")
20
+ end
21
+
22
+ def self.write_right(main_win)
23
+ str = " #{reverse ? '[reverse]' : ''} #{main_win.highlight_position} "
24
+ win.setpos(0, [0, win.maxx - str.size].max)
25
+ win.addstr(str)
26
+ end
27
+
28
+ def self.fill_to_eol
29
+ fill_width = win.maxx - win.curx
30
+ win.addstr(' ' * fill_width) if fill_width.positive?
31
+ end
32
+
33
+ def self.win
34
+ @win ||= Curses::Window.new(1, Curses.cols, 0, 0).tap do |win|
35
+ win.attrset(Color.status_bar | Curses::A_BOLD)
36
+ end
37
+ end
38
+
39
+ def self.branch
40
+ @branch = Git.branch
41
+ end
42
+
43
+ def self.reverse
44
+ @reverse
45
+ end
46
+
47
+ def self.reverse=(reverse)
48
+ @reverse = reverse
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitCrecord
4
- VERSION = '1.1.1'
4
+ VERSION = '1.2.0'
5
5
  end
data/test/system-test.sh CHANGED
@@ -196,6 +196,29 @@ assert-diff '-This is the second line.
196
196
  +This is line 2.
197
197
  +new line2'
198
198
 
199
+ echo "test add empty files ----------------------------------------------------"
200
+ git add .
201
+ touch empty.txt
202
+ touch empty-untrached.txt
203
+ git add -N empty.txt
204
+ run-git-crecord 'AAs'
205
+ assert-status 'M a_file.txt
206
+ M b_file.txt
207
+ A empty-untrached.txt
208
+ A empty.txt
209
+ A new.txt
210
+ A sub/sub2/sub-file.txt'
211
+
212
+ echo "unstage empty file ------------------------------------------------------"
213
+ run-git-crecord-reverse 'AG s'
214
+ assert-status 'M a_file.txt
215
+ M b_file.txt
216
+ A empty-untrached.txt
217
+ A empty.txt
218
+ A new.txt
219
+ ?? sub/'
220
+
221
+
199
222
  popd > /dev/null # $REPO_DIR
200
223
 
201
224
  cat << 'EOF'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-crecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maik Brendler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-19 00:00:00.000000000 Z
11
+ date: 2019-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: curses
@@ -78,6 +78,20 @@ dependencies:
78
78
  - - ">="
79
79
  - !ruby/object:Gem::Version
80
80
  version: 0.56.0
81
+ - !ruby/object:Gem::Dependency
82
+ name: rubocop-performance
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
81
95
  description: This gem adds the git-crecord command. It provides a curses UI to stage/commit
82
96
  git-hunks.
83
97
  email: maik.brendler@invision.de
@@ -107,6 +121,7 @@ files:
107
121
  - lib/git_crecord/ui/color.rb
108
122
  - lib/git_crecord/ui/help_window.rb
109
123
  - lib/git_crecord/ui/hunks_window.rb
124
+ - lib/git_crecord/ui/status_bar.rb
110
125
  - lib/git_crecord/version.rb
111
126
  - screenshot.jpg
112
127
  - test/git_crecord/diff/hunk_test.rb