spoom 1.1.11 → 1.1.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +6 -0
- data/Rakefile +1 -0
- data/lib/spoom/cli/bump.rb +17 -12
- data/lib/spoom/cli/coverage.rb +20 -17
- data/lib/spoom/cli/helper.rb +6 -5
- data/lib/spoom/cli/lsp.rb +4 -3
- data/lib/spoom/cli/run.rb +18 -8
- data/lib/spoom/cli.rb +6 -4
- data/lib/spoom/context.rb +219 -0
- data/lib/spoom/coverage/d3/base.rb +12 -8
- data/lib/spoom/coverage/d3/circle_map.rb +40 -37
- data/lib/spoom/coverage/d3/pie.rb +41 -30
- data/lib/spoom/coverage/d3/timeline.rb +95 -88
- data/lib/spoom/coverage/d3.rb +72 -70
- data/lib/spoom/coverage/report.rb +6 -6
- data/lib/spoom/coverage/snapshot.rb +65 -34
- data/lib/spoom/coverage.rb +104 -81
- data/lib/spoom/file_tree.rb +5 -3
- data/lib/spoom/git.rb +102 -96
- data/lib/spoom/printer.rb +4 -0
- data/lib/spoom/sorbet/errors.rb +30 -12
- data/lib/spoom/sorbet/lsp/base.rb +1 -1
- data/lib/spoom/sorbet/lsp/errors.rb +23 -17
- data/lib/spoom/sorbet/lsp/structures.rb +86 -55
- data/lib/spoom/sorbet/lsp.rb +78 -70
- data/lib/spoom/sorbet/metrics.rb +18 -16
- data/lib/spoom/sorbet/sigils.rb +59 -54
- data/lib/spoom/sorbet.rb +17 -14
- data/lib/spoom/timeline.rb +6 -5
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +43 -25
- metadata +20 -20
- data/lib/spoom/test_helpers/project.rb +0 -138
data/lib/spoom/sorbet/lsp.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require "open3"
|
5
|
+
require "json"
|
6
6
|
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
7
|
+
require_relative "lsp/base"
|
8
|
+
require_relative "lsp/structures"
|
9
|
+
require_relative "lsp/errors"
|
10
10
|
|
11
11
|
module Spoom
|
12
12
|
module LSP
|
@@ -58,10 +58,10 @@ module Spoom
|
|
58
58
|
json = JSON.parse(raw_string)
|
59
59
|
|
60
60
|
# Handle error in the LSP protocol
|
61
|
-
raise ResponseError.from_json(json[
|
61
|
+
raise ResponseError.from_json(json["error"]) if json["error"]
|
62
62
|
|
63
63
|
# Handle typechecking errors
|
64
|
-
raise Error::Diagnostics.from_json(json[
|
64
|
+
raise Error::Diagnostics.from_json(json["params"]) if json["method"] == "textDocument/publishDiagnostics"
|
65
65
|
|
66
66
|
json
|
67
67
|
end
|
@@ -71,16 +71,17 @@ module Spoom
|
|
71
71
|
sig { params(workspace_path: String).void }
|
72
72
|
def open(workspace_path)
|
73
73
|
raise Error::AlreadyOpen, "Error: CLI already opened" if @open
|
74
|
+
|
74
75
|
send(Request.new(
|
75
76
|
next_id,
|
76
|
-
|
77
|
+
"initialize",
|
77
78
|
{
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
"rootPath" => workspace_path,
|
80
|
+
"rootUri" => "file://#{workspace_path}",
|
81
|
+
"capabilities" => {},
|
81
82
|
},
|
82
83
|
))
|
83
|
-
send(Notification.new(
|
84
|
+
send(Notification.new("initialized", {}))
|
84
85
|
@open = true
|
85
86
|
end
|
86
87
|
|
@@ -88,133 +89,140 @@ module Spoom
|
|
88
89
|
def hover(uri, line, column)
|
89
90
|
json = send(Request.new(
|
90
91
|
next_id,
|
91
|
-
|
92
|
+
"textDocument/hover",
|
92
93
|
{
|
93
|
-
|
94
|
-
|
94
|
+
"textDocument" => {
|
95
|
+
"uri" => uri,
|
95
96
|
},
|
96
|
-
|
97
|
-
|
98
|
-
|
97
|
+
"position" => {
|
98
|
+
"line" => line,
|
99
|
+
"character" => column,
|
99
100
|
},
|
100
|
-
}
|
101
|
+
},
|
101
102
|
))
|
102
103
|
|
103
|
-
return nil unless json && json[
|
104
|
-
|
104
|
+
return nil unless json && json["result"]
|
105
|
+
|
106
|
+
Hover.from_json(json["result"])
|
105
107
|
end
|
106
108
|
|
107
109
|
sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[SignatureHelp]) }
|
108
110
|
def signatures(uri, line, column)
|
109
111
|
json = send(Request.new(
|
110
112
|
next_id,
|
111
|
-
|
113
|
+
"textDocument/signatureHelp",
|
112
114
|
{
|
113
|
-
|
114
|
-
|
115
|
+
"textDocument" => {
|
116
|
+
"uri" => uri,
|
115
117
|
},
|
116
|
-
|
117
|
-
|
118
|
-
|
118
|
+
"position" => {
|
119
|
+
"line" => line,
|
120
|
+
"character" => column,
|
119
121
|
},
|
120
|
-
}
|
122
|
+
},
|
121
123
|
))
|
122
124
|
|
123
|
-
return [] unless json && json[
|
124
|
-
|
125
|
+
return [] unless json && json["result"] && json["result"]["signatures"]
|
126
|
+
|
127
|
+
json["result"]["signatures"].map { |loc| SignatureHelp.from_json(loc) }
|
125
128
|
end
|
126
129
|
|
127
130
|
sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[Location]) }
|
128
131
|
def definitions(uri, line, column)
|
129
132
|
json = send(Request.new(
|
130
133
|
next_id,
|
131
|
-
|
134
|
+
"textDocument/definition",
|
132
135
|
{
|
133
|
-
|
134
|
-
|
136
|
+
"textDocument" => {
|
137
|
+
"uri" => uri,
|
135
138
|
},
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
+
"position" => {
|
140
|
+
"line" => line,
|
141
|
+
"character" => column,
|
139
142
|
},
|
140
|
-
}
|
143
|
+
},
|
141
144
|
))
|
142
145
|
|
143
|
-
return [] unless json && json[
|
144
|
-
|
146
|
+
return [] unless json && json["result"]
|
147
|
+
|
148
|
+
json["result"].map { |loc| Location.from_json(loc) }
|
145
149
|
end
|
146
150
|
|
147
151
|
sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[Location]) }
|
148
152
|
def type_definitions(uri, line, column)
|
149
153
|
json = send(Request.new(
|
150
154
|
next_id,
|
151
|
-
|
155
|
+
"textDocument/typeDefinition",
|
152
156
|
{
|
153
|
-
|
154
|
-
|
157
|
+
"textDocument" => {
|
158
|
+
"uri" => uri,
|
155
159
|
},
|
156
|
-
|
157
|
-
|
158
|
-
|
160
|
+
"position" => {
|
161
|
+
"line" => line,
|
162
|
+
"character" => column,
|
159
163
|
},
|
160
|
-
}
|
164
|
+
},
|
161
165
|
))
|
162
166
|
|
163
|
-
return [] unless json && json[
|
164
|
-
|
167
|
+
return [] unless json && json["result"]
|
168
|
+
|
169
|
+
json["result"].map { |loc| Location.from_json(loc) }
|
165
170
|
end
|
166
171
|
|
167
172
|
sig { params(uri: String, line: Integer, column: Integer, include_decl: T::Boolean).returns(T::Array[Location]) }
|
168
173
|
def references(uri, line, column, include_decl = true)
|
169
174
|
json = send(Request.new(
|
170
175
|
next_id,
|
171
|
-
|
176
|
+
"textDocument/references",
|
172
177
|
{
|
173
|
-
|
174
|
-
|
178
|
+
"textDocument" => {
|
179
|
+
"uri" => uri,
|
175
180
|
},
|
176
|
-
|
177
|
-
|
178
|
-
|
181
|
+
"position" => {
|
182
|
+
"line" => line,
|
183
|
+
"character" => column,
|
179
184
|
},
|
180
|
-
|
181
|
-
|
185
|
+
"context" => {
|
186
|
+
"includeDeclaration" => include_decl,
|
182
187
|
},
|
183
|
-
}
|
188
|
+
},
|
184
189
|
))
|
185
190
|
|
186
|
-
return [] unless json && json[
|
187
|
-
|
191
|
+
return [] unless json && json["result"]
|
192
|
+
|
193
|
+
json["result"].map { |loc| Location.from_json(loc) }
|
188
194
|
end
|
189
195
|
|
190
196
|
sig { params(query: String).returns(T::Array[DocumentSymbol]) }
|
191
197
|
def symbols(query)
|
192
198
|
json = send(Request.new(
|
193
199
|
next_id,
|
194
|
-
|
200
|
+
"workspace/symbol",
|
195
201
|
{
|
196
|
-
|
197
|
-
}
|
202
|
+
"query" => query,
|
203
|
+
},
|
198
204
|
))
|
199
205
|
|
200
|
-
return [] unless json && json[
|
201
|
-
|
206
|
+
return [] unless json && json["result"]
|
207
|
+
|
208
|
+
json["result"].map { |loc| DocumentSymbol.from_json(loc) }
|
202
209
|
end
|
203
210
|
|
204
211
|
sig { params(uri: String).returns(T::Array[DocumentSymbol]) }
|
205
212
|
def document_symbols(uri)
|
206
213
|
json = send(Request.new(
|
207
214
|
next_id,
|
208
|
-
|
215
|
+
"textDocument/documentSymbol",
|
209
216
|
{
|
210
|
-
|
211
|
-
|
217
|
+
"textDocument" => {
|
218
|
+
"uri" => uri,
|
212
219
|
},
|
213
|
-
}
|
220
|
+
},
|
214
221
|
))
|
215
222
|
|
216
|
-
return [] unless json && json[
|
217
|
-
|
223
|
+
return [] unless json && json["result"]
|
224
|
+
|
225
|
+
json["result"].map { |loc| DocumentSymbol.from_json(loc) }
|
218
226
|
end
|
219
227
|
|
220
228
|
sig { void }
|
data/lib/spoom/sorbet/metrics.rb
CHANGED
@@ -6,26 +6,28 @@ require_relative "sigils"
|
|
6
6
|
module Spoom
|
7
7
|
module Sorbet
|
8
8
|
module MetricsParser
|
9
|
-
extend T::Sig
|
10
|
-
|
11
9
|
DEFAULT_PREFIX = "ruby_typer.unknown.."
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
parse_string(File.read(path), prefix)
|
16
|
-
end
|
11
|
+
class << self
|
12
|
+
extend T::Sig
|
17
13
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
|
15
|
+
def parse_file(path, prefix = DEFAULT_PREFIX)
|
16
|
+
parse_string(File.read(path), prefix)
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
|
20
|
+
def parse_string(string, prefix = DEFAULT_PREFIX)
|
21
|
+
parse_hash(JSON.parse(string), prefix)
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
|
25
|
+
def parse_hash(obj, prefix = DEFAULT_PREFIX)
|
26
|
+
obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
|
27
|
+
name = metric["name"]
|
28
|
+
name = name.sub(prefix, "")
|
29
|
+
metrics[name] = metric["value"] || 0
|
30
|
+
end
|
29
31
|
end
|
30
32
|
end
|
31
33
|
end
|
data/lib/spoom/sorbet/sigils.rb
CHANGED
@@ -27,70 +27,75 @@ module Spoom
|
|
27
27
|
|
28
28
|
SIGIL_REGEXP = T.let(/^#[\ t]*typed[\ t]*:[ \t]*(\w*)[ \t]*/.freeze, Regexp)
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
def self.sigil_string(strictness)
|
33
|
-
"# typed: #{strictness}"
|
34
|
-
end
|
30
|
+
class << self
|
31
|
+
extend T::Sig
|
35
32
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
# returns the full sigil comment string for the passed strictness
|
34
|
+
sig { params(strictness: String).returns(String) }
|
35
|
+
def sigil_string(strictness)
|
36
|
+
"# typed: #{strictness}"
|
37
|
+
end
|
41
38
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
# returns true if the passed string is a valid strictness (else false)
|
40
|
+
sig { params(strictness: String).returns(T::Boolean) }
|
41
|
+
def valid_strictness?(strictness)
|
42
|
+
VALID_STRICTNESS.include?(strictness.strip)
|
43
|
+
end
|
47
44
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
45
|
+
# returns the strictness of a sigil in the passed file content string (nil if no sigil)
|
46
|
+
sig { params(content: String).returns(T.nilable(String)) }
|
47
|
+
def strictness_in_content(content)
|
48
|
+
SIGIL_REGEXP.match(content)&.[](1)
|
49
|
+
end
|
53
50
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
content = File.read(path, encoding: Encoding::ASCII_8BIT)
|
60
|
-
strictness_in_content(content)
|
61
|
-
end
|
51
|
+
# returns a string which is the passed content but with the sigil updated to a new strictness
|
52
|
+
sig { params(content: String, new_strictness: String).returns(String) }
|
53
|
+
def update_sigil(content, new_strictness)
|
54
|
+
content.sub(SIGIL_REGEXP, sigil_string(new_strictness))
|
55
|
+
end
|
62
56
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
57
|
+
# returns a string containing the strictness of a sigil in a file at the passed path
|
58
|
+
# * returns nil if no sigil
|
59
|
+
sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
|
60
|
+
def file_strictness(path)
|
61
|
+
return nil unless File.file?(path)
|
68
62
|
|
69
|
-
|
63
|
+
content = File.read(path, encoding: Encoding::ASCII_8BIT)
|
64
|
+
strictness_in_content(content)
|
65
|
+
end
|
70
66
|
|
71
|
-
|
72
|
-
|
67
|
+
# changes the sigil in the file at the passed path to the specified new strictness
|
68
|
+
sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
|
69
|
+
def change_sigil_in_file(path, new_strictness)
|
70
|
+
content = File.read(path, encoding: Encoding::ASCII_8BIT)
|
71
|
+
new_content = update_sigil(content, new_strictness)
|
73
72
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
path_list.filter do |path|
|
78
|
-
change_sigil_in_file(path, new_strictness)
|
73
|
+
File.write(path, new_content, encoding: Encoding::ASCII_8BIT)
|
74
|
+
|
75
|
+
strictness_in_content(new_content) == new_strictness
|
79
76
|
end
|
80
|
-
end
|
81
77
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
78
|
+
# changes the sigil to have a new strictness in a list of files
|
79
|
+
sig { params(path_list: T::Array[String], new_strictness: String).returns(T::Array[String]) }
|
80
|
+
def change_sigil_in_files(path_list, new_strictness)
|
81
|
+
path_list.filter do |path|
|
82
|
+
change_sigil_in_file(path, new_strictness)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# finds all files in the specified directory with the passed strictness
|
87
|
+
sig do
|
88
|
+
params(
|
89
|
+
directory: T.any(String, Pathname),
|
90
|
+
strictness: String,
|
91
|
+
extension: String,
|
92
|
+
).returns(T::Array[String])
|
93
|
+
end
|
94
|
+
def files_with_sigil_strictness(directory, strictness, extension: ".rb")
|
95
|
+
paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
|
96
|
+
paths.filter do |path|
|
97
|
+
file_strictness(path) == strictness
|
98
|
+
end
|
94
99
|
end
|
95
100
|
end
|
96
101
|
end
|
data/lib/spoom/sorbet.rb
CHANGED
@@ -25,10 +25,10 @@ module Spoom
|
|
25
25
|
arg: String,
|
26
26
|
path: String,
|
27
27
|
capture_err: T::Boolean,
|
28
|
-
sorbet_bin: T.nilable(String)
|
28
|
+
sorbet_bin: T.nilable(String),
|
29
29
|
).returns(ExecResult)
|
30
30
|
end
|
31
|
-
def srb(*arg, path:
|
31
|
+
def srb(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
32
32
|
if sorbet_bin
|
33
33
|
arg.prepend(sorbet_bin)
|
34
34
|
else
|
@@ -42,20 +42,20 @@ module Spoom
|
|
42
42
|
arg: String,
|
43
43
|
path: String,
|
44
44
|
capture_err: T::Boolean,
|
45
|
-
sorbet_bin: T.nilable(String)
|
45
|
+
sorbet_bin: T.nilable(String),
|
46
46
|
).returns(ExecResult)
|
47
47
|
end
|
48
|
-
def srb_tc(*arg, path:
|
48
|
+
def srb_tc(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
49
49
|
arg.prepend("tc") unless sorbet_bin
|
50
50
|
srb(*T.unsafe(arg), path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
|
51
51
|
end
|
52
52
|
|
53
53
|
# List all files typechecked by Sorbet from its `config`
|
54
54
|
sig { params(config: Config, path: String).returns(T::Array[String]) }
|
55
|
-
def srb_files(config, path:
|
55
|
+
def srb_files(config, path: ".")
|
56
56
|
regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
|
57
|
-
exts = config.allowed_extensions.empty? ? [
|
58
|
-
Dir.glob((Pathname.new(path) / "**/*{#{exts.join(
|
57
|
+
exts = config.allowed_extensions.empty? ? [".rb", ".rbi"] : config.allowed_extensions
|
58
|
+
Dir.glob((Pathname.new(path) / "**/*{#{exts.join(",")}}").to_s).reject do |f|
|
59
59
|
regs.any? { |re| re.match?(f) }
|
60
60
|
end.sort
|
61
61
|
end
|
@@ -65,19 +65,20 @@ module Spoom
|
|
65
65
|
arg: String,
|
66
66
|
path: String,
|
67
67
|
capture_err: T::Boolean,
|
68
|
-
sorbet_bin: T.nilable(String)
|
68
|
+
sorbet_bin: T.nilable(String),
|
69
69
|
).returns(T.nilable(String))
|
70
70
|
end
|
71
|
-
def srb_version(*arg, path:
|
71
|
+
def srb_version(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
72
72
|
result = T.let(T.unsafe(self).srb_tc(
|
73
73
|
"--no-config",
|
74
74
|
"--version",
|
75
75
|
*arg,
|
76
76
|
path: path,
|
77
77
|
capture_err: capture_err,
|
78
|
-
sorbet_bin: sorbet_bin
|
78
|
+
sorbet_bin: sorbet_bin,
|
79
79
|
), ExecResult)
|
80
80
|
return nil unless result.status
|
81
|
+
|
81
82
|
result.out.split(" ")[2]
|
82
83
|
end
|
83
84
|
|
@@ -86,10 +87,10 @@ module Spoom
|
|
86
87
|
arg: String,
|
87
88
|
path: String,
|
88
89
|
capture_err: T::Boolean,
|
89
|
-
sorbet_bin: T.nilable(String)
|
90
|
+
sorbet_bin: T.nilable(String),
|
90
91
|
).returns(T.nilable(T::Hash[String, Integer]))
|
91
92
|
end
|
92
|
-
def srb_metrics(*arg, path:
|
93
|
+
def srb_metrics(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
93
94
|
metrics_file = "metrics.tmp"
|
94
95
|
metrics_path = "#{path}/#{metrics_file}"
|
95
96
|
T.unsafe(self).srb_tc(
|
@@ -98,7 +99,7 @@ module Spoom
|
|
98
99
|
*arg,
|
99
100
|
path: path,
|
100
101
|
capture_err: capture_err,
|
101
|
-
sorbet_bin: sorbet_bin
|
102
|
+
sorbet_bin: sorbet_bin,
|
102
103
|
)
|
103
104
|
if File.exist?(metrics_path)
|
104
105
|
metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
|
@@ -112,11 +113,13 @@ module Spoom
|
|
112
113
|
#
|
113
114
|
# Returns `nil` if `gem` cannot be found in the Gemfile.
|
114
115
|
sig { params(gem: String, path: String).returns(T.nilable(String)) }
|
115
|
-
def version_from_gemfile_lock(gem:
|
116
|
+
def version_from_gemfile_lock(gem: "sorbet", path: ".")
|
116
117
|
gemfile_path = "#{path}/Gemfile.lock"
|
117
118
|
return nil unless File.exist?(gemfile_path)
|
119
|
+
|
118
120
|
content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
|
119
121
|
return nil unless content
|
122
|
+
|
120
123
|
content[1]
|
121
124
|
end
|
122
125
|
end
|
data/lib/spoom/timeline.rb
CHANGED
@@ -15,7 +15,7 @@ module Spoom
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Return one commit for each month between `from` and `to`
|
18
|
-
sig { returns(T::Array[
|
18
|
+
sig { returns(T::Array[Git::Commit]) }
|
19
19
|
def ticks
|
20
20
|
commits_for_dates(months)
|
21
21
|
end
|
@@ -34,20 +34,21 @@ module Spoom
|
|
34
34
|
end
|
35
35
|
|
36
36
|
# Return one commit for each date in `dates`
|
37
|
-
sig { params(dates: T::Array[Time]).returns(T::Array[
|
37
|
+
sig { params(dates: T::Array[Time]).returns(T::Array[Git::Commit]) }
|
38
38
|
def commits_for_dates(dates)
|
39
39
|
dates.map do |t|
|
40
40
|
result = Spoom::Git.log(
|
41
41
|
"--since='#{t}'",
|
42
42
|
"--until='#{t.to_date.next_month}'",
|
43
|
-
"--format='format:%h'",
|
43
|
+
"--format='format:%h %at'",
|
44
44
|
"--author-date-order",
|
45
45
|
"-1",
|
46
46
|
path: @path,
|
47
47
|
)
|
48
48
|
next if result.out.empty?
|
49
|
-
|
50
|
-
|
49
|
+
|
50
|
+
Git.parse_commit(result.out.strip)
|
51
|
+
end.compact.uniq(&:sha)
|
51
52
|
end
|
52
53
|
end
|
53
54
|
end
|
data/lib/spoom/version.rb
CHANGED
data/lib/spoom.rb
CHANGED
@@ -12,41 +12,59 @@ module Spoom
|
|
12
12
|
class Error < StandardError; end
|
13
13
|
|
14
14
|
class ExecResult < T::Struct
|
15
|
+
extend T::Sig
|
16
|
+
|
15
17
|
const :out, String
|
16
18
|
const :err, String
|
17
19
|
const :status, T::Boolean
|
18
20
|
const :exit_code, Integer
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
sig { returns(String) }
|
23
|
+
def to_s
|
24
|
+
<<~STR
|
25
|
+
########## STDOUT ##########
|
26
|
+
#{out.empty? ? "<empty>" : out}
|
27
|
+
########## STDERR ##########
|
28
|
+
#{err.empty? ? "<empty>" : err}
|
29
|
+
########## STATUS: #{status} ##########
|
30
|
+
STR
|
31
|
+
end
|
28
32
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
status:
|
44
|
-
|
45
|
-
|
33
|
+
|
34
|
+
class << self
|
35
|
+
extend T::Sig
|
36
|
+
|
37
|
+
sig do
|
38
|
+
params(
|
39
|
+
cmd: String,
|
40
|
+
arg: String,
|
41
|
+
path: String,
|
42
|
+
capture_err: T::Boolean,
|
43
|
+
).returns(ExecResult)
|
44
|
+
end
|
45
|
+
def exec(cmd, *arg, path: ".", capture_err: false)
|
46
|
+
if capture_err
|
47
|
+
stdout, stderr, status = T.unsafe(Open3).capture3([cmd, *arg].join(" "), chdir: path)
|
48
|
+
ExecResult.new(
|
49
|
+
out: stdout,
|
50
|
+
err: stderr,
|
51
|
+
status: status.success?,
|
52
|
+
exit_code: status.exitstatus,
|
53
|
+
)
|
54
|
+
else
|
55
|
+
stdout, status = T.unsafe(Open3).capture2([cmd, *arg].join(" "), chdir: path)
|
56
|
+
ExecResult.new(
|
57
|
+
out: stdout,
|
58
|
+
err: "",
|
59
|
+
status: status.success?,
|
60
|
+
exit_code: status.exitstatus,
|
61
|
+
)
|
62
|
+
end
|
46
63
|
end
|
47
64
|
end
|
48
65
|
end
|
49
66
|
|
67
|
+
require "spoom/context"
|
50
68
|
require "spoom/colors"
|
51
69
|
require "spoom/sorbet"
|
52
70
|
require "spoom/cli"
|