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 & 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 & 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)