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
data/lib/gemstar/git_repo.rb
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "pathname"
|
|
3
|
+
|
|
1
4
|
module Gemstar
|
|
2
5
|
class GitRepo
|
|
6
|
+
attr_reader :tree_root_directory
|
|
7
|
+
|
|
3
8
|
def initialize(specified_directory)
|
|
4
9
|
@specified_directory = specified_directory || Dir.pwd
|
|
5
|
-
|
|
10
|
+
search_directory = if File.directory?(@specified_directory)
|
|
11
|
+
@specified_directory
|
|
12
|
+
else
|
|
13
|
+
File.dirname(@specified_directory)
|
|
14
|
+
end
|
|
15
|
+
@tree_root_directory = find_git_root(search_directory)
|
|
6
16
|
end
|
|
7
17
|
|
|
8
18
|
def find_git_root(directory)
|
|
9
|
-
|
|
10
|
-
# find_git_root(File.dirname(directory))
|
|
11
|
-
|
|
12
|
-
run_git_command(%W[rev-parse --show-toplevel])
|
|
19
|
+
try_git_command(%W[rev-parse --show-toplevel], in_directory: directory)
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
def git_client
|
|
16
23
|
"git"
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
def
|
|
26
|
+
def build_git_command(command, in_directory: @specified_directory)
|
|
20
27
|
git_command = [git_client]
|
|
21
28
|
git_command += ["-C", in_directory] if in_directory
|
|
22
|
-
git_command
|
|
29
|
+
git_command + command
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run_git_command(command, in_directory: @specified_directory, strip: true)
|
|
33
|
+
git_command = build_git_command(command, in_directory:)
|
|
23
34
|
|
|
24
35
|
puts %[run_git_command (joined): #{git_command.join(" ")}] if Gemstar.debug?
|
|
25
36
|
|
|
@@ -28,6 +39,17 @@ module Gemstar
|
|
|
28
39
|
strip ? output.strip : output
|
|
29
40
|
end
|
|
30
41
|
|
|
42
|
+
def try_git_command(command, in_directory: @specified_directory, strip: true)
|
|
43
|
+
git_command = build_git_command(command, in_directory:)
|
|
44
|
+
|
|
45
|
+
puts %[try_git_command (joined): #{git_command.join(" ")}] if Gemstar.debug?
|
|
46
|
+
|
|
47
|
+
output, status = Open3.capture2e(*git_command)
|
|
48
|
+
return nil unless status.success?
|
|
49
|
+
|
|
50
|
+
strip ? output.strip : output
|
|
51
|
+
end
|
|
52
|
+
|
|
31
53
|
def resolve_commit(revish, default_branch: "HEAD")
|
|
32
54
|
# If it looks like a pure date (or you want to support "date only"),
|
|
33
55
|
# map it to "latest commit before date on default_branch".
|
|
@@ -51,5 +73,50 @@ module Gemstar
|
|
|
51
73
|
def get_full_path(path)
|
|
52
74
|
run_git_command(["ls-files", "--full-name", "--", path])
|
|
53
75
|
end
|
|
76
|
+
|
|
77
|
+
def relative_path(path)
|
|
78
|
+
return nil if tree_root_directory.nil? || tree_root_directory.empty?
|
|
79
|
+
|
|
80
|
+
Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(tree_root_directory)).to_s
|
|
81
|
+
rescue ArgumentError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def origin_repo_url
|
|
86
|
+
remote = try_git_command(["remote", "get-url", "origin"])
|
|
87
|
+
return nil if remote.nil? || remote.empty?
|
|
88
|
+
|
|
89
|
+
normalize_remote_url(remote)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def log_for_paths(paths, limit: 20, reverse: false)
|
|
93
|
+
return "" if tree_root_directory.nil? || tree_root_directory.empty? || paths.empty?
|
|
94
|
+
|
|
95
|
+
format = "%H%x1f%h%x1f%aI%x1f%s"
|
|
96
|
+
command = ["log"]
|
|
97
|
+
command += ["-n", limit.to_s] if limit
|
|
98
|
+
command << "--reverse" if reverse
|
|
99
|
+
command += ["--pretty=format:#{format}", "--", *paths]
|
|
100
|
+
|
|
101
|
+
run_git_command(command, in_directory: tree_root_directory)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def normalize_remote_url(remote)
|
|
107
|
+
normalized = remote.strip.sub(%r{\.git\z}, "")
|
|
108
|
+
|
|
109
|
+
if normalized.start_with?("git@github.com:")
|
|
110
|
+
path = normalized.delete_prefix("git@github.com:")
|
|
111
|
+
return "https://github.com/#{path}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if normalized.start_with?("ssh://git@github.com/")
|
|
115
|
+
path = normalized.delete_prefix("ssh://git@github.com/")
|
|
116
|
+
return "https://github.com/#{path}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
normalized.sub(%r{\Ahttp://}, "https://")
|
|
120
|
+
end
|
|
54
121
|
end
|
|
55
122
|
end
|
data/lib/gemstar/lock_file.rb
CHANGED
|
@@ -2,29 +2,107 @@ module Gemstar
|
|
|
2
2
|
class LockFile
|
|
3
3
|
def initialize(path: nil, content: nil)
|
|
4
4
|
@path = path
|
|
5
|
-
|
|
5
|
+
parsed = content ? parse_content(content) : parse_lockfile(path)
|
|
6
|
+
@specs = parsed[:specs]
|
|
7
|
+
@dependency_graph = parsed[:dependency_graph]
|
|
8
|
+
@direct_dependencies = parsed[:direct_dependencies]
|
|
6
9
|
end
|
|
7
10
|
|
|
8
11
|
attr_reader :specs
|
|
12
|
+
attr_reader :dependency_graph
|
|
13
|
+
attr_reader :direct_dependencies
|
|
14
|
+
|
|
15
|
+
def origins_for(gem_name)
|
|
16
|
+
return [{ type: :direct, path: [gem_name] }] if direct_dependencies.include?(gem_name)
|
|
17
|
+
|
|
18
|
+
direct_dependencies.filter_map do |root_dependency|
|
|
19
|
+
path = shortest_path_from(root_dependency, gem_name)
|
|
20
|
+
next if path.nil?
|
|
21
|
+
|
|
22
|
+
{ type: :transitive, path: path }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
9
25
|
|
|
10
26
|
private
|
|
11
27
|
|
|
28
|
+
def shortest_path_from(root_dependency, target_gem)
|
|
29
|
+
queue = [[root_dependency, [root_dependency]]]
|
|
30
|
+
visited = {}
|
|
31
|
+
|
|
32
|
+
until queue.empty?
|
|
33
|
+
current_name, path = queue.shift
|
|
34
|
+
next if visited[current_name]
|
|
35
|
+
|
|
36
|
+
visited[current_name] = true
|
|
37
|
+
|
|
38
|
+
Array(dependency_graph[current_name]).each do |dependency_name|
|
|
39
|
+
next_path = path + [dependency_name]
|
|
40
|
+
return next_path if dependency_name == target_gem
|
|
41
|
+
|
|
42
|
+
queue << [dependency_name, next_path]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
12
49
|
def parse_lockfile(path)
|
|
13
50
|
parse_content(File.read(path))
|
|
14
51
|
end
|
|
15
52
|
|
|
16
53
|
def parse_content(content)
|
|
17
54
|
specs = {}
|
|
18
|
-
|
|
55
|
+
dependency_graph = Hash.new { |hash, key| hash[key] = [] }
|
|
56
|
+
direct_dependencies = []
|
|
57
|
+
current_section = nil
|
|
58
|
+
current_spec = nil
|
|
59
|
+
|
|
19
60
|
content.each_line do |line|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
stripped = line.strip
|
|
62
|
+
|
|
63
|
+
if stripped.match?(/\A[A-Z][A-Z0-9 ]*\z/)
|
|
64
|
+
current_section = nil
|
|
65
|
+
current_spec = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if stripped == "GEM"
|
|
69
|
+
current_section = :gem
|
|
70
|
+
current_spec = nil
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if stripped == "DEPENDENCIES"
|
|
75
|
+
current_section = :dependencies
|
|
76
|
+
current_spec = nil
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if stripped.empty?
|
|
81
|
+
current_spec = nil if current_section == :dependencies
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
case current_section
|
|
86
|
+
when :gem
|
|
87
|
+
if line =~ /^\s{4}(\S+) \((.+)\)/
|
|
88
|
+
name, version = Regexp.last_match(1), Regexp.last_match(2)
|
|
89
|
+
specs[name] = version
|
|
90
|
+
current_spec = name
|
|
91
|
+
elsif current_spec && line =~ /^\s{6}([^\s(]+)/
|
|
92
|
+
dependency_graph[current_spec] << Regexp.last_match(1)
|
|
93
|
+
end
|
|
94
|
+
when :dependencies
|
|
95
|
+
if line =~ /^\s{2}([^\s!(]+)/
|
|
96
|
+
direct_dependencies << Regexp.last_match(1)
|
|
97
|
+
end
|
|
25
98
|
end
|
|
26
99
|
end
|
|
27
|
-
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
specs: specs,
|
|
103
|
+
dependency_graph: dependency_graph.transform_values(&:uniq),
|
|
104
|
+
direct_dependencies: direct_dependencies.uniq
|
|
105
|
+
}
|
|
28
106
|
end
|
|
29
107
|
end
|
|
30
108
|
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
class Project
|
|
5
|
+
attr_reader :directory
|
|
6
|
+
attr_reader :gemfile_path
|
|
7
|
+
attr_reader :lockfile_path
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
def self.from_cli_argument(input)
|
|
11
|
+
expanded_input = File.expand_path(input)
|
|
12
|
+
gemfile_path = if File.directory?(expanded_input)
|
|
13
|
+
File.join(expanded_input, "Gemfile")
|
|
14
|
+
else
|
|
15
|
+
expanded_input
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
raise ArgumentError, "No Gemfile found for #{input}" unless File.file?(gemfile_path)
|
|
19
|
+
raise ArgumentError, "#{gemfile_path} is not a Gemfile" unless File.basename(gemfile_path) == "Gemfile"
|
|
20
|
+
|
|
21
|
+
new(gemfile_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(gemfile_path)
|
|
25
|
+
@gemfile_path = File.expand_path(gemfile_path)
|
|
26
|
+
@directory = File.dirname(@gemfile_path)
|
|
27
|
+
@lockfile_path = File.join(@directory, "Gemfile.lock")
|
|
28
|
+
@name = File.basename(@directory)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def git_repo
|
|
32
|
+
@git_repo ||= Gemstar::GitRepo.new(directory)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def git_root
|
|
36
|
+
git_repo.tree_root_directory
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def lockfile?
|
|
40
|
+
File.file?(lockfile_path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def current_lockfile
|
|
44
|
+
return nil unless lockfile?
|
|
45
|
+
|
|
46
|
+
@current_lockfile ||= Gemstar::LockFile.new(path: lockfile_path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def revision_history(limit: 20)
|
|
50
|
+
history_for_paths(tracked_git_paths, limit: limit)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lockfile_revision_history(limit: 20)
|
|
54
|
+
return [] unless lockfile?
|
|
55
|
+
|
|
56
|
+
relative_path = git_repo.relative_path(lockfile_path)
|
|
57
|
+
return [] if relative_path.nil?
|
|
58
|
+
|
|
59
|
+
history_for_paths([relative_path], limit: limit)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def gemfile_revision_history(limit: 20)
|
|
63
|
+
relative_path = git_repo.relative_path(gemfile_path)
|
|
64
|
+
return [] if relative_path.nil?
|
|
65
|
+
|
|
66
|
+
history_for_paths([relative_path], limit: limit)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def default_from_revision_id
|
|
70
|
+
default_changed_lockfile_revision_id ||
|
|
71
|
+
gemfile_revision_history(limit: 1).first&.dig(:id) ||
|
|
72
|
+
"worktree"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def revision_options(limit: 20)
|
|
76
|
+
[{ id: "worktree", label: "Worktree", description: "Current Gemfile.lock in the working tree" }] +
|
|
77
|
+
revision_history(limit: limit).map do |revision|
|
|
78
|
+
{
|
|
79
|
+
id: revision[:id],
|
|
80
|
+
label: revision[:short_sha],
|
|
81
|
+
description: "#{revision[:subject]} (#{revision[:authored_at].strftime("%Y-%m-%d %H:%M")})"
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lockfile_for_revision(revision_id)
|
|
87
|
+
return current_lockfile if revision_id.nil? || revision_id == "worktree"
|
|
88
|
+
return nil unless lockfile?
|
|
89
|
+
|
|
90
|
+
relative_lockfile_path = git_repo.relative_path(lockfile_path)
|
|
91
|
+
return nil if relative_lockfile_path.nil?
|
|
92
|
+
|
|
93
|
+
content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_lockfile_path}"])
|
|
94
|
+
return nil if content.nil? || content.empty?
|
|
95
|
+
|
|
96
|
+
Gemstar::LockFile.new(content: content)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree")
|
|
100
|
+
from_lockfile = lockfile_for_revision(from_revision_id)
|
|
101
|
+
to_lockfile = lockfile_for_revision(to_revision_id)
|
|
102
|
+
from_specs = from_lockfile&.specs || {}
|
|
103
|
+
to_specs = to_lockfile&.specs || {}
|
|
104
|
+
|
|
105
|
+
(from_specs.keys | to_specs.keys).map do |gem_name|
|
|
106
|
+
old_version = from_specs[gem_name]
|
|
107
|
+
new_version = to_specs[gem_name]
|
|
108
|
+
bundle_origins = to_lockfile&.origins_for(gem_name) || []
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
name: gem_name,
|
|
112
|
+
old_version: old_version,
|
|
113
|
+
new_version: new_version,
|
|
114
|
+
status: gem_status(old_version, new_version),
|
|
115
|
+
version_label: version_label(old_version, new_version),
|
|
116
|
+
bundle_origins: bundle_origins,
|
|
117
|
+
bundle_origin_labels: bundle_origin_labels(bundle_origins)
|
|
118
|
+
}
|
|
119
|
+
end.sort_by { |gem| gem[:name] }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def gem_added_on(gem_name, revision_id: "worktree")
|
|
123
|
+
return nil unless lockfile?
|
|
124
|
+
|
|
125
|
+
target_lockfile = lockfile_for_revision(revision_id)
|
|
126
|
+
return nil unless target_lockfile&.specs&.key?(gem_name)
|
|
127
|
+
|
|
128
|
+
relative_path = git_repo.relative_path(lockfile_path)
|
|
129
|
+
return nil if relative_path.nil?
|
|
130
|
+
|
|
131
|
+
first_seen_revision = history_for_paths([relative_path], limit: nil, reverse: true).find do |revision|
|
|
132
|
+
lockfile = lockfile_for_revision(revision[:id])
|
|
133
|
+
lockfile&.specs&.key?(gem_name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
return worktree_added_on_info if first_seen_revision.nil? && revision_id == "worktree"
|
|
137
|
+
return nil unless first_seen_revision
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
project_name: name,
|
|
141
|
+
date: first_seen_revision[:authored_at].strftime("%Y-%m-%d"),
|
|
142
|
+
revision: first_seen_revision[:short_sha],
|
|
143
|
+
revision_url: revision_url(first_seen_revision[:id]),
|
|
144
|
+
worktree: false
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def default_changed_lockfile_revision_id
|
|
151
|
+
return nil unless lockfile?
|
|
152
|
+
|
|
153
|
+
current_specs = current_lockfile&.specs || {}
|
|
154
|
+
|
|
155
|
+
lockfile_revision_history(limit: 20).find do |revision|
|
|
156
|
+
revision_lockfile = lockfile_for_revision(revision[:id])
|
|
157
|
+
revision_lockfile && revision_lockfile.specs != current_specs
|
|
158
|
+
end&.dig(:id)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def history_for_paths(paths, limit: 20, reverse: false)
|
|
162
|
+
return [] if git_root.nil? || git_root.empty?
|
|
163
|
+
return [] if paths.empty?
|
|
164
|
+
|
|
165
|
+
output = git_repo.log_for_paths(paths, limit: limit, reverse: reverse)
|
|
166
|
+
return [] if output.nil? || output.empty?
|
|
167
|
+
|
|
168
|
+
output.lines.filter_map do |line|
|
|
169
|
+
full_sha, short_sha, authored_at, subject = line.strip.split("\u001f", 4)
|
|
170
|
+
next if full_sha.nil?
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
id: full_sha,
|
|
174
|
+
full_sha: full_sha,
|
|
175
|
+
short_sha: short_sha,
|
|
176
|
+
authored_at: Time.iso8601(authored_at),
|
|
177
|
+
subject: subject
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
rescue ArgumentError
|
|
181
|
+
[]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def tracked_git_paths
|
|
185
|
+
[gemfile_path, lockfile_path].filter_map do |path|
|
|
186
|
+
next unless File.file?(path)
|
|
187
|
+
|
|
188
|
+
git_repo.relative_path(path)
|
|
189
|
+
end.uniq
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def gem_status(old_version, new_version)
|
|
193
|
+
return :added if old_version.nil? && !new_version.nil?
|
|
194
|
+
return :removed if !old_version.nil? && new_version.nil?
|
|
195
|
+
return :unchanged if old_version == new_version
|
|
196
|
+
|
|
197
|
+
comparison = compare_versions(new_version, old_version)
|
|
198
|
+
return :upgrade if comparison.positive?
|
|
199
|
+
return :downgrade if comparison.negative?
|
|
200
|
+
|
|
201
|
+
:changed
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def version_label(old_version, new_version)
|
|
205
|
+
return "new → #{new_version}" if old_version.nil? && !new_version.nil?
|
|
206
|
+
return "#{old_version} → removed" if !old_version.nil? && new_version.nil?
|
|
207
|
+
return new_version.to_s if old_version == new_version
|
|
208
|
+
|
|
209
|
+
"#{old_version} → #{new_version}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def compare_versions(left, right)
|
|
213
|
+
Gem::Version.new(left.to_s.gsub(/-[\w\-]+$/, "")) <=> Gem::Version.new(right.to_s.gsub(/-[\w\-]+$/, ""))
|
|
214
|
+
rescue ArgumentError
|
|
215
|
+
left.to_s <=> right.to_s
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def bundle_origin_labels(origins)
|
|
219
|
+
Array(origins).map do |origin|
|
|
220
|
+
next "Gemfile" if origin[:type] == :direct
|
|
221
|
+
|
|
222
|
+
["Gemfile", *origin[:path]].join(" → ")
|
|
223
|
+
end.compact.uniq
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def worktree_added_on_info
|
|
227
|
+
return nil unless File.file?(lockfile_path)
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
project_name: name,
|
|
231
|
+
date: File.mtime(lockfile_path).strftime("%Y-%m-%d"),
|
|
232
|
+
revision: "Worktree",
|
|
233
|
+
revision_url: nil,
|
|
234
|
+
worktree: true
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def revision_url(full_sha)
|
|
239
|
+
repo_url = git_repo.origin_repo_url
|
|
240
|
+
return nil unless repo_url&.include?("github.com")
|
|
241
|
+
|
|
242
|
+
"#{repo_url}/commit/#{full_sha}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Gemstar
|
|
2
|
+
class RequestLogger
|
|
3
|
+
def initialize(app, io: $stderr)
|
|
4
|
+
@app = app
|
|
5
|
+
@io = io
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
|
+
status, headers, body = @app.call(env)
|
|
11
|
+
log_request(env, status, started_at)
|
|
12
|
+
[status, headers, body]
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
log_request(env, 500, started_at, error: e)
|
|
15
|
+
raise
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def log_request(env, status, started_at, error: nil)
|
|
21
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(1)
|
|
22
|
+
path = env["PATH_INFO"].to_s
|
|
23
|
+
query = env["QUERY_STRING"].to_s
|
|
24
|
+
full_path = query.empty? ? path : "#{path}?#{query}"
|
|
25
|
+
method = env["REQUEST_METHOD"].to_s
|
|
26
|
+
suffix = error ? " #{error.class}: #{error.message}" : ""
|
|
27
|
+
|
|
28
|
+
@io.puts "[gemstar] #{method} #{full_path} -> #{status} in #{duration_ms}ms#{suffix}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -10,49 +10,65 @@ module Gemstar
|
|
|
10
10
|
|
|
11
11
|
attr_reader :gem_name
|
|
12
12
|
|
|
13
|
-
def meta
|
|
14
|
-
@meta
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
rescue
|
|
23
|
-
nil
|
|
24
|
-
end }
|
|
13
|
+
def meta(cache_only: false)
|
|
14
|
+
return @meta if !cache_only && defined?(@meta)
|
|
15
|
+
|
|
16
|
+
json = if cache_only
|
|
17
|
+
Cache.peek("rubygems-#{gem_name}")
|
|
18
|
+
else
|
|
19
|
+
url = "https://rubygems.org/api/v1/gems/#{URI.encode_www_form_component(gem_name)}.json"
|
|
20
|
+
Cache.fetch("rubygems-#{gem_name}") do
|
|
21
|
+
URI.open(url).read
|
|
25
22
|
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
parsed = begin
|
|
26
|
+
JSON.parse(json) if json
|
|
27
|
+
rescue
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@meta = parsed unless cache_only
|
|
32
|
+
parsed
|
|
26
33
|
end
|
|
27
34
|
|
|
28
|
-
def repo_uri
|
|
29
|
-
|
|
35
|
+
def repo_uri(cache_only: false)
|
|
36
|
+
resolved_meta = meta(cache_only: cache_only)
|
|
37
|
+
return nil unless resolved_meta
|
|
38
|
+
|
|
39
|
+
return @repo_uri if !cache_only && defined?(@repo_uri)
|
|
40
|
+
|
|
41
|
+
repo = begin
|
|
42
|
+
uri = resolved_meta["source_code_uri"]
|
|
43
|
+
|
|
44
|
+
if uri.nil?
|
|
45
|
+
uri = resolved_meta["homepage_uri"]
|
|
46
|
+
if uri.include?("github.com")
|
|
47
|
+
uri = uri[%r{http[s?]://github\.com/[^/]+/[^/]+}]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
30
50
|
|
|
31
|
-
|
|
32
|
-
uri = meta["source_code_uri"]
|
|
51
|
+
uri ||= ""
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
uri = meta["homepage_uri"]
|
|
36
|
-
if uri.include?("github.com")
|
|
37
|
-
uri = uri[%r{http[s?]://github\.com/[^/]+/[^/]+}]
|
|
38
|
-
end
|
|
39
|
-
end
|
|
53
|
+
uri = uri.sub("http://", "https://")
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
uri = uri.gsub(/\.git$/, "")
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
if uri.include?("github.io")
|
|
58
|
+
uri = uri.sub(%r{\Ahttps?://([\w-]+)\.github\.io/([^/]+)}) do
|
|
59
|
+
"https://github.com/#{$1}/#{$2}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
if uri.include?("github.com")
|
|
64
|
+
uri = uri[%r{\Ahttps?://github\.com/[^/]+/[^/]+}] || uri
|
|
65
|
+
end
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
uri = uri.sub(%r{\Ahttps?://([\w-]+)\.github\.io/([^/]+)}) do
|
|
50
|
-
"https://github.com/#{$1}/#{$2}"
|
|
51
|
-
end
|
|
52
|
-
end
|
|
67
|
+
uri
|
|
68
|
+
end
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
@repo_uri = repo unless cache_only
|
|
71
|
+
repo
|
|
56
72
|
end
|
|
57
73
|
|
|
58
74
|
end
|