gemstar 1.0.2 → 1.1
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/CHANGELOG.md +39 -1
- data/README.md +28 -3
- data/bin/gemstar +5 -1
- data/lib/gemstar/cache_warmer.rb +93 -35
- data/lib/gemstar/change_log.rb +337 -31
- data/lib/gemstar/cli.rb +5 -1
- data/lib/gemstar/commands/diff.rb +197 -31
- data/lib/gemstar/commands/server.rb +94 -10
- data/lib/gemstar/data/importmap_package_metadata.json +22 -0
- data/lib/gemstar/data/ruby_gems_metadata.json +9 -0
- data/lib/gemstar/git_repo.rb +41 -3
- data/lib/gemstar/importmap_file.rb +193 -0
- data/lib/gemstar/lock_file.rb +91 -9
- data/lib/gemstar/npm_metadata.rb +159 -0
- data/lib/gemstar/outputs/html.rb +53 -4
- data/lib/gemstar/outputs/markdown.rb +29 -3
- data/lib/gemstar/package_lock_file.rb +101 -0
- data/lib/gemstar/project.rb +325 -37
- data/lib/gemstar/ruby_gems_metadata.rb +77 -2
- data/lib/gemstar/version.rb +1 -2
- data/lib/gemstar/web/app.rb +418 -70
- data/lib/gemstar/web/templates/app.css +40 -5
- data/lib/gemstar/web/templates/app.js.erb +119 -15
- data/lib/gemstar/web/templates/page.html.erb +2 -1
- data/lib/gemstar.rb +3 -0
- metadata +7 -2
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "command"
|
|
4
4
|
require "concurrent-ruby"
|
|
5
5
|
require "tmpdir"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "uri"
|
|
6
8
|
|
|
7
9
|
module Gemstar
|
|
8
10
|
module Commands
|
|
@@ -16,39 +18,39 @@ module Gemstar
|
|
|
16
18
|
attr_reader :lockfile_full_path
|
|
17
19
|
attr_reader :output_file
|
|
18
20
|
attr_reader :output_format
|
|
21
|
+
attr_reader :project
|
|
22
|
+
attr_reader :ecosystem
|
|
23
|
+
attr_reader :since
|
|
24
|
+
attr_reader :considered_commits
|
|
25
|
+
attr_reader :since_cutoff_commit
|
|
19
26
|
|
|
20
27
|
def initialize(options)
|
|
21
28
|
super
|
|
22
29
|
|
|
23
30
|
@debug_gem_regex = Regexp.new(options[:debug_gem_regex] || ENV["GEMSTAR_DEBUG_GEM_REGEX"] || ".*")
|
|
24
31
|
|
|
32
|
+
@since = normalize_since(options[:since])
|
|
25
33
|
@from = options[:from] || "HEAD"
|
|
26
34
|
@to = options[:to]
|
|
27
35
|
@lockfile = options[:lockfile] || "Gemfile.lock"
|
|
28
36
|
@output_format = normalize_output_format(options[:format] || options[:output_format])
|
|
29
37
|
@output_file = options[:output_file] || default_output_file
|
|
38
|
+
@project = options[:project] ? Gemstar::Project.from_cli_argument(options[:project]) : nil
|
|
39
|
+
@ecosystem = normalize_ecosystem(options[:ecosystem])
|
|
40
|
+
@considered_commits = []
|
|
41
|
+
@since_cutoff_commit = nil
|
|
30
42
|
|
|
31
|
-
@git_repo = Gemstar::GitRepo.new(File.dirname(@lockfile))
|
|
43
|
+
@git_repo = project ? project.git_repo : Gemstar::GitRepo.new(File.dirname(@lockfile))
|
|
44
|
+
@from = resolve_since_commit if since
|
|
32
45
|
end
|
|
33
46
|
|
|
34
47
|
def run
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#+++ edit_gitignore?
|
|
38
|
-
|
|
39
|
-
@lockfile_full_path = git_repo.get_full_path(File.basename(lockfile))
|
|
40
|
-
puts "Lockfile path: #{lockfile_full_path}"
|
|
41
|
-
|
|
42
|
-
old = LockFile.new(content: git_repo.show_blob_at(@from, lockfile_full_path))
|
|
43
|
-
new = @to ?
|
|
44
|
-
LockFile.new(content: git_repo.show_blob_at(@to, lockfile_full_path)) :
|
|
45
|
-
LockFile.new(path: lockfile)
|
|
46
|
-
|
|
47
|
-
collect_updates(new_lockfile: new, old_lockfile: old)
|
|
48
|
+
project ? run_project_diff : run_lockfile_diff
|
|
49
|
+
@considered_commits = collect_considered_commits
|
|
48
50
|
|
|
49
51
|
rendered_output = output_renderer.render_diff(self)
|
|
50
52
|
File.write(output_file, rendered_output)
|
|
51
|
-
puts "✅ Changelog report created: #{
|
|
53
|
+
puts "✅ Changelog report created: #{output_file_url}"
|
|
52
54
|
|
|
53
55
|
if failed.any?
|
|
54
56
|
puts "\n⚠️ The following gems failed to process:"
|
|
@@ -65,11 +67,46 @@ module Gemstar
|
|
|
65
67
|
:html
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def normalize_ecosystem(value)
|
|
71
|
+
normalized = value.to_s.strip.downcase
|
|
72
|
+
return "all" if normalized.empty?
|
|
73
|
+
return normalized if %w[all gems js].include?(normalized)
|
|
74
|
+
|
|
75
|
+
raise Thor::Error, "Unsupported ecosystem #{value.inspect}. Expected one of: all, gems, js"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_since(value)
|
|
79
|
+
normalized = value.to_s.strip
|
|
80
|
+
return nil if normalized.empty?
|
|
81
|
+
|
|
82
|
+
if @options[:from].to_s.strip != ""
|
|
83
|
+
raise Thor::Error, "--since cannot be combined with --from"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
normalized.match?(/\bago\z/i) ? normalized : "#{normalized} ago"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolve_since_commit
|
|
90
|
+
commit = git_repo.commit_before(since)
|
|
91
|
+
@since_cutoff_commit = git_repo.commit_info(commit)
|
|
92
|
+
commit
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def log_since_cutoff
|
|
96
|
+
return unless since
|
|
97
|
+
|
|
98
|
+
puts "Since cutoff: #{since} -> #{format_commit(since_cutoff_commit, fallback_revision: from)}"
|
|
99
|
+
end
|
|
100
|
+
|
|
68
101
|
def default_output_file
|
|
69
102
|
extension = output_format == :markdown ? "md" : "html"
|
|
70
103
|
File.join(Dir.tmpdir, "gem_update_changelog.#{extension}")
|
|
71
104
|
end
|
|
72
105
|
|
|
106
|
+
def output_file_url
|
|
107
|
+
"file://#{URI::DEFAULT_PARSER.escape(File.expand_path(output_file))}"
|
|
108
|
+
end
|
|
109
|
+
|
|
73
110
|
def output_renderer
|
|
74
111
|
@output_renderer ||= case output_format
|
|
75
112
|
when :markdown
|
|
@@ -79,8 +116,42 @@ module Gemstar
|
|
|
79
116
|
end
|
|
80
117
|
end
|
|
81
118
|
|
|
82
|
-
def
|
|
83
|
-
|
|
119
|
+
def collect_considered_commits
|
|
120
|
+
git_repo.commits_between(from, commit_log_to_revision)
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
warn "Could not collect considered commits: #{e.message}"
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def format_commit(commit, fallback_revision:)
|
|
127
|
+
return fallback_revision.to_s if commit.nil?
|
|
128
|
+
|
|
129
|
+
date = commit[:authored_at].to_s.split("T").first
|
|
130
|
+
label = [commit[:short_sha] || commit[:id], commit[:subject]].compact.join(" ")
|
|
131
|
+
date.empty? ? label : "#{label} (#{date})"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
public :format_commit
|
|
135
|
+
|
|
136
|
+
def commit_log_to_revision
|
|
137
|
+
return "HEAD" if to.nil? || to == "worktree"
|
|
138
|
+
|
|
139
|
+
to
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def project_name
|
|
143
|
+
return project.name if project
|
|
144
|
+
|
|
145
|
+
Pathname.getwd.basename.to_s
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
public :project_name
|
|
149
|
+
|
|
150
|
+
def build_entry(package_state:)
|
|
151
|
+
package_name = package_state[:name]
|
|
152
|
+
old_version = package_state[:old_version]
|
|
153
|
+
new_version = package_state[:new_version]
|
|
154
|
+
metadata = metadata_for(package_state)
|
|
84
155
|
repo_url = metadata.repo_uri
|
|
85
156
|
changelog = Gemstar::ChangeLog.new(metadata)
|
|
86
157
|
sections = changelog.extract_relevant_sections(old_version, new_version)
|
|
@@ -102,14 +173,17 @@ module Gemstar
|
|
|
102
173
|
end
|
|
103
174
|
end
|
|
104
175
|
|
|
105
|
-
homepage_url = metadata.meta["homepage_uri"] || metadata.meta["source_code_uri"] || "https://rubygems.org/gems/#{
|
|
176
|
+
homepage_url = metadata.meta["homepage_uri"] || metadata.meta["source_code_uri"] || "https://rubygems.org/gems/#{package_name}"
|
|
106
177
|
description = metadata.meta["info"]
|
|
107
178
|
|
|
108
179
|
entry = {
|
|
109
180
|
old: old_version,
|
|
110
181
|
new: new_version,
|
|
111
182
|
homepage_url: homepage_url,
|
|
112
|
-
description: description
|
|
183
|
+
description: description,
|
|
184
|
+
package_scope: package_state[:package_scope],
|
|
185
|
+
package_type_label: package_state[:package_type_label],
|
|
186
|
+
version_label: package_state[:version_label]
|
|
113
187
|
}
|
|
114
188
|
entry[:sections] = sections unless sections.nil? || sections.empty?
|
|
115
189
|
entry[:compare_url] = compare_url if compare_url
|
|
@@ -133,29 +207,81 @@ module Gemstar
|
|
|
133
207
|
entry
|
|
134
208
|
end
|
|
135
209
|
|
|
136
|
-
def
|
|
210
|
+
def metadata_for(package_state)
|
|
211
|
+
if package_state[:package_scope] == "js"
|
|
212
|
+
Gemstar::NpmMetadata.new(package_state[:name])
|
|
213
|
+
else
|
|
214
|
+
Gemstar::RubyGemsMetadata.new(package_state[:name])
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def run_lockfile_diff
|
|
219
|
+
validate_lockfile_ecosystem!
|
|
220
|
+
|
|
221
|
+
@lockfile_full_path = git_repo.get_full_path(File.basename(lockfile))
|
|
222
|
+
puts "Lockfile path: #{lockfile_full_path}"
|
|
223
|
+
log_since_cutoff
|
|
224
|
+
|
|
225
|
+
old = LockFile.new(content: git_repo.show_blob_at(@from, lockfile_full_path))
|
|
226
|
+
new = @to ?
|
|
227
|
+
LockFile.new(content: git_repo.show_blob_at(@to, lockfile_full_path)) :
|
|
228
|
+
LockFile.new(path: lockfile)
|
|
229
|
+
|
|
230
|
+
collect_lockfile_updates(new_lockfile: new, old_lockfile: old)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def run_project_diff
|
|
234
|
+
puts "Project path: #{project.directory}"
|
|
235
|
+
log_since_cutoff
|
|
236
|
+
|
|
237
|
+
changed_states = project.gem_states(from_revision_id: from, to_revision_id: to || "worktree")
|
|
238
|
+
.select { |package_state| include_package_state?(package_state) }
|
|
239
|
+
.reject { |package_state| package_state[:status] == :unchanged }
|
|
240
|
+
changed_states = disambiguate_duplicate_names(changed_states)
|
|
241
|
+
collect_project_updates(changed_states)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def collect_lockfile_updates(new_lockfile:, old_lockfile:)
|
|
245
|
+
package_states = new_lockfile.specs.keys.sort.map do |gem_name|
|
|
246
|
+
old_version = old_lockfile.specs[gem_name]
|
|
247
|
+
new_version = new_lockfile.specs[gem_name]
|
|
248
|
+
next if old_version == new_version
|
|
249
|
+
|
|
250
|
+
{
|
|
251
|
+
name: gem_name,
|
|
252
|
+
display_name: gem_name,
|
|
253
|
+
package_scope: "gems",
|
|
254
|
+
package_type_label: "Gem",
|
|
255
|
+
old_version: old_version,
|
|
256
|
+
new_version: new_version,
|
|
257
|
+
version_label: version_label(old_version, new_version)
|
|
258
|
+
}
|
|
259
|
+
end.compact
|
|
260
|
+
|
|
261
|
+
collect_project_updates(package_states)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def collect_project_updates(package_states)
|
|
137
265
|
@updates = {}
|
|
138
266
|
@failed = []
|
|
139
267
|
mutex = Mutex.new
|
|
140
268
|
pool = Concurrent::FixedThreadPool.new(10)
|
|
141
269
|
|
|
142
|
-
|
|
270
|
+
package_states.each do |package_state|
|
|
143
271
|
pool.post do
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
old_version = old_lockfile.specs[gem_name]
|
|
147
|
-
new_version = new_lockfile.specs[gem_name]
|
|
148
|
-
next if old_version == new_version
|
|
272
|
+
package_name = package_state[:name]
|
|
273
|
+
next unless @debug_gem_regex.match?(package_name)
|
|
149
274
|
|
|
150
|
-
puts "#{
|
|
275
|
+
puts "#{package_state[:display_name] || package_name} (#{package_state[:version_label]})..."
|
|
151
276
|
|
|
152
277
|
begin
|
|
153
|
-
entry = build_entry(
|
|
278
|
+
entry = build_entry(package_state: package_state)
|
|
279
|
+
display_name = package_state[:display_name] || package_name
|
|
154
280
|
|
|
155
|
-
mutex.synchronize { updates[
|
|
281
|
+
mutex.synchronize { updates[display_name] = entry }
|
|
156
282
|
rescue => e
|
|
157
|
-
mutex.synchronize { failed << [
|
|
158
|
-
puts "⚠️ Failed to process #{
|
|
283
|
+
mutex.synchronize { failed << [package_name, e.message] }
|
|
284
|
+
puts "⚠️ Failed to process #{package_name}: #{e.message}"
|
|
159
285
|
end
|
|
160
286
|
end
|
|
161
287
|
end
|
|
@@ -165,6 +291,46 @@ module Gemstar
|
|
|
165
291
|
|
|
166
292
|
@updates = updates
|
|
167
293
|
end
|
|
294
|
+
|
|
295
|
+
def include_package_state?(package_state)
|
|
296
|
+
ecosystem == "all" || package_state[:package_scope] == ecosystem
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def validate_lockfile_ecosystem!
|
|
300
|
+
return if %w[all gems].include?(ecosystem)
|
|
301
|
+
|
|
302
|
+
raise Thor::Error, "--ecosystem=#{ecosystem} requires --project because lockfile mode only supports gems"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def disambiguate_duplicate_names(package_states)
|
|
306
|
+
counts = package_states.each_with_object(Hash.new(0)) do |package_state, index|
|
|
307
|
+
index[package_state[:name]] += 1
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
package_states.map do |package_state|
|
|
311
|
+
next package_state.merge(display_name: package_state[:name]) if counts[package_state[:name]] == 1
|
|
312
|
+
|
|
313
|
+
suffix = case package_state[:package_source_file]
|
|
314
|
+
when :importmap
|
|
315
|
+
"importmap"
|
|
316
|
+
when :package_lock
|
|
317
|
+
"package-lock"
|
|
318
|
+
else
|
|
319
|
+
package_state[:package_scope]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
package_state.merge(display_name: "#{package_state[:name]} (#{suffix})")
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def version_label(old_version, new_version)
|
|
327
|
+
return "new → #{new_version}" if old_version.nil? && !new_version.nil?
|
|
328
|
+
return "#{old_version} → removed" if !old_version.nil? && new_version.nil?
|
|
329
|
+
return new_version.to_s if old_version == new_version
|
|
330
|
+
|
|
331
|
+
"#{old_version} → #{new_version}"
|
|
332
|
+
end
|
|
333
|
+
|
|
168
334
|
end
|
|
169
335
|
end
|
|
170
336
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require_relative "command"
|
|
2
|
+
require "socket"
|
|
2
3
|
require "shellwords"
|
|
4
|
+
require "rbconfig"
|
|
3
5
|
|
|
4
6
|
module Gemstar
|
|
5
7
|
module Commands
|
|
@@ -14,14 +16,18 @@ module Gemstar
|
|
|
14
16
|
attr_reader :port
|
|
15
17
|
attr_reader :project_inputs
|
|
16
18
|
attr_reader :reload
|
|
19
|
+
attr_reader :open_browser
|
|
20
|
+
attr_reader :explicit_port
|
|
17
21
|
|
|
18
22
|
def initialize(options)
|
|
19
23
|
super
|
|
20
24
|
|
|
21
25
|
@bind = options[:bind] || DEFAULT_BIND
|
|
26
|
+
@explicit_port = !options[:port].nil?
|
|
22
27
|
@port = (options[:port] || DEFAULT_PORT).to_i
|
|
23
28
|
@project_inputs = normalize_project_inputs(options[:project])
|
|
24
29
|
@reload = options[:reload]
|
|
30
|
+
@open_browser = options[:open]
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def run
|
|
@@ -34,10 +40,11 @@ module Gemstar
|
|
|
34
40
|
require "gemstar/web/app"
|
|
35
41
|
|
|
36
42
|
Gemstar::Config.ensure_home_directory!
|
|
43
|
+
@port = resolve_port
|
|
37
44
|
|
|
38
45
|
projects = load_projects
|
|
39
46
|
log_loaded_projects(projects)
|
|
40
|
-
cache_warmer =
|
|
47
|
+
cache_warmer = build_cache_warmer
|
|
41
48
|
app = Gemstar::Web::App.build(projects: projects, config_home: Gemstar::Config.home_directory, cache_warmer: cache_warmer)
|
|
42
49
|
app = Gemstar::RequestLogger.new(app, io: $stderr) if debug_request_logging?
|
|
43
50
|
|
|
@@ -45,10 +52,12 @@ module Gemstar
|
|
|
45
52
|
puts "Config home: #{Gemstar::Config.home_directory}"
|
|
46
53
|
Rackup::Server.start(
|
|
47
54
|
app: app,
|
|
55
|
+
server: "webrick",
|
|
48
56
|
Host: bind,
|
|
49
57
|
Port: port,
|
|
50
58
|
AccessLog: [],
|
|
51
|
-
Logger: Gemstar::WEBrickLogger.new($stderr, WEBrick::BasicLog::INFO)
|
|
59
|
+
Logger: Gemstar::WEBrickLogger.new($stderr, WEBrick::BasicLog::INFO),
|
|
60
|
+
StartCallback: server_start_callback(projects, cache_warmer)
|
|
52
61
|
)
|
|
53
62
|
end
|
|
54
63
|
|
|
@@ -121,9 +130,10 @@ module Gemstar
|
|
|
121
130
|
def server_arguments_without_reload
|
|
122
131
|
args = [
|
|
123
132
|
"server",
|
|
124
|
-
"--bind", bind
|
|
125
|
-
"--port", port.to_s
|
|
133
|
+
"--bind", bind
|
|
126
134
|
]
|
|
135
|
+
args += ["--port", port.to_s] if explicit_port
|
|
136
|
+
args << "--open" if open_browser
|
|
127
137
|
project_inputs.each do |project|
|
|
128
138
|
args << "--project"
|
|
129
139
|
args << project
|
|
@@ -146,14 +156,88 @@ module Gemstar
|
|
|
146
156
|
ENV["DEBUG"] == "1"
|
|
147
157
|
end
|
|
148
158
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
project.current_lockfile&.specs&.keys || []
|
|
152
|
-
end.uniq.sort
|
|
159
|
+
def resolve_port
|
|
160
|
+
return port if explicit_port
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
find_available_port(starting_at: port)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def find_available_port(starting_at:, limit: 100)
|
|
166
|
+
starting_at.upto(starting_at + limit - 1) do |candidate|
|
|
167
|
+
return candidate if port_available?(candidate)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
raise Thor::Error, "No available port found from #{starting_at} to #{starting_at + limit - 1}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def port_available?(candidate)
|
|
174
|
+
server = TCPServer.new(bind, candidate)
|
|
175
|
+
server.close
|
|
176
|
+
true
|
|
177
|
+
rescue Errno::EADDRINUSE, Errno::EACCES, SocketError
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def build_cache_warmer
|
|
182
|
+
Gemstar::CacheWarmer.new(io: $stderr, debug: debug_request_logging? || Gemstar.debug?, thread_count: 10)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def start_background_cache_refresh(projects, cache_warmer)
|
|
186
|
+
package_states = projects.flat_map do |project|
|
|
187
|
+
project.gem_states(from_revision_id: "worktree", to_revision_id: "worktree")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
return nil if package_states.empty?
|
|
191
|
+
|
|
192
|
+
cache_warmer.enqueue_many(package_states)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def server_start_callback(projects, cache_warmer)
|
|
196
|
+
proc do
|
|
197
|
+
Thread.new do
|
|
198
|
+
sleep 0.15
|
|
199
|
+
start_background_cache_refresh(projects, cache_warmer)
|
|
200
|
+
launch_browser
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def launch_browser
|
|
206
|
+
return unless open_browser
|
|
207
|
+
|
|
208
|
+
command = browser_command(root_url)
|
|
209
|
+
return unless command
|
|
210
|
+
|
|
211
|
+
pid = spawn(*command, out: File::NULL, err: File::NULL)
|
|
212
|
+
Process.detach(pid)
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
warn "Could not open browser automatically: #{e.message}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def browser_command(url)
|
|
218
|
+
host_os = RbConfig::CONFIG["host_os"].to_s
|
|
219
|
+
|
|
220
|
+
if host_os.include?("darwin")
|
|
221
|
+
[find_executable("open") || "/usr/bin/open", url]
|
|
222
|
+
elsif host_os.match?(/linux|bsd/)
|
|
223
|
+
executable = find_executable("xdg-open")
|
|
224
|
+
executable ? [executable, url] : nil
|
|
225
|
+
elsif host_os.match?(/mswin|mingw|cygwin/)
|
|
226
|
+
["cmd", "/c", "start", "", url]
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def find_executable(name)
|
|
231
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
|
|
232
|
+
candidate = File.join(directory, name)
|
|
233
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
155
238
|
|
|
156
|
-
|
|
239
|
+
def root_url
|
|
240
|
+
"http://#{bind}:#{port}/"
|
|
157
241
|
end
|
|
158
242
|
end
|
|
159
243
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@rails/actioncable": {
|
|
3
|
+
"provider_gem": "actioncable",
|
|
4
|
+
"repo_url": "https://github.com/rails/rails"
|
|
5
|
+
},
|
|
6
|
+
"@rails/actiontext": {
|
|
7
|
+
"provider_gem": "actiontext",
|
|
8
|
+
"repo_url": "https://github.com/rails/rails"
|
|
9
|
+
},
|
|
10
|
+
"@rails/activestorage": {
|
|
11
|
+
"provider_gem": "activestorage",
|
|
12
|
+
"repo_url": "https://github.com/rails/rails"
|
|
13
|
+
},
|
|
14
|
+
"@hotwired/stimulus-loading": {
|
|
15
|
+
"provider_gem": "stimulus-rails",
|
|
16
|
+
"repo_url": "https://github.com/hotwired/stimulus-rails"
|
|
17
|
+
},
|
|
18
|
+
"@hotwired/turbo-rails": {
|
|
19
|
+
"provider_gem": "turbo-rails",
|
|
20
|
+
"repo_url": "https://github.com/hotwired/turbo-rails"
|
|
21
|
+
}
|
|
22
|
+
}
|
data/lib/gemstar/git_repo.rb
CHANGED
|
@@ -54,9 +54,7 @@ module Gemstar
|
|
|
54
54
|
# If it looks like a pure date (or you want to support "date only"),
|
|
55
55
|
# map it to "latest commit before date on default_branch".
|
|
56
56
|
if revish =~ /\d{4}-\d{2}-\d{2}/ || revish =~ /\d{1,2}:\d{2}/i
|
|
57
|
-
|
|
58
|
-
raise "No commit before #{revish} on #{default_branch}" if sha.empty?
|
|
59
|
-
return sha
|
|
57
|
+
return commit_before(revish, default_branch:)
|
|
60
58
|
end
|
|
61
59
|
|
|
62
60
|
# Otherwise let Git parse whatever the user typed.
|
|
@@ -65,6 +63,13 @@ module Gemstar
|
|
|
65
63
|
sha
|
|
66
64
|
end
|
|
67
65
|
|
|
66
|
+
def commit_before(time_expression, default_branch: "HEAD")
|
|
67
|
+
sha = run_git_command(["rev-list", "-1", "--before", time_expression, default_branch])
|
|
68
|
+
raise "No commit before #{time_expression} on #{default_branch}" if sha.empty?
|
|
69
|
+
|
|
70
|
+
sha
|
|
71
|
+
end
|
|
72
|
+
|
|
68
73
|
def show_blob_at(revish, path)
|
|
69
74
|
commit = resolve_commit(revish)
|
|
70
75
|
run_git_command(["show", "#{commit}:#{path}"])
|
|
@@ -101,8 +106,41 @@ module Gemstar
|
|
|
101
106
|
run_git_command(command, in_directory: tree_root_directory)
|
|
102
107
|
end
|
|
103
108
|
|
|
109
|
+
def commits_between(from_revision, to_revision = "HEAD")
|
|
110
|
+
return [] if tree_root_directory.nil? || tree_root_directory.empty?
|
|
111
|
+
|
|
112
|
+
range = "#{from_revision}..#{to_revision || "HEAD"}"
|
|
113
|
+
format = "%H%x1f%h%x1f%aI%x1f%s"
|
|
114
|
+
output = try_git_command(["log", "--reverse", "--pretty=format:#{format}", range], in_directory: tree_root_directory)
|
|
115
|
+
return [] if output.nil? || output.empty?
|
|
116
|
+
|
|
117
|
+
output.lines.filter_map { |line| parse_commit_log_line(line) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def commit_info(revision)
|
|
121
|
+
return nil if tree_root_directory.nil? || tree_root_directory.empty?
|
|
122
|
+
|
|
123
|
+
format = "%H%x1f%h%x1f%aI%x1f%s"
|
|
124
|
+
output = try_git_command(["show", "-s", "--pretty=format:#{format}", revision], in_directory: tree_root_directory)
|
|
125
|
+
return nil if output.nil? || output.empty?
|
|
126
|
+
|
|
127
|
+
parse_commit_log_line(output)
|
|
128
|
+
end
|
|
129
|
+
|
|
104
130
|
private
|
|
105
131
|
|
|
132
|
+
def parse_commit_log_line(line)
|
|
133
|
+
full_sha, short_sha, authored_at, subject = line.strip.split("\u001f", 4)
|
|
134
|
+
return nil if full_sha.nil? || full_sha.empty?
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
id: full_sha,
|
|
138
|
+
short_sha: short_sha,
|
|
139
|
+
authored_at: authored_at,
|
|
140
|
+
subject: subject
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
106
144
|
def normalize_remote_url(remote)
|
|
107
145
|
normalized = remote.strip.sub(%r{\.git\z}, "")
|
|
108
146
|
|