spoom 1.1.11 → 1.1.13
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/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"
|