git-crecord 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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