fix(commits): compute total count on cache miss for pagination

- git_commit_log now computes count when Redis cache misses
- Previous: returned total: 0 when cache empty
- Now: compute + cache on miss (5min TTL)
This commit is contained in:
ZhenYi 2026-04-28 09:42:47 +08:00
parent ddd24bfb6d
commit 21d0d1eae6
2 changed files with 57 additions and 8 deletions

View File

@ -818,6 +818,8 @@ impl AppService {
"git:commit:count:{}:{}:{:?}:{:?}",
namespace, repo_name, from, to,
);
// Get total count for pagination. Cache miss → compute + cache.
let total: usize = if let Ok(mut conn) = self.cache.conn().await {
if let Ok(cached) = conn.get::<_, String>(total_cache_key.clone()).await {
if let Ok(cached) = serde_json::from_str::<CommitCountResponse>(&cached) {
@ -826,10 +828,28 @@ impl AppService {
0
}
} else {
0
// Cache miss: compute count and cache it.
let computed = git_spawn!(repo, domain -> {
domain.commit_count(from.as_deref(), to.as_deref())
}).unwrap_or(0);
if let Err(e) = conn
.set_ex::<String, String, ()>(
total_cache_key.clone(),
serde_json::to_string(&CommitCountResponse { count: computed })
.unwrap_or_default(),
300,
)
.await
{
tracing::debug!(error = ?e, "cache set failed (non-fatal)");
}
computed
}
} else {
0
// No cache: compute directly.
git_spawn!(repo, domain -> {
domain.commit_count(from.as_deref(), to.as_deref())
}).unwrap_or(0)
};
let total_pages = if total == 0 {

View File

@ -52,6 +52,8 @@ export const RepoCommits = () => {
const [copiedSha, setCopiedSha] = useState<string | null>(null);
const [showBranchDropdown, setShowBranchDropdown] = useState(false);
const [activeTab, setActiveTab] = useState<"list" | "graph" | "reflog">("list");
const PAGE_SIZE = 50;
const [page, setPage] = useState(1);
const { data: branches = [] } = useQuery({
queryKey: ["repo-branches", namespace, repoName],
@ -156,6 +158,7 @@ export const RepoCommits = () => {
// Reset to page 1 when search changes
const handleSearchChange = (value: string) => {
setSearchQuery(value);
setPage(1);
};
const handleCopySha = async (sha: string) => {
@ -304,11 +307,11 @@ export const RepoCommits = () => {
</div>
) : (
<div className="divide-y">
{filteredCommits.slice(0, 100).map((commit, index) => {
{filteredCommits.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE).map((commit, index) => {
const { title, description } = getCommitMessageLines(commit.message);
const isCopied = copiedSha === commit.oid;
const isMergeCommit = commit.parent_ids?.length > 1;
const isLast = index === Math.min(filteredCommits.length, 100) - 1;
const isLast = index === Math.min(filteredCommits.length - (page - 1) * PAGE_SIZE, PAGE_SIZE) - 1;
return (
<div
@ -442,10 +445,36 @@ export const RepoCommits = () => {
</div>
)}
{filteredCommits.length > 100 && activeTab === "list" && (
<p className="text-sm text-muted-foreground text-center py-2">
Showing 100 of {filteredCommits.length} commits
</p>
{activeTab === "list" && filteredCommits.length > 0 && (
<div className="flex items-center justify-center gap-2 py-4">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-3 py-1.5 text-sm rounded-md border bg-background hover:bg-muted transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: Math.ceil(filteredCommits.length / PAGE_SIZE) }, (_, i) => (
<button
key={i}
onClick={() => setPage(i + 1)}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
page === i + 1
? "bg-primary text-primary-foreground"
: "border bg-background hover:bg-muted"
}`}
>
{i + 1}
</button>
))}
<button
onClick={() => setPage(p => Math.min(Math.ceil(filteredCommits.length / PAGE_SIZE), p + 1))}
disabled={page >= Math.ceil(filteredCommits.length / PAGE_SIZE)}
className="px-3 py-1.5 text-sm rounded-md border bg-background hover:bg-muted transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
Next
</button>
</div>
)}
</div>
</>