git-status-tree 3.5.1 → 3.7.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 +4 -4
- data/VERSION +1 -1
- data/bin/git-status-tree +19 -4
- data/lib/node.rb +53 -13
- data/lib/node_collapsing.rb +1 -2
- data/src/git_status_tree.rb +112 -4
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 465d1e069565fd9710b3ee6077ccedb08d94ffaf97b413a6f09fc6c18e24e277
|
|
4
|
+
data.tar.gz: 9a04be3d59a43020c81f251e87c18a8c3246bb63d1bd84873db8ba9aacb05a46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f07708afe5c16f2df3131e687ece19887f8ce8321552c5b26c5d3beca3ceab2978111c07ee72081ff2b6cab460f830819002c835146dcf4eacdcaaf6430a5669
|
|
7
|
+
data.tar.gz: eedfc54282fb88c3662f881dcf94d8b2ced92730f77d0e42ad2f3928916361520f769e5a9b06d9c16ea64e9ff6ac7a12887e489a57121536e9818be802a88a42
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.7.0
|
data/bin/git-status-tree
CHANGED
|
@@ -8,7 +8,7 @@ require File.join File.dirname(__FILE__), '../lib/version'
|
|
|
8
8
|
options = {}
|
|
9
9
|
|
|
10
10
|
parser = OptionParser.new do |opts|
|
|
11
|
-
opts.banner = 'Usage: git-status-tree [options]'
|
|
11
|
+
opts.banner = 'Usage: git-status-tree [options] [<commit> [<commit>] | <commit>..<commit>]'
|
|
12
12
|
|
|
13
13
|
opts.on('-i', '--indent INDENT', Integer, 'Set indentation (2-10 spaces)') do |indent|
|
|
14
14
|
options[:indent] = indent
|
|
@@ -36,9 +36,24 @@ end
|
|
|
36
36
|
begin
|
|
37
37
|
parser.parse!
|
|
38
38
|
rescue OptionParser::InvalidOption => e
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
warn "Error: #{e.message}"
|
|
40
|
+
warn parser
|
|
41
41
|
exit 1
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# Residual ARGV holds the positional commit args (options are consumed by
|
|
45
|
+
# parse!). At most two commits are accepted; a single "A..B" range token counts
|
|
46
|
+
# as one positional and is expanded later in GitStatusTree.
|
|
47
|
+
if ARGV.length > 2
|
|
48
|
+
warn 'Error: git tree accepts at most two commits'
|
|
49
|
+
warn parser
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
options[:commits] = ARGV.dup
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
puts GitStatusTree.new(options)
|
|
56
|
+
rescue GitStatusTree::RevisionError => e
|
|
57
|
+
warn e.message
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
data/lib/node.rb
CHANGED
|
@@ -11,7 +11,8 @@ class Node
|
|
|
11
11
|
include NodeCollapsing
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
|
|
14
|
+
# mode is :status (default working-tree view) or :commit (history view).
|
|
15
|
+
attr_accessor :indent, :collapse_dirs, :mode
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
attr_accessor :status, :name, :children
|
|
@@ -129,9 +130,13 @@ class Node
|
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
132
|
-
def file?
|
|
133
|
+
def file?
|
|
134
|
+
children.nil?
|
|
135
|
+
end
|
|
133
136
|
|
|
134
|
-
def dir?
|
|
137
|
+
def dir?
|
|
138
|
+
!file?
|
|
139
|
+
end
|
|
135
140
|
|
|
136
141
|
def valid?
|
|
137
142
|
file? ? valid_file? : valid_dir?
|
|
@@ -168,17 +173,44 @@ class Node
|
|
|
168
173
|
end
|
|
169
174
|
end
|
|
170
175
|
|
|
171
|
-
def modified?
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
def modified?
|
|
177
|
+
status.include?('M')
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def added?
|
|
181
|
+
status.include?('A')
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def deleted?
|
|
185
|
+
status.include?('D')
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def renamed?
|
|
189
|
+
status.include?('R')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def copied?
|
|
193
|
+
status.include?('C')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def unmerged?
|
|
197
|
+
status.include?('U')
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def new?
|
|
201
|
+
status.include?('?')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def staged?
|
|
205
|
+
staged_status?(status)
|
|
206
|
+
end
|
|
179
207
|
|
|
180
208
|
private
|
|
181
209
|
|
|
210
|
+
def staged_status?(status)
|
|
211
|
+
status.include?('+')
|
|
212
|
+
end
|
|
213
|
+
|
|
182
214
|
def validate_name!(name)
|
|
183
215
|
msg = '"name" must be a String.'
|
|
184
216
|
raise NodeNameError, msg unless name.is_a? String
|
|
@@ -196,13 +228,21 @@ class Node
|
|
|
196
228
|
if dir?
|
|
197
229
|
color_name += BashColor::EMB + name
|
|
198
230
|
else
|
|
199
|
-
color_name +=
|
|
200
|
-
color_name += BashColor::R unless staged?
|
|
231
|
+
color_name += status_color(status)
|
|
201
232
|
color_name += "#{name} (#{status})"
|
|
202
233
|
end
|
|
203
234
|
color_name + BashColor::NONE
|
|
204
235
|
end
|
|
205
236
|
|
|
237
|
+
# Commit-mode entries render in a distinct third color (cyan) to signal
|
|
238
|
+
# "history view, not your working tree". Status mode keeps the green=staged /
|
|
239
|
+
# red=dirty palette unchanged. Shared with the collapsed-file renderer.
|
|
240
|
+
def status_color(status)
|
|
241
|
+
return BashColor::C if self.class.mode == :commit
|
|
242
|
+
|
|
243
|
+
staged_status?(status) ? BashColor::G : BashColor::R
|
|
244
|
+
end
|
|
245
|
+
|
|
206
246
|
def name_valid?
|
|
207
247
|
name.is_a?(String) &&
|
|
208
248
|
name.length.positive? &&
|
data/lib/node_collapsing.rb
CHANGED
|
@@ -104,7 +104,6 @@ module NodeCollapsing
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def color_file_with_status(file_name, status)
|
|
107
|
-
|
|
108
|
-
"#{color}#{file_name} (#{status})#{BashColor::NONE}"
|
|
107
|
+
"#{status_color(status)}#{file_name} (#{status})#{BashColor::NONE}"
|
|
109
108
|
end
|
|
110
109
|
end
|
data/src/git_status_tree.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'open3'
|
|
3
4
|
require File.join File.dirname(__FILE__), '../lib/node'
|
|
4
5
|
require File.join File.dirname(__FILE__), '../lib/node_collapsing'
|
|
5
6
|
require File.join File.dirname(__FILE__), '../lib/nodes_collection'
|
|
@@ -8,19 +9,22 @@ require File.join File.dirname(__FILE__), '../lib/version'
|
|
|
8
9
|
|
|
9
10
|
# Main class for generating and displaying git status as a tree structure
|
|
10
11
|
class GitStatusTree
|
|
12
|
+
# Raised when a positional commit argument is not a valid revision.
|
|
13
|
+
class RevisionError < StandardError; end
|
|
14
|
+
|
|
11
15
|
attr_reader :files, :nodes, :tree
|
|
12
16
|
|
|
13
17
|
def initialize(options = {})
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@files =
|
|
18
|
+
commits = Array(options[:commits])
|
|
19
|
+
configure_node(options, commits)
|
|
20
|
+
@files = source_files(commits, options)
|
|
17
21
|
@nodes = files.map { |file| Node.create_from_string file }
|
|
18
22
|
@tree = nodes.reduce { |a, i| (a + i).nodes[0] }
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def to_s
|
|
22
26
|
if tree.nil?
|
|
23
|
-
'(working directory clean)'
|
|
27
|
+
Node.mode == :commit ? '(no changes)' : '(working directory clean)'
|
|
24
28
|
else
|
|
25
29
|
tree.to_tree_s
|
|
26
30
|
end
|
|
@@ -28,6 +32,110 @@ class GitStatusTree
|
|
|
28
32
|
|
|
29
33
|
private
|
|
30
34
|
|
|
35
|
+
def configure_node(options, commits)
|
|
36
|
+
Node.indent = indent(options)
|
|
37
|
+
Node.collapse_dirs = collapse(options)
|
|
38
|
+
Node.mode = commits.empty? ? :status : :commit
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Dispatch on the number of commit args: no commits keeps the working-tree
|
|
42
|
+
# status view; one or two commits switch to the git diff-tree history view.
|
|
43
|
+
def source_files(commits, options)
|
|
44
|
+
if commits.empty?
|
|
45
|
+
parse_status(`git status --porcelain -z#{untracked_files(options)}`)
|
|
46
|
+
else
|
|
47
|
+
parse_diff_tree(diff_tree_output(commits))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def diff_tree_output(commits)
|
|
52
|
+
refs = validate_revs!(commits)
|
|
53
|
+
# -M enables rename detection (no -C: copy detection is off by default in
|
|
54
|
+
# git and is slower/noisier), -r recurses into subtrees, --no-commit-id
|
|
55
|
+
# suppresses the leading SHA line, --root lists every file as added for a
|
|
56
|
+
# root commit (no parent), -z gives unambiguous paths.
|
|
57
|
+
# --diff-merges=first-parent diffs a single merge commit against its first
|
|
58
|
+
# parent (diff-tree skips merges by default, and plain -m diffs against
|
|
59
|
+
# every parent); it is a no-op for a two-commit diff.
|
|
60
|
+
run_git!('diff-tree', '--no-commit-id', '--name-status', '-M', '-r', '--root',
|
|
61
|
+
'--diff-merges=first-parent', '-z', *refs)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Run git with an argv array (no shell, so revisions are never word-split,
|
|
65
|
+
# globbed or interpreted) and raise RevisionError with git's own stderr on
|
|
66
|
+
# failure.
|
|
67
|
+
def run_git!(*args)
|
|
68
|
+
stdout, stderr, status = Open3.capture3('git', *args)
|
|
69
|
+
raise RevisionError, stderr.strip unless status.success?
|
|
70
|
+
|
|
71
|
+
stdout
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Translate diff-tree -z output into the porcelain entry strings the node
|
|
75
|
+
# pipeline already understands. The stream is "<status>\0<path>\0 …", with
|
|
76
|
+
# rename/copy entries spanning a triplet "<status>\0<old>\0<new>\0".
|
|
77
|
+
def parse_diff_tree(raw)
|
|
78
|
+
tokens = raw.force_encoding('UTF-8').split("\0")
|
|
79
|
+
files = []
|
|
80
|
+
index = 0
|
|
81
|
+
while index < tokens.length
|
|
82
|
+
status = tokens[index]
|
|
83
|
+
index += 1
|
|
84
|
+
next if status.nil? || status.empty?
|
|
85
|
+
|
|
86
|
+
index = append_diff_entry(files, tokens, index, status[0])
|
|
87
|
+
end
|
|
88
|
+
files
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Append a single porcelain entry and return the advanced token index.
|
|
92
|
+
# The status letter goes in the porcelain Y column so Node.status reads it
|
|
93
|
+
# verbatim (no staged "+" suffix), and commit-mode rendering colors it cyan.
|
|
94
|
+
def append_diff_entry(files, tokens, index, code)
|
|
95
|
+
if %w[R C].include?(code)
|
|
96
|
+
old = tokens[index]
|
|
97
|
+
new = tokens[index + 1]
|
|
98
|
+
files << " #{code} #{old} -> #{new}"
|
|
99
|
+
index + 2
|
|
100
|
+
else
|
|
101
|
+
files << " #{code} #{tokens[index]}"
|
|
102
|
+
index + 1
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Normalize positional args into resolved SHAs, expanding a single "A..B"
|
|
107
|
+
# token into two endpoints; an omitted side defaults to HEAD, as in git.
|
|
108
|
+
# "A...B" is rejected: diff-tree does not honor symmetric-difference
|
|
109
|
+
# semantics and would silently return nothing.
|
|
110
|
+
def validate_revs!(commits)
|
|
111
|
+
if commits.length == 1 && commits[0].include?('..')
|
|
112
|
+
if commits[0].include?('...')
|
|
113
|
+
raise RevisionError, "fatal: '#{commits[0]}': '...' range is not supported; use 'A..B'"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
commits = commits[0].split('..', 2).map { |rev| rev.empty? ? 'HEAD' : rev }
|
|
117
|
+
end
|
|
118
|
+
commits.map { |rev| validate_rev!(rev) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Resolve first, then peel to a commit in a second step: appending ^{commit}
|
|
122
|
+
# to the user's text would break revisions like ":/message", where git
|
|
123
|
+
# treats everything after ":/" as the search pattern.
|
|
124
|
+
def validate_rev!(rev)
|
|
125
|
+
object = rev_parse(rev, rev)
|
|
126
|
+
rev_parse("#{object}^{commit}", rev)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def rev_parse(spec, rev)
|
|
130
|
+
stdout, stderr, status = Open3.capture3('git', 'rev-parse', '--verify', '--quiet', '--end-of-options', spec)
|
|
131
|
+
sha = stdout.strip
|
|
132
|
+
return sha if status.success? && !sha.empty?
|
|
133
|
+
|
|
134
|
+
# --quiet silences "bad revision"; any stderr left is a real git error
|
|
135
|
+
# (e.g. not a git repository), so surface it instead.
|
|
136
|
+
raise RevisionError, stderr.strip.empty? ? "fatal: bad revision '#{rev}'" : stderr.strip
|
|
137
|
+
end
|
|
138
|
+
|
|
31
139
|
# Parse NUL-terminated `git status --porcelain -z` output into porcelain
|
|
32
140
|
# entry strings. Unlike the default output, -z never quotes or escapes
|
|
33
141
|
# paths, so names with spaces, non-ASCII characters, quotes, backslashes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-status-tree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wolfgang Teuber
|
|
@@ -45,14 +45,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
45
45
|
requirements:
|
|
46
46
|
- - ">="
|
|
47
47
|
- !ruby/object:Gem::Version
|
|
48
|
-
version: '
|
|
48
|
+
version: '2.6'
|
|
49
49
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
54
|
requirements: []
|
|
55
|
-
rubygems_version:
|
|
55
|
+
rubygems_version: 4.0.19
|
|
56
56
|
specification_version: 4
|
|
57
57
|
summary: git status in file tree format
|
|
58
58
|
test_files: []
|