rails-wayback 0.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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +172 -0
- data/Rakefile +11 -0
- data/app/controllers/rails_wayback/application_controller.rb +19 -0
- data/app/controllers/rails_wayback/pages_controller.rb +53 -0
- data/config/routes.rb +10 -0
- data/exe/rails-wayback +8 -0
- data/lib/generators/rails_wayback/install/USAGE +8 -0
- data/lib/generators/rails_wayback/install/install_generator.rb +39 -0
- data/lib/generators/rails_wayback/install/templates/initializer.rb.tt +18 -0
- data/lib/rails-wayback.rb +3 -0
- data/lib/rails_wayback/bar_middleware.rb +199 -0
- data/lib/rails_wayback/bar_renderer.rb +301 -0
- data/lib/rails_wayback/cli.rb +78 -0
- data/lib/rails_wayback/configuration.rb +42 -0
- data/lib/rails_wayback/controller_extensions.rb +94 -0
- data/lib/rails_wayback/engine.rb +49 -0
- data/lib/rails_wayback/git.rb +140 -0
- data/lib/rails_wayback/toggle.rb +53 -0
- data/lib/rails_wayback/version.rb +5 -0
- data/lib/rails_wayback/view_source.rb +99 -0
- data/lib/rails_wayback.rb +59 -0
- data/lib/tasks/rails_wayback.rake +25 -0
- data/rails-wayback.gemspec +44 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 837e9fe96c57c95c97120d9763ce5f479f96de05ef809efd9ff28489a4747e33
|
|
4
|
+
data.tar.gz: 1123f82ce9d8e4a16b71fa318c881ca2fc0ec3a8ca50556a4bc3da720e1cab67
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 289a30da1575e6f95f57c15ea74172d2ae7b10a97a396d18ea449008300f2345583a39ae853e0bd1e3d28281de72465e10ec9e5d2b656974c697c547a467ac40
|
|
7
|
+
data.tar.gz: 67f4833b49c5c36381f672b8b48ec2368db8ef66bc04a10e5f70feb11fbf9c4c2beba98a0a311a46506fce8f942f037e90940b2002d381653842dbf24383e4c5
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MrCesar107
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# rails-wayback
|
|
2
|
+
|
|
3
|
+
Preview and compare Rails UI across git branches and commits, directly in the
|
|
4
|
+
browser, from inside the developer's own Rails process. Toggle it on and off
|
|
5
|
+
from the terminal so it never gets in the way of normal development.
|
|
6
|
+
|
|
7
|
+
## Idea
|
|
8
|
+
|
|
9
|
+
When enabled, `rails-wayback` overlays a small bar at the bottom of every HTML
|
|
10
|
+
page of your app:
|
|
11
|
+
|
|
12
|
+
- Pick a branch and a commit.
|
|
13
|
+
- Hit **Travel**.
|
|
14
|
+
- The current URL reloads with `?_wayback_ref=<sha>`, and the app renders
|
|
15
|
+
that exact page using the **views/layouts/partials of the chosen ref**,
|
|
16
|
+
while keeping the current controller's data and models.
|
|
17
|
+
|
|
18
|
+
Design goals:
|
|
19
|
+
|
|
20
|
+
- **Zero configuration.** Drop the gem in, run the installer, turn it on.
|
|
21
|
+
- **One process only.** Uses the running dev app — no extra Rails server.
|
|
22
|
+
- **HTML render, not screenshots.**
|
|
23
|
+
- **Toggleable from the CLI.** Stays inert when you don't need it.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Add the gem to the host app's `Gemfile`, in the development group:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
group :development do
|
|
31
|
+
gem "rails-wayback"
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then run the installer:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bin/rails generate rails_wayback:install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That adds a conditional mount to `config/routes.rb`, an initializer at
|
|
42
|
+
`config/initializers/rails_wayback.rb` (optional overrides), and a
|
|
43
|
+
`.gitignore` entry for `tmp/rails_wayback/`.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
Turn the UI on when you need it, off when you don't:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bundle exec rails-wayback on # enable and inject the bar in every page
|
|
51
|
+
bundle exec rails-wayback off # disable it
|
|
52
|
+
bundle exec rails-wayback status # print current state
|
|
53
|
+
bundle exec rails-wayback clean # drop the tmp/rails_wayback ref cache
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Same commands as rake tasks: `rake wayback:on`, `wayback:off`,
|
|
57
|
+
`wayback:status`, `wayback:clean`.
|
|
58
|
+
|
|
59
|
+
With the gem enabled, load any page of your app. A dark bar appears at the
|
|
60
|
+
bottom with:
|
|
61
|
+
|
|
62
|
+
- `Current branch` — dropdown of your local branches.
|
|
63
|
+
- `Current commit` — dropdown of the recent commits on that branch.
|
|
64
|
+
- `Travel` — reloads the current URL using views from that commit.
|
|
65
|
+
- `Return to HEAD` — appears while traveling; goes back to live views.
|
|
66
|
+
|
|
67
|
+
There is no dedicated `/rails-wayback` page. The gem stays quiet on assets,
|
|
68
|
+
ActionCable, and every URL under `/rails-wayback/*.json` (used by the bar to
|
|
69
|
+
fetch git data).
|
|
70
|
+
|
|
71
|
+
## How "travel" actually works
|
|
72
|
+
|
|
73
|
+
`rails-wayback` never runs a second Rails server. Travel is just:
|
|
74
|
+
|
|
75
|
+
1. The bar appends `?_wayback_ref=<sha>` to the current URL and reloads.
|
|
76
|
+
2. A `before_action`, auto-included in every `ActionController::Base`
|
|
77
|
+
subclass, sees `_wayback_ref` and materialises that ref under
|
|
78
|
+
`tmp/rails_wayback/refs/<sha>/` (cached per commit).
|
|
79
|
+
3. The same before_action calls `prepend_view_path` with the sandbox, so
|
|
80
|
+
Action View resolves templates from the ref before falling back to the
|
|
81
|
+
current tree.
|
|
82
|
+
4. Your controller runs as usual, sets its `@` variables, and renders. The
|
|
83
|
+
visual layer (layouts, partials, ERB) comes from the past; the data and
|
|
84
|
+
business logic come from the present.
|
|
85
|
+
|
|
86
|
+
If a historical template references a helper or assign that no longer
|
|
87
|
+
exists, you'll get the same `ActionView` error you'd get from any missing
|
|
88
|
+
piece — the app itself never changes.
|
|
89
|
+
|
|
90
|
+
## Configuration (optional)
|
|
91
|
+
|
|
92
|
+
`config/initializers/rails_wayback.rb`:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
RailsWayback.configure do |config|
|
|
96
|
+
# config.view_paths = %w[app/views]
|
|
97
|
+
# config.asset_paths = %w[app/assets public]
|
|
98
|
+
# config.max_commits = 50
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Everything above has a sensible default. Delete the initializer if you
|
|
103
|
+
don't need to change anything.
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bundle install
|
|
109
|
+
bundle exec rspec
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Release
|
|
113
|
+
|
|
114
|
+
Releases are cut by pushing an annotated `vX.Y.Z` git tag. GitHub Actions runs
|
|
115
|
+
the specs and pushes the gem to RubyGems using
|
|
116
|
+
[Trusted Publishing (OIDC)](https://guides.rubygems.org/trusted-publishing/),
|
|
117
|
+
so there are no API tokens stored on GitHub and MFA on the RubyGems account is
|
|
118
|
+
never in the way.
|
|
119
|
+
|
|
120
|
+
### One-time setup
|
|
121
|
+
|
|
122
|
+
1. On GitHub, go to **Settings → Environments → New environment** and create
|
|
123
|
+
an environment named exactly `release`. (Optionally add branch protection
|
|
124
|
+
so only `main` and tags can deploy to it.)
|
|
125
|
+
2. On [rubygems.org](https://rubygems.org):
|
|
126
|
+
- For the very first release, go to **Profile → Trusted publishers →
|
|
127
|
+
Pending trusted publishers → Create** and fill in:
|
|
128
|
+
- RubyGem name: `rails-wayback`
|
|
129
|
+
- Repository owner: `MrCesar107`
|
|
130
|
+
- Repository name: `rails-wayback`
|
|
131
|
+
- Workflow filename: `release.yml`
|
|
132
|
+
- Environment: `release`
|
|
133
|
+
- For every release after 0.1.0, the pending publisher is promoted to a
|
|
134
|
+
normal trusted publisher automatically — nothing to configure again.
|
|
135
|
+
3. Make sure MFA is enabled on your RubyGems account (the gemspec sets
|
|
136
|
+
`rubygems_mfa_required = true`).
|
|
137
|
+
|
|
138
|
+
### Cutting a release
|
|
139
|
+
|
|
140
|
+
1. Update `lib/rails_wayback/version.rb` following
|
|
141
|
+
[SemVer](https://semver.org/spec/v2.0.0.html).
|
|
142
|
+
2. Move the pending entries in `CHANGELOG.md` from `[Unreleased]` into a new
|
|
143
|
+
`[X.Y.Z] - YYYY-MM-DD` section and update the compare/tag links at the
|
|
144
|
+
bottom of the file.
|
|
145
|
+
3. Commit both files:
|
|
146
|
+
```bash
|
|
147
|
+
git commit -am "Release vX.Y.Z"
|
|
148
|
+
```
|
|
149
|
+
4. Tag and push:
|
|
150
|
+
```bash
|
|
151
|
+
git tag -a vX.Y.Z -m "Release vX.Y.Z"
|
|
152
|
+
git push origin main --follow-tags
|
|
153
|
+
```
|
|
154
|
+
5. Watch the `Release` workflow on GitHub. It refuses to publish if the tag
|
|
155
|
+
doesn't match `RailsWayback::VERSION`, so a typo in the tag will fail
|
|
156
|
+
loudly before touching RubyGems.
|
|
157
|
+
|
|
158
|
+
### Manual fallback
|
|
159
|
+
|
|
160
|
+
If the workflow is broken and you need to publish from your laptop:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
gem build rails-wayback.gemspec
|
|
164
|
+
gem push rails-wayback-X.Y.Z.gem
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`gem push` will ask for your RubyGems credentials and MFA code the first
|
|
168
|
+
time.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsWayback
|
|
4
|
+
class ApplicationController < ::ActionController::API
|
|
5
|
+
before_action :ensure_enabled!
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def ensure_enabled!
|
|
10
|
+
return if RailsWayback.enabled?
|
|
11
|
+
|
|
12
|
+
render json: { error: "rails-wayback is disabled" }, status: :service_unavailable
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def git
|
|
16
|
+
@git ||= RailsWayback::Git.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsWayback
|
|
4
|
+
# JSON endpoints consumed by the bar's JavaScript to populate the
|
|
5
|
+
# branch and commit selectors. All rendering of the actual UI lives
|
|
6
|
+
# in the middleware; this controller only exposes git metadata.
|
|
7
|
+
class PagesController < ApplicationController
|
|
8
|
+
def branches
|
|
9
|
+
current_branch = safe_call { git.current_branch }
|
|
10
|
+
current_commit = safe_call { git.current_commit }
|
|
11
|
+
branches = safe_call { git.branches }
|
|
12
|
+
|
|
13
|
+
render json: {
|
|
14
|
+
current_branch: current_branch.value,
|
|
15
|
+
current_commit: current_commit.value,
|
|
16
|
+
branches: branches.value || [],
|
|
17
|
+
errors: [branches.error].compact
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def commits
|
|
22
|
+
branch = params[:branch].to_s.sub(/\.json\z/, "")
|
|
23
|
+
result = safe_call { git.commits(branch) }
|
|
24
|
+
|
|
25
|
+
list = (result.value || []).map do |commit|
|
|
26
|
+
{
|
|
27
|
+
sha: commit.sha,
|
|
28
|
+
short_sha: commit.short_sha,
|
|
29
|
+
subject: commit.subject,
|
|
30
|
+
author: commit.author,
|
|
31
|
+
date: commit.date
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
render json: {
|
|
36
|
+
branch: branch,
|
|
37
|
+
commits: list,
|
|
38
|
+
error: result.error
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
Result = Struct.new(:value, :error)
|
|
45
|
+
|
|
46
|
+
def safe_call
|
|
47
|
+
Result.new(yield, nil)
|
|
48
|
+
rescue RailsWayback::Git::GitError => e
|
|
49
|
+
Rails.logger.warn("[rails-wayback] #{e.class}: #{e.message}") if defined?(Rails)
|
|
50
|
+
Result.new(nil, "#{e.class}: #{e.message}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RailsWayback::Engine.routes.draw do
|
|
4
|
+
# `format: false` keeps Rails from parsing a trailing `.json` as an
|
|
5
|
+
# implicit format, and the glob `*branch` allows branch names that
|
|
6
|
+
# contain slashes (e.g. "feature/foo"). The controller trims a
|
|
7
|
+
# trailing `.json` defensively in case a client still appends it.
|
|
8
|
+
get "branches", to: "pages#branches", format: false, as: :branches
|
|
9
|
+
get "commits/*branch", to: "pages#commits", format: false, as: :commits
|
|
10
|
+
end
|
data/exe/rails-wayback
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsWayback
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def copy_initializer
|
|
11
|
+
template "initializer.rb.tt", "config/initializers/rails_wayback.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mount_engine
|
|
15
|
+
route_snippet = <<~ROUTE
|
|
16
|
+
mount RailsWayback::Engine => "/rails-wayback" if defined?(RailsWayback) && RailsWayback.enabled?
|
|
17
|
+
ROUTE
|
|
18
|
+
|
|
19
|
+
routes_file = "config/routes.rb"
|
|
20
|
+
|
|
21
|
+
if File.file?(routes_file) && File.read(routes_file).include?("RailsWayback::Engine")
|
|
22
|
+
say_status :skip, "RailsWayback::Engine mount already present in #{routes_file}"
|
|
23
|
+
else
|
|
24
|
+
route route_snippet.strip
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update_gitignore
|
|
29
|
+
gitignore = ".gitignore"
|
|
30
|
+
entry = "/tmp/rails_wayback/\n"
|
|
31
|
+
if File.file?(gitignore) && !File.read(gitignore).include?("tmp/rails_wayback")
|
|
32
|
+
append_to_file(gitignore, entry)
|
|
33
|
+
elsif !File.file?(gitignore)
|
|
34
|
+
create_file(gitignore, entry)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Optional configuration for the rails-wayback engine.
|
|
4
|
+
#
|
|
5
|
+
# The engine is only mounted when it is toggled on from the terminal
|
|
6
|
+
# (see `bundle exec rails-wayback on|off|status`). When it is on, a
|
|
7
|
+
# floating bar is injected at the bottom of every HTML page of your app
|
|
8
|
+
# so you can pick a branch/commit and travel to that ref's views for
|
|
9
|
+
# the current URL. Delete this file if the defaults below work for you.
|
|
10
|
+
RailsWayback.configure do |config|
|
|
11
|
+
# Directories (relative to Rails.root) whose contents are pulled from
|
|
12
|
+
# the target git ref and used to render the historical preview.
|
|
13
|
+
# config.view_paths = %w[app/views]
|
|
14
|
+
# config.asset_paths = %w[app/assets public]
|
|
15
|
+
|
|
16
|
+
# How many recent commits per branch are shown in the bar dropdown.
|
|
17
|
+
# config.max_commits = 50
|
|
18
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "rails_wayback/bar_renderer"
|
|
5
|
+
|
|
6
|
+
module RailsWayback
|
|
7
|
+
# Rack middleware that appends the wayback bar to every successful
|
|
8
|
+
# HTML response of the host application while the gem is enabled.
|
|
9
|
+
# Skips assets, ActionCable, non-2xx responses and non-HTML bodies so
|
|
10
|
+
# it never touches anything but the developer-facing pages.
|
|
11
|
+
class BarMiddleware
|
|
12
|
+
SKIP_PATH_PREFIXES = %w[
|
|
13
|
+
/assets
|
|
14
|
+
/packs
|
|
15
|
+
/rails/
|
|
16
|
+
/cable
|
|
17
|
+
/rails-wayback
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(app)
|
|
21
|
+
@app = app
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
# Always reset the per-request render tracker before invoking the
|
|
26
|
+
# app so the middleware can accurately report which templates the
|
|
27
|
+
# current page rendered. Cheap and thread-safe (Thread.current).
|
|
28
|
+
Thread.current[RailsWayback::RENDER_TRACKER_KEY] = []
|
|
29
|
+
|
|
30
|
+
status, headers, body = @app.call(env)
|
|
31
|
+
return [status, headers, body] unless inject?(env, status, headers)
|
|
32
|
+
|
|
33
|
+
html = read_body(body)
|
|
34
|
+
snippet = build_snippet(env)
|
|
35
|
+
new_html = inject(html, snippet)
|
|
36
|
+
|
|
37
|
+
new_headers = headers.dup
|
|
38
|
+
length_key = new_headers.key?("Content-Length") ? "Content-Length" : (new_headers.key?("content-length") ? "content-length" : nil)
|
|
39
|
+
new_headers[length_key] = new_html.bytesize.to_s if length_key
|
|
40
|
+
|
|
41
|
+
[status, new_headers, [new_html]]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def inject?(env, status, headers)
|
|
47
|
+
return false unless RailsWayback.enabled?
|
|
48
|
+
return false unless status.to_i.between?(200, 299) || status.to_i.between?(400, 499)
|
|
49
|
+
|
|
50
|
+
path = env["PATH_INFO"].to_s
|
|
51
|
+
return false if SKIP_PATH_PREFIXES.any? { |prefix| path.start_with?(prefix) }
|
|
52
|
+
|
|
53
|
+
content_type = headers["Content-Type"] || headers["content-type"] || ""
|
|
54
|
+
return false unless content_type.include?("text/html")
|
|
55
|
+
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def read_body(body)
|
|
60
|
+
buffer = +""
|
|
61
|
+
body.each { |chunk| buffer << chunk.to_s }
|
|
62
|
+
buffer
|
|
63
|
+
ensure
|
|
64
|
+
body.close if body.respond_to?(:close)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inject(html, snippet)
|
|
68
|
+
if html.include?("</body>")
|
|
69
|
+
html.sub("</body>", "#{snippet}</body>")
|
|
70
|
+
else
|
|
71
|
+
html + snippet
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_snippet(env)
|
|
76
|
+
git = RailsWayback::Git.new
|
|
77
|
+
active_ref = extract_active_ref(env)
|
|
78
|
+
active_branch = active_ref ? active_branch_for(env, git, active_ref) : nil
|
|
79
|
+
|
|
80
|
+
renderer = BarRenderer.new(
|
|
81
|
+
current_branch: safe(:current_branch, git),
|
|
82
|
+
current_commit: safe(:current_commit, git),
|
|
83
|
+
active_ref: active_ref,
|
|
84
|
+
active_branch: active_branch,
|
|
85
|
+
engine_mount: engine_mount,
|
|
86
|
+
diff_info: active_ref ? build_diff_info(git, active_ref) : nil
|
|
87
|
+
)
|
|
88
|
+
renderer.render
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
Rails.logger.warn("[rails-wayback] failed to render bar: #{e.class}: #{e.message}") if defined?(Rails)
|
|
91
|
+
""
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def safe(method, git)
|
|
95
|
+
git.public_send(method)
|
|
96
|
+
rescue StandardError
|
|
97
|
+
""
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Determines the branch label to show in the bar while traveling.
|
|
101
|
+
# Prefers the branch the user explicitly picked in the bar (passed
|
|
102
|
+
# via the `_wayback_branch` query param, so it survives the reload).
|
|
103
|
+
# Falls back to inferring it from git — useful when a developer opens
|
|
104
|
+
# a shared URL that only carries `_wayback_ref`.
|
|
105
|
+
def active_branch_for(env, git, ref)
|
|
106
|
+
explicit = extract_active_branch(env)
|
|
107
|
+
return explicit if explicit && !explicit.empty?
|
|
108
|
+
|
|
109
|
+
git.resolve_branch_for(ref)
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Builds the diff summary shown in the bar when a ref is active.
|
|
115
|
+
# `changed_files`: view/asset paths that differ between the ref and
|
|
116
|
+
# the current working tree, always scoped to the paths the gem
|
|
117
|
+
# actually swaps. `rendered_from_ref`: relative paths (under
|
|
118
|
+
# app/views, app/components, ...) that the current request rendered
|
|
119
|
+
# from the sandbox. `matched`: intersection — the templates on this
|
|
120
|
+
# page that actually reflect a real diff between branches.
|
|
121
|
+
def build_diff_info(git, ref)
|
|
122
|
+
config = RailsWayback.configuration
|
|
123
|
+
tracked_paths = (config.view_paths + config.asset_paths).uniq
|
|
124
|
+
changed_files = git.diff_paths(ref, paths: tracked_paths)
|
|
125
|
+
|
|
126
|
+
rendered_from_ref = rendered_relative_paths(config)
|
|
127
|
+
matched = changed_files & rendered_from_ref
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
changed_files: changed_files,
|
|
131
|
+
rendered_from_ref: rendered_from_ref,
|
|
132
|
+
matched: matched
|
|
133
|
+
}
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
Rails.logger.warn("[rails-wayback] failed to build diff info: #{e.class}: #{e.message}") if defined?(Rails)
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def rendered_relative_paths(config)
|
|
140
|
+
tracker = Array(Thread.current[RailsWayback::RENDER_TRACKER_KEY])
|
|
141
|
+
return [] if tracker.empty?
|
|
142
|
+
|
|
143
|
+
sandbox_prefix = config.refs_cache_path.to_s
|
|
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
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Query params take priority (one-off overrides / shared links);
|
|
157
|
+
# cookies keep the travel state alive as the developer navigates
|
|
158
|
+
# around the app without repeating the params in every URL.
|
|
159
|
+
def extract_active_ref(env)
|
|
160
|
+
extract_query_param(env, "_wayback_ref") ||
|
|
161
|
+
extract_cookie(env, "rails_wayback_ref")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def extract_active_branch(env)
|
|
165
|
+
extract_query_param(env, "_wayback_branch") ||
|
|
166
|
+
extract_cookie(env, "rails_wayback_branch")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def extract_query_param(env, name)
|
|
170
|
+
query = env["QUERY_STRING"].to_s
|
|
171
|
+
return nil if query.empty?
|
|
172
|
+
prefix = "#{name}="
|
|
173
|
+
match = query.split("&").find { |pair| pair.start_with?(prefix) }
|
|
174
|
+
return nil unless match
|
|
175
|
+
|
|
176
|
+
value = CGI.unescape(match.split("=", 2).last.to_s)
|
|
177
|
+
value.empty? ? nil : value
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def extract_cookie(env, name)
|
|
181
|
+
header = env["HTTP_COOKIE"].to_s
|
|
182
|
+
return nil if header.empty?
|
|
183
|
+
prefix = "#{name}="
|
|
184
|
+
match = header.split(/;\s*/).find { |pair| pair.start_with?(prefix) }
|
|
185
|
+
return nil unless match
|
|
186
|
+
|
|
187
|
+
value = CGI.unescape(match.split("=", 2).last.to_s)
|
|
188
|
+
value.empty? ? nil : value
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def engine_mount
|
|
192
|
+
RailsWayback::Engine.routes.default_url_options[:script_name] ||
|
|
193
|
+
RailsWayback::Engine.routes.find_script_name({}) ||
|
|
194
|
+
"/rails-wayback"
|
|
195
|
+
rescue StandardError
|
|
196
|
+
"/rails-wayback"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|