git-status-tree 3.6.0 → 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 +17 -4
- data/lib/node_collapsing.rb +1 -2
- data/src/git_status_tree.rb +112 -4
- metadata +2 -2
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
|
|
@@ -201,11 +202,15 @@ class Node
|
|
|
201
202
|
end
|
|
202
203
|
|
|
203
204
|
def staged?
|
|
204
|
-
|
|
205
|
+
staged_status?(status)
|
|
205
206
|
end
|
|
206
207
|
|
|
207
208
|
private
|
|
208
209
|
|
|
210
|
+
def staged_status?(status)
|
|
211
|
+
status.include?('+')
|
|
212
|
+
end
|
|
213
|
+
|
|
209
214
|
def validate_name!(name)
|
|
210
215
|
msg = '"name" must be a String.'
|
|
211
216
|
raise NodeNameError, msg unless name.is_a? String
|
|
@@ -223,13 +228,21 @@ class Node
|
|
|
223
228
|
if dir?
|
|
224
229
|
color_name += BashColor::EMB + name
|
|
225
230
|
else
|
|
226
|
-
color_name +=
|
|
227
|
-
color_name += BashColor::R unless staged?
|
|
231
|
+
color_name += status_color(status)
|
|
228
232
|
color_name += "#{name} (#{status})"
|
|
229
233
|
end
|
|
230
234
|
color_name + BashColor::NONE
|
|
231
235
|
end
|
|
232
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
|
+
|
|
233
246
|
def name_valid?
|
|
234
247
|
name.is_a?(String) &&
|
|
235
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
|
|
@@ -52,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
54
|
requirements: []
|
|
55
|
-
rubygems_version: 4.0.
|
|
55
|
+
rubygems_version: 4.0.19
|
|
56
56
|
specification_version: 4
|
|
57
57
|
summary: git status in file tree format
|
|
58
58
|
test_files: []
|