Improve commit and diff views, show real commit feed on profile

- Redesign commit diff view to group changes by file, show additions/deletions, and improve layout - Show real recent commits on user profile, not hardcoded list - Refactor syntax highlighting to highlight lines for better display - Update registration/login branding to "siGit Code & Deploy" - Update dev:seed_repo to always point to project .git for local dev - Add .smb/config.toml for project metadata - Set format: false on blob/raw routes to avoid .html extension issues

Seto Elkahfi committed Apr 12, 2026 at 14:02 UTC 9e5813f06e6e3d6f2c07976731efb538bbb150dc
11 files changed +191 -73
.smb/config.toml new
+11
@@ -0,0 +1,11 @@
1 +name = "sigitsi"
2 +description = "sigitsi"
3 +
4 +[project]
5 +id = 56
6 +runner = 0
7 +name = "sigitsi"
8 +repository = "sigitsi"
9 +description = "siGit Si"
10 +created_at = "2025-06-28T22:43:02.646Z"
11 +updated_at = "2025-06-28T22:43:02.646Z"
app/controllers/blobs_controller.rb
+23 -5
@@ -21,7 +21,7 @@ class BlobsController < ApplicationController
21 @file_size = content.bytesize
22
23 unless @is_binary
24 - @highlighted = highlight_code(content, @filename)
24 + @highlighted_lines = highlight_lines(content, @filename)
25 @line_count = content.lines.count
26 end
27 end
@@ -66,11 +66,29 @@ class BlobsController < ApplicationController
66 content.bytes.first(8192).include?(0)
67 end
68
69 - def highlight_code(content, filename)
69 + def highlight_lines(content, filename)
70 lexer = Rouge::Lexer.guess(filename: filename, source: content)
71 - formatter = Rouge::Formatters::HTML.new
72 - formatter.format(lexer.lex(content)).html_safe
71 + html_formatter = Rouge::Formatters::HTML.new
72 +
73 + # Tokenize the entire file at once (preserves stateful lexer context across
74 + # lines — e.g. multiline strings, heredocs) then split the token stream on
75 + # newline boundaries to get one HTML fragment per source line.
76 + lines = [[]]
77 + lexer.lex(content).each do |token, value|
78 + value.split(/(\n)/, -1).each do |part|
79 + if part == "\n"
80 + lines << []
81 + else
82 + lines.last << [token, part] unless part.empty?
83 + end
84 + end
85 + end
86 +
87 + # Drop a trailing empty entry when the file ends with a newline.
88 + lines.pop if lines.last&.empty?
89 +
90 + lines.map { |line_tokens| html_formatter.format(line_tokens).html_safe }
91 rescue StandardError
74 - Rouge::Formatters::HTML.new.format(Rouge::Lexers::PlainText.new.lex(content)).html_safe
92 + content.lines.map { |l| ERB::Util.html_escape(l.chomp).html_safe }
93 end
94 end
app/controllers/users_controller.rb
+10
@@ -8,6 +8,16 @@ class UsersController < ApplicationController
8 else
9 @profile_user.repositories.where(is_private: false).order(updated_at: :desc)
10 end
11 +
12 + feed_repo = @repositories.find { |r| r.name == "sigit-si" } || @repositories.first
13 + if feed_repo&.initialized?
14 + @feed_repo = feed_repo
15 + @feed_commits = GitRepositoryService.commits(
16 + feed_repo.disk_path,
17 + feed_repo.default_branch,
18 + limit: 3
19 + )
20 + end
21 rescue ActiveRecord::RecordNotFound
22 render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
23 end
app/services/git_repository_service.rb
+19 -13
@@ -82,26 +82,32 @@ class GitRepositoryService
82 end
83
84 def self.commit(path, sha)
85 - format = "%H%x00%h%x00%s%x00%B%x00%an%x00%ae%x00%ad%x00%T"
85 + # %B contains embedded newlines, so we cannot rely on splitting by "\n" to
86 + # find the end of the format output. Instead we append a sentinel that git
87 + # will never emit naturally and split on that.
88 + sentinel = "SIGIT-EOH"
89 + format = "%H%x00%h%x00%s%x00%B%x00%an%x00%ae%x00%ad%x00%T%x00#{sentinel}"
90 out, _err, status = Open3.capture3(
91 "git", "--git-dir", path, "show",
88 - "--format=#{format}", "--stat", "--no-patch",
92 + "--no-patch", "--format=#{format}",
93 sha
94 )
95 return nil unless status.success?
92 - lines = out.split("\n")
93 - parts = lines.first.to_s.split("\x00")
94 - return nil unless parts.length == 8
96 +
97 + header_raw = out.split(sentinel, 2).first.to_s
98 + parts = header_raw.split("\x00", 9) # 8 fields + possible trailing newline
99 + return nil unless parts.length >= 8
100 +
101 diff_out, _e, _s = Open3.capture3("git", "--git-dir", path, "show", "--format=", sha)
102 {
97 - sha: parts[0],
98 - short_sha: parts[1],
99 - subject: parts[2],
100 - body: parts[3],
101 - author_name: parts[4],
102 - author_email: parts[5],
103 - authored_date: (Time.parse(parts[6]) rescue nil),
104 - diff: diff_out
103 + sha: parts[0].strip,
104 + short_sha: parts[1].strip,
105 + subject: parts[2].strip,
106 + body: parts[3],
107 + author_name: parts[4].strip,
108 + author_email: parts[5].strip,
109 + authored_date: (Time.parse(parts[6].strip) rescue nil),
110 + diff: diff_out
111 }
112 end
113
app/views/blobs/show.html.erb
+5 -5
@@ -44,15 +44,15 @@
44 Binary file — <%= number_to_human_size(@file_size) %>
45 </div>
46 <% else %>
47 - <div class="code-container rounded-none border-0 relative">
47 + <div class="code-container rounded-none border-0 relative overflow-x-auto">
48 <table class="w-full">
49 - <tbody>
50 - <% @raw_content.lines.each_with_index do |line, i| %>
49 + <tbody class="highlight">
50 + <% @highlighted_lines.each_with_index do |highlighted_line, i| %>
51 <tr class="hover:bg-brand-500/10">
52 - <td class="text-right text-gray-500 pr-4 pl-4 py-0 select-none w-10 text-xs border-r border-surface-600 align-top" id="L<%= i+1 %>">
52 + <td class="text-right text-gray-500 pr-4 pl-4 py-0 select-none w-10 text-xs border-r border-surface-600 align-top font-mono" id="L<%= i+1 %>">
53 <a href="#L<%= i+1 %>" class="hover:text-brand-500"><%= i+1 %></a>
54 </td>
55 - <td class="pl-4 pr-4 py-0 font-mono text-xs leading-5 whitespace-pre-wrap break-all text-gray-200"><%= line %></td>
55 + <td class="pl-4 pr-4 py-0 font-mono text-xs leading-5 whitespace-pre text-gray-200"><%= highlighted_line %></td>
56 </tr>
57 <% end %>
58 </tbody>
app/views/commits/show.html.erb
+87 -23
@@ -2,43 +2,107 @@
2
3 <div class="border-b border-surface-600 bg-surface-800">
4 <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-6 pb-4">
5 - <div class="flex items-center gap-2 text-sm mb-3">
5 + <div class="flex items-center gap-2 text-sm mb-4">
6 <%= link_to "@#{@owner.username}", user_profile_path(@owner.username), class: "text-brand-500 hover:underline font-medium" %>
7 <span class="text-gray-500">/</span>
8 <%= link_to @repository.name, repository_path(@owner.username, @repository.name), class: "text-brand-500 hover:underline font-semibold" %>
9 + <span class="text-gray-500">/</span>
10 + <%= link_to "commits", repository_commits_path(@owner.username, @repository.name, @repository.default_branch), class: "text-gray-400 hover:text-gray-200 transition-colors" %>
11 + <span class="text-gray-500">/</span>
12 + <span class="font-mono text-gray-400"><%= @commit[:short_sha] %></span>
13 </div>
14 +
15 <h1 class="text-xl font-semibold text-gray-100 mb-1"><%= @commit[:subject] %></h1>
11 - <% if @commit[:body].present? && @commit[:body] != @commit[:subject] %>
12 - <p class="text-sm text-gray-400 whitespace-pre-line mb-3"><%= @commit[:body].delete_prefix(@commit[:subject]).strip %></p>
16 +
17 + <% body = @commit[:body].to_s.delete_prefix(@commit[:subject]).strip %>
18 + <% if body.present? %>
19 + <p class="text-sm text-gray-400 whitespace-pre-line mt-2 mb-3"><%= body %></p>
20 <% end %>
14 - <div class="flex items-center gap-4 text-xs text-gray-400">
21 +
22 + <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-400 mt-3">
23 <span><strong class="text-gray-300"><%= @commit[:author_name] %></strong> committed</span>
24 <% if @commit[:authored_date] %>
25 <span><%= @commit[:authored_date].strftime("%b %-d, %Y at %H:%M UTC") %></span>
26 <% end %>
19 - <span class="font-mono badge-gray"><%= @commit[:sha] %></span>
27 + <span class="font-mono badge-gray select-all"><%= @commit[:sha] %></span>
28 </div>
29 </div>
30 </div>
31
24 -<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6">
25 - <% if @commit[:diff].present? %>
26 - <div class="card">
27 - <div class="px-4 py-2.5 border-b border-surface-600 bg-surface-600 text-xs font-medium text-gray-400">Diff</div>
28 - <div class="code-container rounded-none border-0 font-mono text-xs leading-5 overflow-x-auto">
29 - <% @commit[:diff].lines.each do |line| %>
30 - <% css = if line.start_with?("+") && !line.start_with?("+++")
31 - "diff-add block px-4"
32 - elsif line.start_with?("-") && !line.start_with?("---")
33 - "diff-remove block px-4"
34 - elsif line.start_with?("@@")
35 - "diff-header block px-4"
36 - else
37 - "block px-4 text-gray-400"
38 - end %>
39 - <span class="<%= css %>"><%= line %></span>
40 - <% end %>
41 - </div>
32 +<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6 space-y-3">
33 + <%
34 + file_diffs = []
35 + current = nil
36 +
37 + @commit[:diff].to_s.each_line do |line|
38 + if line.start_with?("diff --git ")
39 + file_diffs << current if current
40 + current = { header: line, lines: [] }
41 + elsif current
42 + current[:lines] << line
43 + end
44 + end
45 + file_diffs << current if current
46 + %>
47 +
48 + <% if file_diffs.empty? %>
49 + <p class="text-sm text-gray-400">No diff available for this commit.</p>
50 + <% else %>
51 + <%
52 + total_additions = 0
53 + total_deletions = 0
54 + file_diffs.each do |fd|
55 + total_additions += fd[:lines].count { |l| l.start_with?("+") && !l.start_with?("+++") }
56 + total_deletions += fd[:lines].count { |l| l.start_with?("-") && !l.start_with?("---") }
57 + end
58 + %>
59 +
60 + <div class="text-xs text-gray-400 mb-4">
61 + <span class="text-gray-300 font-medium"><%= file_diffs.size %> <%= "file".pluralize(file_diffs.size) %> changed</span>
62 + <% if total_additions > 0 %>
63 + <span class="text-green-400 ml-2">+<%= total_additions %></span>
64 + <% end %>
65 + <% if total_deletions > 0 %>
66 + <span class="text-red-400 ml-1">-<%= total_deletions %></span>
67 + <% end %>
68 </div>
69 +
70 + <% file_diffs.each do |fd| %>
71 + <%
72 + m = fd[:header].match(/diff --git a\/(.*) b\/(.*)/)
73 + filename = m ? m[2].chomp : fd[:header].chomp
74 + additions = fd[:lines].count { |l| l.start_with?("+") && !l.start_with?("+++") }
75 + deletions = fd[:lines].count { |l| l.start_with?("-") && !l.start_with?("---") }
76 + %>
77 +
78 + <div class="card overflow-hidden">
79 + <div class="flex items-center justify-between px-4 py-2.5 border-b border-surface-600 bg-surface-700">
80 + <span class="font-mono text-xs text-gray-200 truncate"><%= filename %></span>
81 + <div class="flex items-center gap-2 shrink-0 ml-4 font-mono text-xs">
82 + <% if additions > 0 %>
83 + <span class="text-green-400">+<%= additions %></span>
84 + <% end %>
85 + <% if deletions > 0 %>
86 + <span class="text-red-400">-<%= deletions %></span>
87 + <% end %>
88 + </div>
89 + </div>
90 +
91 + <div class="font-mono text-xs leading-5 overflow-x-auto">
92 + <% fd[:lines].each do |line| %>
93 + <% css = if line.start_with?("+") && !line.start_with?("+++")
94 + "diff-add block px-4"
95 + elsif line.start_with?("-") && !line.start_with?("---")
96 + "diff-remove block px-4"
97 + elsif line.start_with?("@@")
98 + "diff-header block px-4"
99 + else
100 + "block px-4 text-gray-500"
101 + end %>
102 + <span class="<%= css %>"><%= line.chomp %></span>
103 + <% end %>
104 + </div>
105 + </div>
106 + <% end %>
107 <% end %>
108 </div>
app/views/registrations/new.html.erb
+1 -1
@@ -5,7 +5,7 @@
5 <div class="mb-10 flex flex-col items-center gap-3">
6 <%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
7 <img src="/icon.png" alt="siGit" class="h-9 w-auto">
8 - <span class="text-xl font-semibold tracking-tight">siGit</span>
8 + <span class="text-xl font-semibold tracking-tight">siGit Code &amp; Deploy</span>
9 <% end %>
10 <p class="text-sm text-gray-500">Create your account</p>
11 </div>
app/views/sessions/new.html.erb
+1 -1
@@ -5,7 +5,7 @@
5 <div class="mb-10 flex flex-col items-center gap-3">
6 <%= link_to root_path, class: "flex items-center gap-3 text-gray-100 hover:text-brand-500 transition-colors" do %>
7 <img src="/icon.png" alt="siGit" class="h-9 w-auto">
8 - <span class="text-xl font-semibold tracking-tight">siGit</span>
8 + <span class="text-xl font-semibold tracking-tight">siGit Code &amp; Deploy</span>
9 <% end %>
10 <p class="text-sm text-gray-500">Sign in to continue</p>
11 </div>
app/views/users/show.html.erb
+15 -17
@@ -65,26 +65,24 @@
65 <p class="text-xs text-gray-500 mb-2">2 hours ago</p>
66 <p class="text-sm mb-3">
67 <span class="text-gray-400">Pushed </span>
68 - <span class="font-semibold text-gray-200">3 commits</span>
68 + <span class="font-semibold text-gray-200"><%= @feed_commits&.size || 0 %> commits</span>
69 <span class="text-gray-400"> to </span>
70 - <%= link_to "@#{@profile_user.username}/sigit-si", repository_path(@profile_user.username, "sigit-si"), class: "text-brand-500 hover:underline font-medium" %>
70 + <% if @feed_repo %>
71 + <%= link_to "@#{@profile_user.username}/#{@feed_repo.name}", repository_path(@profile_user.username, @feed_repo.name), class: "text-brand-500 hover:underline font-medium" %>
72 + <% end %>
73 <span class="text-gray-400"> on </span>
72 - <code class="bg-surface-600 px-1.5 py-0.5 rounded text-xs text-gray-200">main</code>
74 + <code class="bg-surface-600 px-1.5 py-0.5 rounded text-xs text-gray-200"><%= @feed_repo&.default_branch || "main" %></code>
75 </p>
74 - <ul class="space-y-1.5 border-t border-surface-600 pt-3">
75 - <li class="flex items-start gap-2 text-xs text-gray-400">
76 - <%= link_to "a3f9d12", repository_commit_path(@profile_user.username, "sigit-si", "a3f9d12"), class: "text-brand-400 shrink-0 font-mono hover:underline" %>
77 - <span>Fix avatar fallback when gravatar URL is empty</span>
78 - </li>
79 - <li class="flex items-start gap-2 text-xs text-gray-400">
80 - <%= link_to "b1e2c34", repository_commit_path(@profile_user.username, "sigit-si", "b1e2c34"), class: "text-brand-400 shrink-0 font-mono hover:underline" %>
81 - <span>Add dev panel to footer, remove Rails 8 credit</span>
82 - </li>
83 - <li class="flex items-start gap-2 text-xs text-gray-400">
84 - <%= link_to "8d04f71", repository_commit_path(@profile_user.username, "sigit-si", "8d04f71"), class: "text-brand-400 shrink-0 font-mono hover:underline" %>
85 - <span>Scope private repos to authenticated owner only</span>
86 - </li>
87 - </ul>
76 + <% if @feed_commits.present? %>
77 + <ul class="space-y-1.5 border-t border-surface-600 pt-3">
78 + <% @feed_commits.each do |commit| %>
79 + <li class="flex items-start gap-2 text-xs text-gray-400">
80 + <%= link_to commit[:short_sha], repository_commit_path(@profile_user.username, @feed_repo.name, commit[:sha]), class: "text-brand-400 shrink-0 font-mono hover:underline" %>
81 + <span><%= commit[:subject] %></span>
82 + </li>
83 + <% end %>
84 + </ul>
85 + <% end %>
86 </div>
87 </li>
88
config/routes.rb
+2 -2
@@ -34,8 +34,8 @@ Rails.application.routes.draw do
34 get "/:username/:repository/commits/:branch", to: "commits#index", as: :repository_commits,
35 constraints: { branch: /[^\/]+/, repository: /[^\/.][^\/]*/ }
36 get "/:username/:repository/commit/:sha", to: "commits#show", as: :repository_commit
37 - get "/:username/:repository/blob/:branch/*path", to: "blobs#show", as: :repository_blob
38 - get "/:username/:repository/raw/:branch/*path", to: "blobs#raw", as: :repository_blob_raw
37 + get "/:username/:repository/blob/:branch/*path", to: "blobs#show", as: :repository_blob, format: false
38 + get "/:username/:repository/raw/:branch/*path", to: "blobs#raw", as: :repository_blob_raw, format: false
39 get "/:username/:repository/tree/:branch/*path", to: "repositories#tree", as: :repository_tree
40 get "/:username/:repository/tree/:branch", to: "repositories#tree", as: :repository_branch
41 get "/:username/:repository", to: "repositories#show", as: :repository
lib/tasks/dev.rake
+17 -6
@@ -19,15 +19,26 @@ namespace :dev do
19 user = User.find_by(username: username)
20 abort " [dev:seed_repo] No user '#{username}' found. Sign in once first." unless user
21
22 - # ── Skip if already exists ────────────────────────────────────────────────
23 - if user.repositories.exists?(name: repo_name)
24 - puts " [dev:seed_repo] '#{username}/#{repo_name}' already exists — skipping."
22 + # ── Point disk_path at the project's own .git directory ──────────────────
23 + # This gives the web UI access to every real commit SHA without needing a
24 + # separate bare repo or a push step.
25 + git_path = Rails.root.join(".git").to_s
26 +
27 + repo = user.repositories.find_or_initialize_by(name: repo_name)
28 + if repo.persisted? && repo.disk_path == git_path
29 + puts " [dev:seed_repo] '#{username}/#{repo_name}' already up to date — skipping."
30 next
31 end
32
28 - local_repos = Rails.root.join(".local-repos").to_s
29 - bare_path = File.join(local_repos, username, "#{repo_name}.git")
30 - puts " [dev:seed_repo] Creating bare repo at #{bare_path} …"
33 + repo.assign_attributes(
34 + description: description,
35 + disk_path: git_path,
36 + default_branch: branch,
37 + is_private: false
38 + )
39 + repo.save!
40 + puts " [dev:seed_repo] #{repo.previously_new_record? ? "Created" : "Updated"} '#{username}/#{repo_name}' → #{git_path}"
41 + next
42
43 # ── 1. Create bare repo ───────────────────────────────────────────────────
44 FileUtils.mkdir_p(bare_path)