gemstar 0.0.2 → 1.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/CHANGELOG.md +6 -14
- data/README.md +13 -0
- data/lib/gemstar/cache.rb +47 -10
- data/lib/gemstar/cache_cli.rb +12 -0
- data/lib/gemstar/cache_warmer.rb +120 -0
- data/lib/gemstar/change_log.rb +75 -44
- data/lib/gemstar/cli.rb +12 -0
- data/lib/gemstar/commands/cache.rb +12 -0
- data/lib/gemstar/commands/diff.rb +3 -3
- data/lib/gemstar/commands/server.rb +136 -0
- data/lib/gemstar/config.rb +15 -0
- data/lib/gemstar/git_repo.rb +74 -7
- data/lib/gemstar/lock_file.rb +86 -8
- data/lib/gemstar/project.rb +245 -0
- data/lib/gemstar/request_logger.rb +31 -0
- data/lib/gemstar/ruby_gems_metadata.rb +49 -33
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +936 -0
- data/lib/gemstar/web/templates/app.css +523 -0
- data/lib/gemstar/web/templates/app.js.erb +226 -0
- data/lib/gemstar/web/templates/page.html.erb +15 -0
- data/lib/gemstar/webrick_logger.rb +22 -0
- data/lib/gemstar.rb +6 -1
- metadata +70 -3
- data/lib/gemstar/railtie.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65596efbc0ff5c10ad950ca3cc8bb19e9ca9030db58b73d046ebce447200aa07
|
|
4
|
+
data.tar.gz: e5ab0cbb7f3db4b3d9949011f3b6446aecb725ee1863fd61dd196660332606de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef75cdf526345f289eff5f0a8bff44520eb1d0aea55eff617de71d585832721655fd70c680e0e27ef7584c15dd4f304d9ee3b079c70fbafc7e4ccc5c8360c4ad
|
|
7
|
+
data.tar.gz: 13e3d871e1772c7f8ec183a3801504123896e8bd5365bba2a3e6fae5668415dcf2a40ef29f3494a2e9bef47251a3b913c37ae122879c3e8367cba9cf4490af1e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
## TODO
|
|
4
|
-
- Diff:
|
|
5
|
-
- Gems:
|
|
6
|
-
- benchmark
|
|
7
|
-
- brakeman
|
|
8
|
-
- json (not using CHANGES.md?)
|
|
9
|
-
- nio4r not using releases.md?
|
|
10
|
-
- parser not using CHANGELOG.md?
|
|
11
|
-
- actioncable-next uses release tag names?
|
|
12
|
-
- paper_trail not using CHANGELOG.md?
|
|
13
|
-
- playwright-ruby-client uses release tags?
|
|
14
|
-
- bundler itself
|
|
15
|
-
- use changelog files from installed gems where present
|
|
16
|
-
|
|
17
3
|
## Unreleased
|
|
18
4
|
|
|
5
|
+
- Added `gemstar server`, your interactive Gemfile.lock explorer and more.
|
|
6
|
+
- Default location for `diff` is now a tmp file.
|
|
7
|
+
- Removed Railtie from this gem.
|
|
8
|
+
- Improve how git root dir is determined.
|
|
9
|
+
|
|
10
|
+
|
|
19
11
|
## 0.0.2
|
|
20
12
|
|
|
21
13
|
- Diff: Fix regex warnings shown in terminal.
|
data/README.md
CHANGED
|
@@ -22,6 +22,19 @@ gem "gemstar", group: :development
|
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
+
### gemstar server
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Start the interactive web UI:
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
gemstar server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
By default, the server listens to http://127.0.0.1:2112/
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
### gemstar diff
|
|
26
39
|
|
|
27
40
|
Run this after you've updated your gems.
|
data/lib/gemstar/cache.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
require_relative "config"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "digest"
|
|
3
4
|
|
|
4
5
|
module Gemstar
|
|
5
6
|
class Cache
|
|
6
7
|
MAX_CACHE_AGE = 60 * 60 * 24 * 7 # 1 week
|
|
7
|
-
CACHE_DIR =
|
|
8
|
+
CACHE_DIR = File.join(Gemstar::Config.home_directory, "cache")
|
|
8
9
|
|
|
9
10
|
@@initialized = false
|
|
10
11
|
|
|
@@ -18,15 +19,12 @@ module Gemstar
|
|
|
18
19
|
def self.fetch(key, &block)
|
|
19
20
|
init
|
|
20
21
|
|
|
21
|
-
path =
|
|
22
|
+
path = path_for(key)
|
|
22
23
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
return nil if content == "__404__"
|
|
28
|
-
return content
|
|
29
|
-
end
|
|
24
|
+
if fresh?(path)
|
|
25
|
+
content = File.read(path)
|
|
26
|
+
return nil if content == "__404__"
|
|
27
|
+
return content
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
begin
|
|
@@ -39,11 +37,50 @@ module Gemstar
|
|
|
39
37
|
end
|
|
40
38
|
end
|
|
41
39
|
|
|
40
|
+
def self.peek(key)
|
|
41
|
+
init
|
|
42
|
+
|
|
43
|
+
path = path_for(key)
|
|
44
|
+
return nil unless fresh?(path)
|
|
45
|
+
|
|
46
|
+
content = File.read(path)
|
|
47
|
+
return nil if content == "__404__"
|
|
48
|
+
|
|
49
|
+
content
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.path_for(key)
|
|
53
|
+
File.join(CACHE_DIR, Digest::SHA256.hexdigest(key))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.fresh?(path)
|
|
57
|
+
return false unless File.exist?(path)
|
|
58
|
+
|
|
59
|
+
(Time.now - File.mtime(path)) <= MAX_CACHE_AGE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.flush!
|
|
63
|
+
init
|
|
64
|
+
|
|
65
|
+
flush_directory(CACHE_DIR)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.flush_directory(directory)
|
|
69
|
+
return 0 unless Dir.exist?(directory)
|
|
70
|
+
|
|
71
|
+
entries = Dir.children(directory)
|
|
72
|
+
entries.each do |entry|
|
|
73
|
+
FileUtils.rm_rf(File.join(directory, entry))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
entries.count
|
|
77
|
+
end
|
|
78
|
+
|
|
42
79
|
end
|
|
43
80
|
|
|
44
81
|
def edit_gitignore
|
|
45
82
|
gitignore_path = ".gitignore"
|
|
46
|
-
ignore_entries = %w[
|
|
83
|
+
ignore_entries = %w[gem_update_changelog.html]
|
|
47
84
|
|
|
48
85
|
existing_lines = File.exist?(gitignore_path) ? File.read(gitignore_path).lines.map(&:chomp) : []
|
|
49
86
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
require "thread"
|
|
3
|
+
|
|
4
|
+
module Gemstar
|
|
5
|
+
class CacheWarmer
|
|
6
|
+
DEFAULT_THREADS = 10
|
|
7
|
+
|
|
8
|
+
def initialize(io: $stderr, debug: false, thread_count: DEFAULT_THREADS)
|
|
9
|
+
@io = io
|
|
10
|
+
@debug = debug
|
|
11
|
+
@thread_count = thread_count
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@condition = ConditionVariable.new
|
|
14
|
+
@queue = []
|
|
15
|
+
@queued = Set.new
|
|
16
|
+
@in_progress = Set.new
|
|
17
|
+
@completed = Set.new
|
|
18
|
+
@workers = []
|
|
19
|
+
@started = false
|
|
20
|
+
@total = 0
|
|
21
|
+
@completed_count = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enqueue_many(gem_names)
|
|
25
|
+
names = gem_names.uniq
|
|
26
|
+
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
names.each do |gem_name|
|
|
29
|
+
next if @completed.include?(gem_name) || @queued.include?(gem_name) || @in_progress.include?(gem_name)
|
|
30
|
+
|
|
31
|
+
@queue << gem_name
|
|
32
|
+
@queued << gem_name
|
|
33
|
+
end
|
|
34
|
+
@total += names.count
|
|
35
|
+
start_workers_unlocked unless @started
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
log "Background cache refresh queued for #{names.count} gems."
|
|
39
|
+
@condition.broadcast
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def prioritize(gem_name)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
return if @completed.include?(gem_name) || @in_progress.include?(gem_name)
|
|
46
|
+
|
|
47
|
+
if @queued.include?(gem_name)
|
|
48
|
+
@queue.delete(gem_name)
|
|
49
|
+
else
|
|
50
|
+
@queued << gem_name
|
|
51
|
+
@total += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@queue.unshift(gem_name)
|
|
55
|
+
start_workers_unlocked unless @started
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
log "Prioritized #{gem_name}"
|
|
59
|
+
@condition.broadcast
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def start_workers_unlocked
|
|
65
|
+
return if @started
|
|
66
|
+
|
|
67
|
+
@started = true
|
|
68
|
+
@thread_count.times do
|
|
69
|
+
@workers << Thread.new { worker_loop }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def worker_loop
|
|
74
|
+
Thread.current.name = "gemstar-cache-worker" if Thread.current.respond_to?(:name=)
|
|
75
|
+
|
|
76
|
+
loop do
|
|
77
|
+
gem_name = @mutex.synchronize do
|
|
78
|
+
while @queue.empty?
|
|
79
|
+
@condition.wait(@mutex)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
next_gem = @queue.shift
|
|
83
|
+
@queued.delete(next_gem)
|
|
84
|
+
@in_progress << next_gem
|
|
85
|
+
next_gem
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
warm_cache_for_gem(gem_name)
|
|
89
|
+
|
|
90
|
+
current = @mutex.synchronize do
|
|
91
|
+
@in_progress.delete(gem_name)
|
|
92
|
+
@completed << gem_name
|
|
93
|
+
@completed_count += 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
log_progress(gem_name, current)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def warm_cache_for_gem(gem_name)
|
|
101
|
+
metadata = Gemstar::RubyGemsMetadata.new(gem_name)
|
|
102
|
+
metadata.meta
|
|
103
|
+
metadata.repo_uri
|
|
104
|
+
Gemstar::ChangeLog.new(metadata).sections
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
log "Cache refresh failed for #{gem_name}: #{e.class}: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def log_progress(gem_name, current)
|
|
110
|
+
return unless @debug
|
|
111
|
+
return unless current <= 5 || (current % 25).zero?
|
|
112
|
+
|
|
113
|
+
log "Background cache refresh #{current}/#{@total}: #{gem_name}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def log(message)
|
|
117
|
+
@io.puts(message)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/gemstar/change_log.rb
CHANGED
|
@@ -10,21 +10,30 @@ module Gemstar
|
|
|
10
10
|
|
|
11
11
|
attr_reader :metadata
|
|
12
12
|
|
|
13
|
-
def content
|
|
14
|
-
@content
|
|
13
|
+
def content(cache_only: false)
|
|
14
|
+
return @content if !cache_only && defined?(@content)
|
|
15
|
+
|
|
16
|
+
result = fetch_changelog_content(cache_only: cache_only)
|
|
17
|
+
@content = result unless cache_only
|
|
18
|
+
result
|
|
15
19
|
end
|
|
16
20
|
|
|
17
|
-
def sections
|
|
18
|
-
@sections
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def sections(cache_only: false)
|
|
22
|
+
return @sections if !cache_only && defined?(@sections)
|
|
23
|
+
|
|
24
|
+
result = begin
|
|
25
|
+
s = parse_changelog_sections(cache_only: cache_only)
|
|
26
|
+
if s.nil? || s.empty?
|
|
27
|
+
s = parse_github_release_sections(cache_only: cache_only)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
pp @@candidates_found if Gemstar.debug? && !cache_only
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
s
|
|
33
|
+
end
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
@sections = result unless cache_only
|
|
36
|
+
result
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
def extract_relevant_sections(old_version, new_version)
|
|
@@ -52,26 +61,30 @@ module Gemstar
|
|
|
52
61
|
def extract_version_from_heading(line)
|
|
53
62
|
return nil unless line
|
|
54
63
|
heading = line.to_s
|
|
64
|
+
version_token = /(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)/
|
|
55
65
|
# 1) Prefer version inside parentheses after a date: "### 2025-11-07 (2.16.0)"
|
|
56
66
|
# Ensure we ONLY treat it as a version if it actually looks like a version (has a dot),
|
|
57
67
|
# so we don't capture dates like (2025-11-21).
|
|
58
|
-
return $1 if heading[/\(\s*v
|
|
68
|
+
return $1 if heading[/\(\s*v?#{version_token}(?![A-Za-z0-9])\s*\)/]
|
|
59
69
|
# 2) Version-first with optional leading markers/labels: "## v1.2.6 - 2025-10-21"
|
|
60
70
|
# Require a dot in the numeric token to avoid capturing dates like 2025-11-21.
|
|
61
|
-
return $1 if heading[/^\s*(?:#+|=+)?\s*(?:Version\s+)?\[
|
|
71
|
+
return $1 if heading[/^\s*(?:[-*]\s+)?(?:#+|=+)?\s*(?:Version\s+)?\[*v?#{version_token}(?![A-Za-z0-9])\]*/i]
|
|
62
72
|
# 3) Anywhere: first semver-like token with a dot
|
|
63
|
-
return $1 if heading[/\bv
|
|
73
|
+
return $1 if heading[/\bv?#{version_token}(?![A-Za-z0-9])\b/]
|
|
64
74
|
nil
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
def changelog_uri_candidates
|
|
77
|
+
def changelog_uri_candidates(cache_only: false)
|
|
68
78
|
candidates = []
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
repo_uri = @metadata.repo_uri(cache_only: cache_only)
|
|
81
|
+
return [] if repo_uri.nil? || repo_uri.empty?
|
|
82
|
+
|
|
83
|
+
if repo_uri =~ %r{https://github\.com/aws/aws-sdk-ruby}
|
|
71
84
|
base = "https://raw.githubusercontent.com/aws/aws-sdk-ruby/refs/heads/version-3/gems/#{@metadata.gem_name}"
|
|
72
85
|
aws_style = true
|
|
73
86
|
else
|
|
74
|
-
base =
|
|
87
|
+
base = repo_uri.sub("https://github.com", "https://raw.githubusercontent.com")
|
|
75
88
|
aws_style = false
|
|
76
89
|
end
|
|
77
90
|
|
|
@@ -94,7 +107,8 @@ module Gemstar
|
|
|
94
107
|
end
|
|
95
108
|
|
|
96
109
|
# Add the gem's changelog_uri last as it's usually not the most parsable:
|
|
97
|
-
|
|
110
|
+
meta = @metadata.meta(cache_only: cache_only)
|
|
111
|
+
candidates += [Gemstar::GitHub::github_blob_to_raw(meta["changelog_uri"])] if meta
|
|
98
112
|
|
|
99
113
|
candidates.flatten!
|
|
100
114
|
candidates.uniq!
|
|
@@ -103,15 +117,19 @@ module Gemstar
|
|
|
103
117
|
candidates
|
|
104
118
|
end
|
|
105
119
|
|
|
106
|
-
def fetch_changelog_content
|
|
120
|
+
def fetch_changelog_content(cache_only: false)
|
|
107
121
|
content = nil
|
|
108
122
|
|
|
109
|
-
changelog_uri_candidates.find do |candidate|
|
|
110
|
-
content =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
changelog_uri_candidates(cache_only: cache_only).find do |candidate|
|
|
124
|
+
content = if cache_only
|
|
125
|
+
Cache.peek("changelog-#{candidate}")
|
|
126
|
+
else
|
|
127
|
+
Cache.fetch("changelog-#{candidate}") do
|
|
128
|
+
URI.open(candidate, read_timeout: 8)&.read
|
|
129
|
+
rescue => e
|
|
130
|
+
puts "#{candidate}: #{e}" if Gemstar.debug?
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
115
133
|
end
|
|
116
134
|
|
|
117
135
|
# puts "fetch_changelog_content #{candidate}:\n#{content}" if Gemstar.debug?
|
|
@@ -127,18 +145,18 @@ module Gemstar
|
|
|
127
145
|
end
|
|
128
146
|
|
|
129
147
|
VERSION_PATTERNS = [
|
|
130
|
-
/^\s*(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d[
|
|
131
|
-
/^\s*(?:#+|=+)\s*\[
|
|
132
|
-
/^\s*(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[
|
|
133
|
-
/^\s*(?:#+|=+)\s*(?:Version\s+)?\[
|
|
134
|
-
/^\s*(?:Version\s+)?v?(\d+\.\d+(?:\.\d+)?[A-Za-z0-9
|
|
148
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\s*\)/, # prefer this
|
|
149
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\[*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\]*\s*(?:—|–|-)\s*\d{4}-\d{2}-\d{2}\b/,
|
|
150
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\]*(?:\s*[-(].*)?/i,
|
|
151
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?\[*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\]*(?:\s*[-(].*)?/i,
|
|
152
|
+
/^\s*(?:[-*]\s+)?(?:Version\s+)?v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])(?:\s*[-(].*)?/i
|
|
135
153
|
]
|
|
136
154
|
|
|
137
|
-
def parse_changelog_sections
|
|
155
|
+
def parse_changelog_sections(cache_only: false)
|
|
138
156
|
# If the fetched content looks like a GitHub Releases HTML page, return {}
|
|
139
157
|
# so that the GitHub releases scraper can handle it. This avoids
|
|
140
158
|
# accidentally parsing HTML from /releases pages as a markdown changelog.
|
|
141
|
-
c = content
|
|
159
|
+
c = content(cache_only: cache_only)
|
|
142
160
|
return {} if c.nil? || c.strip.empty?
|
|
143
161
|
if (c.include?("<html") || c.include?("<!DOCTYPE html")) &&
|
|
144
162
|
(c.include?('data-test-selector="body-content"') || c.include?("/releases/tag/"))
|
|
@@ -194,24 +212,37 @@ module Gemstar
|
|
|
194
212
|
sections
|
|
195
213
|
end
|
|
196
214
|
|
|
197
|
-
def parse_github_release_sections
|
|
215
|
+
def parse_github_release_sections(cache_only: false)
|
|
198
216
|
begin
|
|
199
217
|
require "nokogiri"
|
|
200
218
|
rescue LoadError
|
|
201
219
|
return {}
|
|
202
220
|
end
|
|
203
221
|
|
|
204
|
-
|
|
222
|
+
repo_uri = @metadata&.repo_uri(cache_only: cache_only)
|
|
223
|
+
return {} unless repo_uri&.include?("github.com")
|
|
205
224
|
|
|
206
|
-
url = github_releases_url
|
|
225
|
+
url = github_releases_url(repo_uri)
|
|
207
226
|
return {} unless url
|
|
208
227
|
|
|
209
|
-
html =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
html = if cache_only
|
|
229
|
+
Cache.peek("releases-#{url}")
|
|
230
|
+
else
|
|
231
|
+
Cache.fetch("releases-#{url}") do
|
|
232
|
+
begin
|
|
233
|
+
URI.open(url, read_timeout: 8)&.read
|
|
234
|
+
rescue => e
|
|
235
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if (html.nil? || html.strip.empty?) && cache_only
|
|
242
|
+
cached_content = content(cache_only: true)
|
|
243
|
+
if cached_content&.include?("<html") &&
|
|
244
|
+
(cached_content.include?('data-test-selector="body-content"') || cached_content.include?("/releases/tag/"))
|
|
245
|
+
html = cached_content
|
|
215
246
|
end
|
|
216
247
|
end
|
|
217
248
|
|
|
@@ -271,9 +302,9 @@ module Gemstar
|
|
|
271
302
|
sections
|
|
272
303
|
end
|
|
273
304
|
|
|
274
|
-
def github_releases_url
|
|
275
|
-
return nil unless
|
|
276
|
-
repo =
|
|
305
|
+
def github_releases_url(repo_uri = @metadata&.repo_uri)
|
|
306
|
+
return nil unless repo_uri
|
|
307
|
+
repo = repo_uri.chomp("/")
|
|
277
308
|
return nil if repo.empty?
|
|
278
309
|
"#{repo}/releases"
|
|
279
310
|
end
|
data/lib/gemstar/cli.rb
CHANGED
|
@@ -19,6 +19,18 @@ module Gemstar
|
|
|
19
19
|
Gemstar::Commands::Diff.new(options).run
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
desc "server", "Start the interactive web server"
|
|
23
|
+
method_option :bind, type: :string, default: "127.0.0.1", desc: "Bind address"
|
|
24
|
+
method_option :port, type: :numeric, default: 2112, desc: "Port"
|
|
25
|
+
method_option :project, type: :string, repeatable: true, desc: "Project directories or Gemfile paths"
|
|
26
|
+
method_option :reload, type: :boolean, default: false, desc: "Restart automatically when files change"
|
|
27
|
+
def server
|
|
28
|
+
Gemstar::Commands::Server.new(options).run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "cache SUBCOMMAND ...ARGS", "Manage gemstar caches"
|
|
32
|
+
subcommand "cache", Gemstar::CacheCLI
|
|
33
|
+
|
|
22
34
|
# desc "pick", "Interactively cherry-pick and upgrade gems"
|
|
23
35
|
# option :gem, type: :string, desc: "Gem name to cherry-pick"
|
|
24
36
|
# def pick
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require_relative "command"
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
module Commands
|
|
5
|
+
class Cache < Command
|
|
6
|
+
def flush
|
|
7
|
+
removed_entries = Gemstar::Cache.flush!
|
|
8
|
+
puts "Flushed #{removed_entries} cache entr#{removed_entries == 1 ? 'y' : 'ies'} from #{Gemstar::Cache::CACHE_DIR}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
4
|
require "concurrent-ruby"
|
|
5
|
-
require "
|
|
5
|
+
require "tmpdir"
|
|
6
6
|
|
|
7
7
|
module Gemstar
|
|
8
8
|
module Commands
|
|
@@ -24,7 +24,7 @@ module Gemstar
|
|
|
24
24
|
@from = options[:from] || "HEAD"
|
|
25
25
|
@to = options[:to]
|
|
26
26
|
@lockfile = options[:lockfile] || "Gemfile.lock"
|
|
27
|
-
@output_file = options[:output_file] || "gem_update_changelog.html"
|
|
27
|
+
@output_file = options[:output_file] || File.join(Dir.tmpdir, "gem_update_changelog.html")
|
|
28
28
|
|
|
29
29
|
@git_repo = Gemstar::GitRepo.new(File.dirname(@lockfile))
|
|
30
30
|
end
|
|
@@ -46,7 +46,7 @@ module Gemstar
|
|
|
46
46
|
|
|
47
47
|
html = Outputs::HTML.new.render_diff(self)
|
|
48
48
|
File.write(output_file, html)
|
|
49
|
-
puts "✅
|
|
49
|
+
puts "✅ Changelog report created: #{File.expand_path(output_file)}"
|
|
50
50
|
|
|
51
51
|
if failed.any?
|
|
52
52
|
puts "\n⚠️ The following gems failed to process:"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require_relative "command"
|
|
2
|
+
require "shellwords"
|
|
3
|
+
|
|
4
|
+
module Gemstar
|
|
5
|
+
module Commands
|
|
6
|
+
class Server < Command
|
|
7
|
+
DEFAULT_BIND = "127.0.0.1"
|
|
8
|
+
DEFAULT_PORT = 2112
|
|
9
|
+
RELOAD_ENV_VAR = "GEMSTAR_RELOAD_ACTIVE"
|
|
10
|
+
RELOAD_GLOB = "{lib/**/*.rb,lib/gemstar/web/templates/**/*,bin/gemstar,README.md}"
|
|
11
|
+
|
|
12
|
+
attr_reader :bind
|
|
13
|
+
attr_reader :port
|
|
14
|
+
attr_reader :project_inputs
|
|
15
|
+
attr_reader :reload
|
|
16
|
+
|
|
17
|
+
def initialize(options)
|
|
18
|
+
super
|
|
19
|
+
|
|
20
|
+
@bind = options[:bind] || DEFAULT_BIND
|
|
21
|
+
@port = (options[:port] || DEFAULT_PORT).to_i
|
|
22
|
+
@project_inputs = normalize_project_inputs(options[:project])
|
|
23
|
+
@reload = options[:reload]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
restart_with_rerun if reload_requested?
|
|
28
|
+
|
|
29
|
+
require "rackup"
|
|
30
|
+
require "webrick"
|
|
31
|
+
require "gemstar/request_logger"
|
|
32
|
+
require "gemstar/webrick_logger"
|
|
33
|
+
require "gemstar/web/app"
|
|
34
|
+
|
|
35
|
+
Gemstar::Config.ensure_home_directory!
|
|
36
|
+
|
|
37
|
+
projects = load_projects
|
|
38
|
+
log_loaded_projects(projects)
|
|
39
|
+
cache_warmer = start_background_cache_refresh(projects)
|
|
40
|
+
app = Gemstar::Web::App.build(projects: projects, config_home: Gemstar::Config.home_directory, cache_warmer: cache_warmer)
|
|
41
|
+
app = Gemstar::RequestLogger.new(app, io: $stderr) if debug_request_logging?
|
|
42
|
+
|
|
43
|
+
puts "Gemstar server listening on http://#{bind}:#{port}"
|
|
44
|
+
puts "Config home: #{Gemstar::Config.home_directory}"
|
|
45
|
+
Rackup::Server.start(
|
|
46
|
+
app: app,
|
|
47
|
+
Host: bind,
|
|
48
|
+
Port: port,
|
|
49
|
+
AccessLog: [],
|
|
50
|
+
Logger: Gemstar::WEBrickLogger.new($stderr, WEBrick::BasicLog::INFO)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def normalize_project_inputs(project_option)
|
|
57
|
+
inputs = Array(project_option).compact.map(&:to_s)
|
|
58
|
+
return ["."] if inputs.empty?
|
|
59
|
+
|
|
60
|
+
inputs.uniq
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reload_requested?
|
|
64
|
+
reload && ENV[RELOAD_ENV_VAR] != "1"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def restart_with_rerun
|
|
68
|
+
rerun_executable = find_rerun_executable
|
|
69
|
+
unless rerun_executable
|
|
70
|
+
raise Thor::Error, "The `rerun` gem is not installed. Run `bundle install` and try `gemstar server --reload` again."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts "Starting gemstar server in reload mode..."
|
|
74
|
+
puts "Watching changes matching #{RELOAD_GLOB.inspect}"
|
|
75
|
+
|
|
76
|
+
env = ENV.to_h.merge(RELOAD_ENV_VAR => "1")
|
|
77
|
+
exec env, *rerun_command(rerun_executable)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def find_rerun_executable
|
|
81
|
+
Gem.bin_path("rerun", "rerun")
|
|
82
|
+
rescue Gem::Exception
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rerun_command(rerun_executable)
|
|
87
|
+
[
|
|
88
|
+
rerun_executable,
|
|
89
|
+
"--pattern",
|
|
90
|
+
RELOAD_GLOB,
|
|
91
|
+
"--",
|
|
92
|
+
Gem.ruby,
|
|
93
|
+
File.expand_path($PROGRAM_NAME)
|
|
94
|
+
] + server_arguments_without_reload
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def server_arguments_without_reload
|
|
98
|
+
args = [
|
|
99
|
+
"server",
|
|
100
|
+
"--bind", bind,
|
|
101
|
+
"--port", port.to_s
|
|
102
|
+
]
|
|
103
|
+
project_inputs.each do |project|
|
|
104
|
+
args << "--project"
|
|
105
|
+
args << project
|
|
106
|
+
end
|
|
107
|
+
args
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_projects
|
|
111
|
+
project_inputs.map { |input| Gemstar::Project.from_cli_argument(input) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def log_loaded_projects(projects)
|
|
115
|
+
return unless debug_request_logging?
|
|
116
|
+
|
|
117
|
+
$stderr.puts "[gemstar] project inputs: #{project_inputs.inspect}"
|
|
118
|
+
$stderr.puts "[gemstar] loaded projects (#{projects.count}): #{projects.map(&:directory).inspect}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def debug_request_logging?
|
|
122
|
+
ENV["DEBUG"] == "1"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def start_background_cache_refresh(projects)
|
|
126
|
+
gem_names = projects.flat_map do |project|
|
|
127
|
+
project.current_lockfile&.specs&.keys || []
|
|
128
|
+
end.uniq.sort
|
|
129
|
+
|
|
130
|
+
return nil if gem_names.empty?
|
|
131
|
+
|
|
132
|
+
Gemstar::CacheWarmer.new(io: $stderr, debug: debug_request_logging? || Gemstar.debug?, thread_count: 10).enqueue_many(gem_names)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|