rails-wayback 0.1.0 → 0.2.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/README.md +120 -7
- data/Rakefile +17 -1
- data/app/controllers/rails_wayback/assets_controller.rb +78 -0
- data/app/controllers/rails_wayback/pages_controller.rb +50 -9
- data/config/routes.rb +5 -0
- data/lib/generators/rails_wayback/install/install_generator.rb +5 -3
- data/lib/generators/rails_wayback/install/templates/initializer.rb.tt +18 -1
- data/lib/rails_wayback/asset_provenance.rb +41 -0
- data/lib/rails_wayback/bar_middleware.rb +235 -67
- data/lib/rails_wayback/bar_renderer.css +76 -0
- data/lib/rails_wayback/bar_renderer.html.erb +43 -0
- data/lib/rails_wayback/bar_renderer.js +361 -0
- data/lib/rails_wayback/bar_renderer.rb +70 -218
- data/lib/rails_wayback/bar_search.js +73 -0
- data/lib/rails_wayback/cache_inventory.rb +199 -0
- data/lib/rails_wayback/cli.rb +70 -3
- data/lib/rails_wayback/configuration.rb +13 -2
- data/lib/rails_wayback/controller_extensions.rb +71 -20
- data/lib/rails_wayback/doctor.rb +123 -0
- data/lib/rails_wayback/engine.rb +27 -19
- data/lib/rails_wayback/engine_mount.rb +15 -0
- data/lib/rails_wayback/git.rb +92 -8
- data/lib/rails_wayback/historical_asset_body.rb +41 -0
- data/lib/rails_wayback/historical_asset_helper.rb +68 -0
- data/lib/rails_wayback/historical_asset_resolver.rb +102 -0
- data/lib/rails_wayback/ref_policy.rb +64 -0
- data/lib/rails_wayback/render_context.rb +24 -0
- data/lib/rails_wayback/render_provenance.rb +84 -0
- data/lib/rails_wayback/render_tracker.rb +34 -0
- data/lib/rails_wayback/toggle.rb +10 -2
- data/lib/rails_wayback/version.rb +1 -1
- data/lib/rails_wayback/view_source.rb +171 -24
- data/lib/rails_wayback.rb +42 -6
- data/lib/tasks/rails_wayback.rake +17 -0
- data/rails-wayback.gemspec +2 -5
- metadata +21 -33
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "cgi"
|
|
4
|
+
require "rails_wayback/asset_provenance"
|
|
4
5
|
require "rails_wayback/bar_renderer"
|
|
6
|
+
require "rails_wayback/engine_mount"
|
|
7
|
+
require "rails_wayback/ref_policy"
|
|
5
8
|
|
|
6
9
|
module RailsWayback
|
|
7
10
|
# Rack middleware that appends the wayback bar to every successful
|
|
8
|
-
# HTML response of the host application while the gem
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
+
# or client-error HTML response of the host application while the gem
|
|
12
|
+
# is enabled. Only complete, bounded Rack-buffered documents are
|
|
13
|
+
# transformed; streaming and otherwise unsafe responses pass through.
|
|
11
14
|
class BarMiddleware
|
|
15
|
+
UNSAFE_STATUSES = [204, 205, 206].freeze
|
|
16
|
+
UNSAFE_RESPONSE_HEADERS = %w[
|
|
17
|
+
transfer-encoding
|
|
18
|
+
content-range
|
|
19
|
+
rack.hijack
|
|
20
|
+
x-sendfile
|
|
21
|
+
x-accel-redirect
|
|
22
|
+
].freeze
|
|
23
|
+
INVALIDATED_RESPONSE_HEADERS = %w[
|
|
24
|
+
etag
|
|
25
|
+
content-md5
|
|
26
|
+
digest
|
|
27
|
+
content-digest
|
|
28
|
+
repr-digest
|
|
29
|
+
content-range
|
|
30
|
+
accept-ranges
|
|
31
|
+
].freeze
|
|
12
32
|
SKIP_PATH_PREFIXES = %w[
|
|
13
33
|
/assets
|
|
14
34
|
/packs
|
|
@@ -22,60 +42,163 @@ module RailsWayback
|
|
|
22
42
|
end
|
|
23
43
|
|
|
24
44
|
def call(env)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
tracker = RailsWayback::RenderTracker.new
|
|
46
|
+
RailsWayback::RenderContext.with(tracker) { call_with_tracker(env, tracker) }
|
|
47
|
+
ensure
|
|
48
|
+
tracker&.close
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
def call_with_tracker(env, tracker)
|
|
54
|
+
response = @app.call(env)
|
|
55
|
+
status, headers, body = response
|
|
56
|
+
return response unless inject?(env, status, headers, body)
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
parts = body.to_ary
|
|
59
|
+
return [status, headers, parts] unless body_within_limit?(parts)
|
|
60
|
+
|
|
61
|
+
html = parts.join
|
|
62
|
+
return [status, headers, parts] unless injectable_document?(html)
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
new_headers[length_key] = new_html.bytesize.to_s if length_key
|
|
64
|
+
snippet = build_snippet(env, tracker)
|
|
65
|
+
return [status, headers, parts] if snippet.empty?
|
|
40
66
|
|
|
41
|
-
|
|
67
|
+
new_html = inject(html, snippet)
|
|
68
|
+
[status, repaired_headers(headers, new_html), [new_html]]
|
|
42
69
|
end
|
|
43
70
|
|
|
44
|
-
|
|
71
|
+
def inject?(env, status, headers, body)
|
|
72
|
+
RailsWayback.enabled? &&
|
|
73
|
+
request_injectable?(env) &&
|
|
74
|
+
response_injectable?(status, headers) &&
|
|
75
|
+
body_injectable?(body, headers)
|
|
76
|
+
end
|
|
45
77
|
|
|
46
|
-
def
|
|
47
|
-
return false
|
|
48
|
-
return false unless status.to_i.between?(200, 299) || status.to_i.between?(400, 499)
|
|
78
|
+
def request_injectable?(env)
|
|
79
|
+
return false if env["REQUEST_METHOD"].to_s.casecmp?("HEAD")
|
|
49
80
|
|
|
50
81
|
path = env["PATH_INFO"].to_s
|
|
51
|
-
|
|
82
|
+
SKIP_PATH_PREFIXES.none? { |prefix| path.start_with?(prefix) }
|
|
83
|
+
end
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
def response_injectable?(status, headers)
|
|
86
|
+
injectable_status?(status) &&
|
|
87
|
+
html_response?(headers) &&
|
|
88
|
+
identity_encoded?(headers) &&
|
|
89
|
+
!attachment?(headers) &&
|
|
90
|
+
!no_transform?(headers)
|
|
91
|
+
end
|
|
55
92
|
|
|
56
|
-
|
|
93
|
+
def injectable_status?(status)
|
|
94
|
+
code = status.to_i
|
|
95
|
+
(code.between?(200, 299) || code.between?(400, 499)) && !UNSAFE_STATUSES.include?(code)
|
|
57
96
|
end
|
|
58
97
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
def body_injectable?(body, headers)
|
|
99
|
+
UNSAFE_RESPONSE_HEADERS.none? { |name| header_present?(headers, name) } &&
|
|
100
|
+
!body.respond_to?(:to_path) &&
|
|
101
|
+
body.respond_to?(:to_ary) &&
|
|
102
|
+
declared_body_within_limit?(headers)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def html_response?(headers)
|
|
106
|
+
media_type = header_value(headers, "content-type").to_s.split(";", 2).first
|
|
107
|
+
media_type.strip.casecmp?("text/html")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def identity_encoded?(headers)
|
|
111
|
+
value = header_value(headers, "content-encoding").to_s.strip
|
|
112
|
+
return true if value.empty?
|
|
113
|
+
|
|
114
|
+
value.split(",").all? { |encoding| encoding.strip.casecmp?("identity") }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def attachment?(headers)
|
|
118
|
+
header_value(headers, "content-disposition").to_s.match?(/\battachment\b/i)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def no_transform?(headers)
|
|
122
|
+
header_value(headers, "cache-control").to_s.match?(/\bno-transform\b/i)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def declared_body_within_limit?(headers)
|
|
126
|
+
length = integer_header(headers, "content-length")
|
|
127
|
+
return false if length&.zero?
|
|
128
|
+
|
|
129
|
+
limit = max_response_bytes
|
|
130
|
+
limit.nil? || length.nil? || length <= limit
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def body_within_limit?(parts)
|
|
134
|
+
limit = max_response_bytes
|
|
135
|
+
limit.nil? || parts.sum(&:bytesize) <= limit
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def max_response_bytes
|
|
139
|
+
value = RailsWayback.configuration.max_response_bytes
|
|
140
|
+
value.nil? ? nil : Integer(value)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def integer_header(headers, name)
|
|
144
|
+
value = header_value(headers, name)
|
|
145
|
+
value.nil? ? nil : Integer(value, 10)
|
|
146
|
+
rescue ArgumentError, TypeError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def injectable_document?(html)
|
|
151
|
+
html.match?(/<body(?:\s|>)/i) &&
|
|
152
|
+
html.match?(%r{</body\s*>}i) &&
|
|
153
|
+
!html.match?(/\bid=(["'])rails-wayback-bar\1/i)
|
|
65
154
|
end
|
|
66
155
|
|
|
67
156
|
def inject(html, snippet)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
157
|
+
html.sub(%r{</body\s*>}i) { |closing_tag| "#{snippet}#{closing_tag}" }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def repaired_headers(headers, html)
|
|
161
|
+
repaired = headers.dup
|
|
162
|
+
INVALIDATED_RESPONSE_HEADERS.each { |name| delete_header(repaired, name) }
|
|
163
|
+
set_header(repaired, "content-length", html.bytesize.to_s)
|
|
164
|
+
add_no_store(repaired)
|
|
165
|
+
repaired
|
|
73
166
|
end
|
|
74
167
|
|
|
75
|
-
def
|
|
168
|
+
def add_no_store(headers)
|
|
169
|
+
current = header_value(headers, "cache-control").to_s
|
|
170
|
+
return if current.match?(/(?:\A|,)\s*no-store\s*(?:,|\z)/i)
|
|
171
|
+
|
|
172
|
+
set_header(headers, "cache-control", current.empty? ? "no-store" : "#{current}, no-store")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def header_present?(headers, name)
|
|
176
|
+
!header_key(headers, name).nil?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def header_value(headers, name)
|
|
180
|
+
key = header_key(headers, name)
|
|
181
|
+
key ? headers[key] : nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def header_key(headers, name)
|
|
185
|
+
headers.keys.find { |key| key.to_s.casecmp?(name) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def set_header(headers, name, value)
|
|
189
|
+
headers[header_key(headers, name) || name] = value
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def delete_header(headers, name)
|
|
193
|
+
key = header_key(headers, name)
|
|
194
|
+
headers.delete(key) if key
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def build_snippet(env, tracker)
|
|
76
198
|
git = RailsWayback::Git.new
|
|
77
|
-
|
|
78
|
-
|
|
199
|
+
selection = ref_selection(env)
|
|
200
|
+
active_ref = selection&.accepted? ? selection.sha : nil
|
|
201
|
+
active_branch = active_ref ? active_branch_for(env, git, selection) : nil
|
|
79
202
|
|
|
80
203
|
renderer = BarRenderer.new(
|
|
81
204
|
current_branch: safe(:current_branch, git),
|
|
@@ -83,7 +206,9 @@ module RailsWayback
|
|
|
83
206
|
active_ref: active_ref,
|
|
84
207
|
active_branch: active_branch,
|
|
85
208
|
engine_mount: engine_mount,
|
|
86
|
-
diff_info: active_ref ? build_diff_info(git, active_ref) : nil
|
|
209
|
+
diff_info: active_ref ? build_diff_info(git, active_ref, tracker) : nil,
|
|
210
|
+
csp_nonce: content_security_policy_nonce(env),
|
|
211
|
+
ref_error: selection&.rejected? ? selection.message : nil
|
|
87
212
|
)
|
|
88
213
|
renderer.render
|
|
89
214
|
rescue StandardError => e
|
|
@@ -97,38 +222,81 @@ module RailsWayback
|
|
|
97
222
|
""
|
|
98
223
|
end
|
|
99
224
|
|
|
225
|
+
def content_security_policy_nonce(env)
|
|
226
|
+
return unless defined?(ActionDispatch::Request)
|
|
227
|
+
|
|
228
|
+
request = ActionDispatch::Request.new(env)
|
|
229
|
+
request.content_security_policy_nonce if request.respond_to?(:content_security_policy_nonce)
|
|
230
|
+
rescue StandardError
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
100
234
|
# Determines the branch label to show in the bar while traveling.
|
|
101
235
|
# Prefers the branch the user explicitly picked in the bar (passed
|
|
102
236
|
# via the `_wayback_branch` query param, so it survives the reload).
|
|
103
237
|
# Falls back to inferring it from git — useful when a developer opens
|
|
104
238
|
# a shared URL that only carries `_wayback_ref`.
|
|
105
|
-
def active_branch_for(env, git,
|
|
239
|
+
def active_branch_for(env, git, selection)
|
|
106
240
|
explicit = extract_active_branch(env)
|
|
107
|
-
|
|
241
|
+
explicit_ref = trusted_ref_for(explicit, selection.trusted_refs)
|
|
242
|
+
return reference_label(explicit_ref) if explicit_ref
|
|
243
|
+
|
|
244
|
+
inferred = git.resolve_branch_for(selection.sha)
|
|
245
|
+
inferred_ref = "refs/heads/#{inferred}"
|
|
246
|
+
return inferred if inferred && selection.trusted_refs.include?(inferred_ref)
|
|
108
247
|
|
|
109
|
-
|
|
248
|
+
reference_label(selection.trusted_refs.first)
|
|
110
249
|
rescue StandardError
|
|
111
250
|
nil
|
|
112
251
|
end
|
|
113
252
|
|
|
253
|
+
def trusted_ref_for(selector, trusted_refs)
|
|
254
|
+
return if selector.to_s.empty?
|
|
255
|
+
return selector if trusted_refs.include?(selector)
|
|
256
|
+
|
|
257
|
+
trusted_refs.find do |reference|
|
|
258
|
+
reference_name(reference) == selector || reference_label(reference) == selector
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def reference_name(reference)
|
|
263
|
+
reference.to_s.sub(%r{\Arefs/(?:heads|tags|remotes)/}, "")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def reference_label(reference)
|
|
267
|
+
return if reference.to_s.empty?
|
|
268
|
+
|
|
269
|
+
name = reference_name(reference)
|
|
270
|
+
reference.start_with?("refs/tags/") ? "tag:#{name}" : name
|
|
271
|
+
end
|
|
272
|
+
|
|
114
273
|
# Builds the diff summary shown in the bar when a ref is active.
|
|
115
274
|
# `changed_files`: view/asset paths that differ between the ref and
|
|
116
275
|
# the current working tree, always scoped to the paths the gem
|
|
117
|
-
# actually swaps.
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
def build_diff_info(git, ref)
|
|
276
|
+
# actually swaps. Rendered paths are split by origin so the toolbar can
|
|
277
|
+
# warn when a successful page silently mixes historical templates with
|
|
278
|
+
# current-tree fallbacks.
|
|
279
|
+
def build_diff_info(git, ref, tracker)
|
|
122
280
|
config = RailsWayback.configuration
|
|
123
281
|
tracked_paths = (config.view_paths + config.asset_paths).uniq
|
|
124
282
|
changed_files = git.diff_paths(ref, paths: tracked_paths)
|
|
125
283
|
|
|
126
|
-
|
|
284
|
+
rendered = RailsWayback::RenderProvenance.paths_by_origin(
|
|
285
|
+
tracker.entries,
|
|
286
|
+
configuration: config
|
|
287
|
+
)
|
|
288
|
+
rendered_from_ref = rendered[:historical]
|
|
289
|
+
rendered_from_current = rendered[:current]
|
|
290
|
+
assets = RailsWayback::AssetProvenance.paths_by_origin(tracker.entries)
|
|
127
291
|
matched = changed_files & rendered_from_ref
|
|
128
292
|
|
|
129
293
|
{
|
|
130
294
|
changed_files: changed_files,
|
|
131
295
|
rendered_from_ref: rendered_from_ref,
|
|
296
|
+
rendered_from_current: rendered_from_current,
|
|
297
|
+
historical_assets: assets[:historical],
|
|
298
|
+
current_asset_fallbacks: assets[:current],
|
|
299
|
+
preview_mode: preview_mode(rendered_from_ref, rendered_from_current),
|
|
132
300
|
matched: matched
|
|
133
301
|
}
|
|
134
302
|
rescue StandardError => e
|
|
@@ -136,21 +304,12 @@ module RailsWayback
|
|
|
136
304
|
nil
|
|
137
305
|
end
|
|
138
306
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
return
|
|
307
|
+
def preview_mode(historical, current)
|
|
308
|
+
return :mixed if historical.any? && current.any?
|
|
309
|
+
return :historical if historical.any?
|
|
310
|
+
return :current_fallback if current.any?
|
|
142
311
|
|
|
143
|
-
|
|
144
|
-
# Sandbox identifiers look like:
|
|
145
|
-
# /.../tmp/rails_wayback/refs/<sha>/app/views/foo/_bar.html.haml
|
|
146
|
-
# Strip up to and including the sha directory to get back
|
|
147
|
-
# `app/views/foo/_bar.html.haml`, which matches `git diff --name-only`.
|
|
148
|
-
strip_re = %r{\A#{Regexp.escape(sandbox_prefix)}/[^/]+/}
|
|
149
|
-
|
|
150
|
-
tracker.filter_map do |identifier|
|
|
151
|
-
next unless identifier.start_with?(sandbox_prefix)
|
|
152
|
-
identifier.sub(strip_re, "")
|
|
153
|
-
end.uniq
|
|
312
|
+
:unknown
|
|
154
313
|
end
|
|
155
314
|
|
|
156
315
|
# Query params take priority (one-off overrides / shared links);
|
|
@@ -161,6 +320,17 @@ module RailsWayback
|
|
|
161
320
|
extract_cookie(env, "rails_wayback_ref")
|
|
162
321
|
end
|
|
163
322
|
|
|
323
|
+
def ref_selection(env)
|
|
324
|
+
return env[RailsWayback::RefPolicy::ENV_KEY] if env.key?(RailsWayback::RefPolicy::ENV_KEY)
|
|
325
|
+
|
|
326
|
+
ref = extract_active_ref(env)
|
|
327
|
+
return unless ref
|
|
328
|
+
|
|
329
|
+
RailsWayback::RefPolicy.new.authorize(ref).tap do |selection|
|
|
330
|
+
env[RailsWayback::RefPolicy::ENV_KEY] = selection
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
164
334
|
def extract_active_branch(env)
|
|
165
335
|
extract_query_param(env, "_wayback_branch") ||
|
|
166
336
|
extract_cookie(env, "rails_wayback_branch")
|
|
@@ -169,6 +339,7 @@ module RailsWayback
|
|
|
169
339
|
def extract_query_param(env, name)
|
|
170
340
|
query = env["QUERY_STRING"].to_s
|
|
171
341
|
return nil if query.empty?
|
|
342
|
+
|
|
172
343
|
prefix = "#{name}="
|
|
173
344
|
match = query.split("&").find { |pair| pair.start_with?(prefix) }
|
|
174
345
|
return nil unless match
|
|
@@ -180,6 +351,7 @@ module RailsWayback
|
|
|
180
351
|
def extract_cookie(env, name)
|
|
181
352
|
header = env["HTTP_COOKIE"].to_s
|
|
182
353
|
return nil if header.empty?
|
|
354
|
+
|
|
183
355
|
prefix = "#{name}="
|
|
184
356
|
match = header.split(/;\s*/).find { |pair| pair.start_with?(prefix) }
|
|
185
357
|
return nil unless match
|
|
@@ -189,11 +361,7 @@ module RailsWayback
|
|
|
189
361
|
end
|
|
190
362
|
|
|
191
363
|
def engine_mount
|
|
192
|
-
RailsWayback::
|
|
193
|
-
RailsWayback::Engine.routes.find_script_name({}) ||
|
|
194
|
-
"/rails-wayback"
|
|
195
|
-
rescue StandardError
|
|
196
|
-
"/rails-wayback"
|
|
364
|
+
RailsWayback::EngineMount.path
|
|
197
365
|
end
|
|
198
366
|
end
|
|
199
367
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#rails-wayback-bar {
|
|
2
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483000;
|
|
3
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
4
|
+
font-size: 12px; color: #e6e9ef;
|
|
5
|
+
background: rgba(15, 17, 21, 0.96);
|
|
6
|
+
border-top: 1px solid #2a2f3a;
|
|
7
|
+
box-shadow: 0 -8px 20px rgba(0,0,0,.35);
|
|
8
|
+
}
|
|
9
|
+
#rails-wayback-bar * { box-sizing: border-box; }
|
|
10
|
+
#rails-wayback-bar .rw-bar-inner {
|
|
11
|
+
display: flex; align-items: end; gap: 12px; padding: 8px 12px;
|
|
12
|
+
}
|
|
13
|
+
#rails-wayback-bar .rw-field { display: flex; flex-direction: column; gap: 3px; min-width: 180px; }
|
|
14
|
+
#rails-wayback-bar .rw-flex { flex: 1; }
|
|
15
|
+
#rails-wayback-bar .rw-selector { display: flex; gap: 4px; }
|
|
16
|
+
#rails-wayback-bar label {
|
|
17
|
+
color: #8a93a6; text-transform: uppercase; letter-spacing: .06em; font-size: 10px;
|
|
18
|
+
}
|
|
19
|
+
#rails-wayback-bar select,
|
|
20
|
+
#rails-wayback-bar input[type="search"] {
|
|
21
|
+
background: #0b0d12; color: #e6e9ef;
|
|
22
|
+
border: 1px solid #2a2f3a; border-radius: 4px;
|
|
23
|
+
padding: 5px 8px; font-family: inherit; font-size: 12px; width: 100%;
|
|
24
|
+
}
|
|
25
|
+
#rails-wayback-bar input[type="search"] { flex: 0 1 150px; min-width: 90px; }
|
|
26
|
+
#rails-wayback-bar select { flex: 1; min-width: 0; }
|
|
27
|
+
#rails-wayback-bar select:focus-visible,
|
|
28
|
+
#rails-wayback-bar input[type="search"]:focus-visible {
|
|
29
|
+
border-color: #6ea8fe; outline: 2px solid rgba(110, 168, 254, .35); outline-offset: 1px;
|
|
30
|
+
}
|
|
31
|
+
#rails-wayback-bar input[type="search"][aria-invalid="true"] { border-color: #ff7b72; }
|
|
32
|
+
#rails-wayback-bar select:disabled,
|
|
33
|
+
#rails-wayback-bar input[type="search"]:disabled { opacity: .55; }
|
|
34
|
+
#rails-wayback-bar .rw-search-status { color: #8a93a6; font-size: 9px; min-height: 10px; }
|
|
35
|
+
#rails-wayback-bar .rw-state { color: #8a93a6; font-size: 11px; margin: 0 4px; }
|
|
36
|
+
#rails-wayback-bar .rw-state.rw-active { color: #ffd479; }
|
|
37
|
+
#rails-wayback-bar .rw-btn {
|
|
38
|
+
border: 0; border-radius: 4px; padding: 8px 14px; cursor: pointer;
|
|
39
|
+
font-family: inherit; font-size: 12px; font-weight: 600;
|
|
40
|
+
}
|
|
41
|
+
#rails-wayback-bar .rw-btn-primary { background: #6ea8fe; color: #0b0d12; }
|
|
42
|
+
#rails-wayback-bar .rw-btn-ghost { background: transparent; color: #8a93a6; border: 1px solid #2a2f3a; }
|
|
43
|
+
#rails-wayback-bar .rw-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
44
|
+
#rails-wayback-bar .rw-trust-notice {
|
|
45
|
+
border-top: 1px solid #2a2f3a; padding: 5px 12px;
|
|
46
|
+
color: #ffb86b; font-size: 10px;
|
|
47
|
+
}
|
|
48
|
+
#rails-wayback-bar .rw-ref-rejected { margin-left: 8px; color: #ff7b72; }
|
|
49
|
+
#rails-wayback-bar .rw-diff {
|
|
50
|
+
border-top: 1px solid #2a2f3a;
|
|
51
|
+
padding: 6px 12px; font-size: 11px; color: #8a93a6;
|
|
52
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
53
|
+
}
|
|
54
|
+
#rails-wayback-bar .rw-diff-match .rw-diff-msg { color: #7ee787; }
|
|
55
|
+
#rails-wayback-bar .rw-diff-miss .rw-diff-msg { color: #ffd479; }
|
|
56
|
+
#rails-wayback-bar .rw-diff-neutral .rw-diff-msg { color: #8a93a6; }
|
|
57
|
+
#rails-wayback-bar .rw-provenance { font-weight: 600; }
|
|
58
|
+
#rails-wayback-bar .rw-provenance-historical { color: #7ee787; }
|
|
59
|
+
#rails-wayback-bar .rw-provenance-mixed,
|
|
60
|
+
#rails-wayback-bar .rw-provenance-current { color: #ffb86b; }
|
|
61
|
+
#rails-wayback-bar .rw-diff-details { color: #8a93a6; }
|
|
62
|
+
#rails-wayback-bar .rw-diff-details summary {
|
|
63
|
+
cursor: pointer; user-select: none;
|
|
64
|
+
}
|
|
65
|
+
#rails-wayback-bar .rw-diff-details[open] {
|
|
66
|
+
flex-basis: 100%;
|
|
67
|
+
}
|
|
68
|
+
#rails-wayback-bar .rw-diff-details ul {
|
|
69
|
+
margin: 6px 0 0; padding: 6px 10px; list-style: none;
|
|
70
|
+
background: #0b0d12; border: 1px solid #2a2f3a; border-radius: 4px;
|
|
71
|
+
max-height: 200px; overflow: auto;
|
|
72
|
+
font-family: inherit; font-size: 11px;
|
|
73
|
+
}
|
|
74
|
+
#rails-wayback-bar .rw-diff-details li { padding: 2px 0; color: #c8cfdc; }
|
|
75
|
+
body { padding-bottom: 96px !important; }
|
|
76
|
+
body:has(#rails-wayback-bar .rw-diff) { padding-bottom: 128px !important; }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<link rel="stylesheet" href="<%= escape(stylesheet_url) %>" data-rails-wayback<%= nonce_attribute %>>
|
|
2
|
+
<div id="rails-wayback-bar" data-mount="<%= escape(@engine_mount) %>"
|
|
3
|
+
data-branch="<%= escape(@current_branch) %>"
|
|
4
|
+
data-commit="<%= escape(@current_commit) %>"
|
|
5
|
+
data-active-ref="<%= escape(@active_ref.to_s) %>"
|
|
6
|
+
data-active-branch="<%= escape(@active_branch.to_s) %>"
|
|
7
|
+
data-ref-error="<%= escape(@ref_error) %>"
|
|
8
|
+
data-preview-mode="<%= escape(@diff_info&.dig(:preview_mode).to_s) %>">
|
|
9
|
+
<div class="rw-bar-inner">
|
|
10
|
+
<div class="rw-field">
|
|
11
|
+
<label for="rails-wayback-branches"><%= escape(branch_label) %></label>
|
|
12
|
+
<div class="rw-selector">
|
|
13
|
+
<input type="search" data-rw-branch-search aria-label="Search Git refs"
|
|
14
|
+
aria-controls="rails-wayback-branches" placeholder="Search refs"
|
|
15
|
+
autocomplete="off" spellcheck="false" disabled>
|
|
16
|
+
<select id="rails-wayback-branches" data-rw-branch aria-label="Git ref" disabled></select>
|
|
17
|
+
</div>
|
|
18
|
+
<span class="rw-search-status" data-rw-branch-results role="status" aria-live="polite"></span>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="rw-field rw-flex">
|
|
21
|
+
<label for="rails-wayback-commits"><%= escape(commit_label) %></label>
|
|
22
|
+
<div class="rw-selector">
|
|
23
|
+
<input type="search" data-rw-commit-search aria-label="Search commits"
|
|
24
|
+
aria-controls="rails-wayback-commits" placeholder="Search commits"
|
|
25
|
+
autocomplete="off" spellcheck="false" disabled>
|
|
26
|
+
<select id="rails-wayback-commits" data-rw-commit aria-label="Git commit" disabled></select>
|
|
27
|
+
</div>
|
|
28
|
+
<span class="rw-search-status" data-rw-commit-results role="status" aria-live="polite"></span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="rw-state" data-rw-state></div>
|
|
31
|
+
<button type="button" data-rw-travel class="rw-btn rw-btn-primary">Travel</button>
|
|
32
|
+
<button type="button" data-rw-reset class="rw-btn rw-btn-ghost" hidden>Return to HEAD</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="rw-trust-notice" data-rw-trust-notice>
|
|
35
|
+
Historical templates execute Ruby in this process. Only travel to commits you trust.
|
|
36
|
+
Historical JavaScript executes in this application's browser origin.
|
|
37
|
+
<% unless @ref_error.empty? %>
|
|
38
|
+
<strong class="rw-ref-rejected"><%= escape(@ref_error) %></strong>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
<%= diff_summary_html %>
|
|
42
|
+
</div>
|
|
43
|
+
<script src="<%= escape(javascript_url) %>" defer data-rails-wayback<%= nonce_attribute %>></script>
|