From ffad84f18acb55ea22b1ea7ff3dca8254a7accdc Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:26:57 +0200 Subject: [PATCH] Ensure frontend reports git-aware version strings (#321) * Ensure frontend reports git-aware version strings * Keep footer fixed across viewport widths --- web/lib/potato_mesh/application/identity.rb | 30 ++++++- web/public/assets/styles/base.css | 64 +++++++++++++-- web/spec/app_spec.rb | 88 ++++++++++++++++----- web/views/index.erb | 35 +++++--- 4 files changed, 173 insertions(+), 44 deletions(-) diff --git a/web/lib/potato_mesh/application/identity.rb b/web/lib/potato_mesh/application/identity.rb index 101157b..cd165c8 100644 --- a/web/lib/potato_mesh/application/identity.rb +++ b/web/lib/potato_mesh/application/identity.rb @@ -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] 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. # diff --git a/web/public/assets/styles/base.css b/web/public/assets/styles/base.css index fef0934..77a99e7 100644 --- a/web/public/assets/styles/base.css +++ b/web/public/assets/styles/base.css @@ -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 { diff --git a/web/spec/app_spec.rb b/web/spec/app_spec.rb index 3f96c6b..bbcaa41 100644 --- a/web/spec/app_spec.rb +++ b/web/spec/app_spec.rb @@ -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('