mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-09 18:33:02 +02:00
Ensure frontend reports git-aware version strings (#321)
* Ensure frontend reports git-aware version strings * Keep footer fixed across viewport widths
This commit is contained in:
@@ -19,9 +19,8 @@ module PotatoMesh
|
||||
#
|
||||
# @return [String] semantic version compatible identifier.
|
||||
def determine_app_version
|
||||
repo_root = File.expand_path("../../..", __dir__)
|
||||
git_dir = File.join(repo_root, ".git")
|
||||
return PotatoMesh::Config.version_fallback unless File.directory?(git_dir)
|
||||
repo_root = locate_git_repo_root(File.expand_path("../../..", __dir__))
|
||||
return PotatoMesh::Config.version_fallback unless repo_root
|
||||
|
||||
stdout, status = Open3.capture2("git", "-C", repo_root, "describe", "--tags", "--long", "--abbrev=7")
|
||||
return PotatoMesh::Config.version_fallback unless status.success?
|
||||
@@ -42,6 +41,29 @@ module PotatoMesh
|
||||
PotatoMesh::Config.version_fallback
|
||||
end
|
||||
|
||||
# Discover the root directory of the git repository containing the
|
||||
# application by traversing parent directories until a ``.git`` entry is
|
||||
# located. This supports both traditional repositories where ``.git`` is a
|
||||
# directory and worktree checkouts where it is a plain file.
|
||||
#
|
||||
# @param start_dir [String] absolute path where the search should begin.
|
||||
# @return [String, nil] absolute path to the repository root when found,
|
||||
# otherwise ``nil``.
|
||||
def locate_git_repo_root(start_dir)
|
||||
current = File.expand_path(start_dir)
|
||||
loop do
|
||||
git_entry = File.join(current, ".git")
|
||||
return current if File.exist?(git_entry)
|
||||
|
||||
parent = File.dirname(current)
|
||||
break if parent == current
|
||||
|
||||
current = parent
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# Load the persisted instance private key or generate a new one when absent.
|
||||
#
|
||||
# @return [Array<OpenSSL::PKey::RSA, Boolean>] tuple of key and generation flag.
|
||||
@@ -111,7 +133,7 @@ module PotatoMesh
|
||||
end
|
||||
end
|
||||
|
||||
private :migrate_legacy_keyfile_for_identity!
|
||||
private :migrate_legacy_keyfile_for_identity!, :locate_git_repo_root
|
||||
|
||||
# Return the directory used to store well-known documents.
|
||||
#
|
||||
|
||||
@@ -126,7 +126,7 @@ tbody tr:nth-child(even) td {
|
||||
body {
|
||||
font-family: system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif;
|
||||
margin: var(--pad);
|
||||
padding-bottom: 32px;
|
||||
padding-bottom: 96px;
|
||||
--map-tiles-filter: var(--map-tile-filter-light);
|
||||
}
|
||||
|
||||
@@ -740,16 +740,66 @@ input[type="radio"] {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
footer {
|
||||
.app-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: var(--pad);
|
||||
width: calc(100% - 2 * var(--pad));
|
||||
inset-block-end: 0;
|
||||
inset-inline: 0;
|
||||
width: 100%;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
z-index: 4100;
|
||||
}
|
||||
|
||||
.app-footer .footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
padding: 6px var(--pad);
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-footer .footer-separator {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.app-footer .footer-links {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-footer .footer-brand {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-footer a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.app-footer .footer-content {
|
||||
padding: 10px 12px;
|
||||
justify-content: center;
|
||||
gap: 4px 8px;
|
||||
}
|
||||
|
||||
.app-footer .footer-links {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.app-footer .footer-separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-overlay {
|
||||
|
||||
+68
-20
@@ -408,65 +408,106 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#determine_app_version" do
|
||||
let(:repo_root) { File.expand_path("..", __dir__) }
|
||||
let(:git_dir) { File.join(repo_root, ".git") }
|
||||
describe ".locate_git_repo_root" do
|
||||
it "returns nil when a git directory cannot be found" do
|
||||
nested_dir = Dir.mktmpdir("potato-mesh-no-git-")
|
||||
begin
|
||||
deep_dir = File.join(nested_dir, "a", "b", "c")
|
||||
FileUtils.mkdir_p(deep_dir)
|
||||
|
||||
before do
|
||||
allow(File).to receive(:directory?).and_call_original
|
||||
result = application_class.send(:locate_git_repo_root, deep_dir)
|
||||
expect(result).to be_nil
|
||||
ensure
|
||||
FileUtils.remove_entry(nested_dir)
|
||||
end
|
||||
end
|
||||
|
||||
it "returns the fallback when the git directory is missing" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(false)
|
||||
it "locates a git directory" do
|
||||
nested_dir = Dir.mktmpdir("potato-mesh-with-git-")
|
||||
begin
|
||||
repo_root = File.join(nested_dir, "repo")
|
||||
FileUtils.mkdir_p(File.join(repo_root, ".git"))
|
||||
deep_dir = File.join(repo_root, "lib", "potato")
|
||||
FileUtils.mkdir_p(deep_dir)
|
||||
|
||||
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
result = application_class.send(:locate_git_repo_root, deep_dir)
|
||||
expect(result).to eq(repo_root)
|
||||
ensure
|
||||
FileUtils.remove_entry(nested_dir)
|
||||
end
|
||||
end
|
||||
|
||||
it "recognises git worktree files" do
|
||||
nested_dir = Dir.mktmpdir("potato-mesh-worktree-")
|
||||
begin
|
||||
repo_root = File.join(nested_dir, "worktree")
|
||||
FileUtils.mkdir_p(repo_root)
|
||||
File.write(File.join(repo_root, ".git"), "gitdir: /tmp/worktree")
|
||||
deep_dir = File.join(repo_root, "app", "lib")
|
||||
FileUtils.mkdir_p(deep_dir)
|
||||
|
||||
result = application_class.send(:locate_git_repo_root, deep_dir)
|
||||
expect(result).to eq(repo_root)
|
||||
ensure
|
||||
FileUtils.remove_entry(nested_dir)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#determine_app_version" do
|
||||
let(:repo_root) { File.expand_path("..", __dir__) }
|
||||
|
||||
it "returns the fallback when the git directory is missing" do
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(nil)
|
||||
|
||||
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
end
|
||||
|
||||
it "returns the fallback when git describe fails" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(true)
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
|
||||
status = instance_double(Process::Status, success?: false)
|
||||
allow(Open3).to receive(:capture2).and_return(["ignored", status])
|
||||
|
||||
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
end
|
||||
|
||||
it "returns the fallback when git describe output is empty" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(true)
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
|
||||
status = instance_double(Process::Status, success?: true)
|
||||
allow(Open3).to receive(:capture2).and_return(["\n", status])
|
||||
|
||||
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
end
|
||||
|
||||
it "returns the original describe output when the format is unexpected" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(true)
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
|
||||
status = instance_double(Process::Status, success?: true)
|
||||
allow(Open3).to receive(:capture2).and_return(["weird-output", status])
|
||||
|
||||
expect(determine_app_version).to eq("weird-output")
|
||||
expect(application_class.determine_app_version).to eq("weird-output")
|
||||
end
|
||||
|
||||
it "normalises the version when no commits are ahead of the tag" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(true)
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
|
||||
status = instance_double(Process::Status, success?: true)
|
||||
allow(Open3).to receive(:capture2).and_return(["v1.2.3-0-gabcdef1", status])
|
||||
|
||||
expect(determine_app_version).to eq("v1.2.3")
|
||||
expect(application_class.determine_app_version).to eq("v1.2.3")
|
||||
end
|
||||
|
||||
it "includes commit metadata when ahead of the tag" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(true)
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
|
||||
status = instance_double(Process::Status, success?: true)
|
||||
allow(Open3).to receive(:capture2).and_return(["v1.2.3-5-gabcdef1", status])
|
||||
|
||||
expect(determine_app_version).to eq("v1.2.3+5-abcdef1")
|
||||
expect(application_class.determine_app_version).to eq("v1.2.3+5-abcdef1")
|
||||
end
|
||||
|
||||
it "returns the fallback when git describe raises an error" do
|
||||
allow(File).to receive(:directory?).with(git_dir).and_return(true)
|
||||
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
|
||||
allow(Open3).to receive(:capture2).and_raise(StandardError, "boom")
|
||||
|
||||
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -764,6 +805,13 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
expect(last_response.body).to include("#{APP_VERSION}")
|
||||
end
|
||||
|
||||
it "renders the responsive footer container" do
|
||||
get "/"
|
||||
|
||||
expect(last_response.body).to include('<footer class="app-footer">')
|
||||
expect(last_response.body).to include('class="footer-content"')
|
||||
end
|
||||
|
||||
it "includes SEO metadata from configuration" do
|
||||
allow(PotatoMesh::Config).to receive(:site_name).and_return("Spec Mesh Title")
|
||||
allow(PotatoMesh::Config).to receive(:channel).and_return("#SpecChannel")
|
||||
|
||||
+22
-13
@@ -196,20 +196,29 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<footer>
|
||||
PotatoMesh
|
||||
<% if version && !version.empty? %>
|
||||
<span class="mono"><%= version %></span> —
|
||||
<% end %>
|
||||
GitHub: <a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
|
||||
<% if contact_link && !contact_link.empty? %>
|
||||
— <%= site_name %> chat:
|
||||
<% if contact_link_url %>
|
||||
<a href="<%= contact_link_url %>" target="_blank"><%= contact_link %></a>
|
||||
<% else %>
|
||||
<%= contact_link %>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<span class="footer-brand">PotatoMesh</span>
|
||||
<% if version && !version.empty? %>
|
||||
<span class="mono"><%= version %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="footer-separator" aria-hidden="true">—</span>
|
||||
<span class="footer-links">
|
||||
GitHub:
|
||||
<a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
|
||||
<% if contact_link && !contact_link.empty? %>
|
||||
<span class="footer-separator" aria-hidden="true">—</span>
|
||||
<span class="footer-contact">
|
||||
<%= site_name %> chat:
|
||||
<% if contact_link_url %>
|
||||
<a href="<%= contact_link_url %>" target="_blank"><%= contact_link %></a>
|
||||
<% else %>
|
||||
<%= contact_link %>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user