Detailed changes
@@ -449,6 +449,79 @@ func NewGitCommitHandler(mrc *cache.MultiRepoCache) http.Handler {
return &gitCommitHandler{mrc: mrc}
}
+// ── GET /api/repos/{owner}/{repo}/git/commits/{sha}/diff?path= ───────────────
+
+type gitCommitDiffHandler struct{ mrc *cache.MultiRepoCache }
+
+func NewGitCommitDiffHandler(mrc *cache.MultiRepoCache) http.Handler {
+ return &gitCommitDiffHandler{mrc: mrc}
+}
+
+func (h *gitCommitDiffHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ sha := mux.Vars(r)["sha"]
+ filePath := r.URL.Query().Get("path")
+ if filePath == "" {
+ http.Error(w, "missing path", http.StatusBadRequest)
+ return
+ }
+
+ _, br, err := browseRepo(h.mrc, r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ fd, err := br.CommitFileDiff(repository.Hash(sha), filePath)
+ if err == repository.ErrNotFound {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ type diffLineResp struct {
+ Type string `json:"type"`
+ Content string `json:"content"`
+ OldLine int `json:"oldLine,omitempty"`
+ NewLine int `json:"newLine,omitempty"`
+ }
+ type diffHunkResp struct {
+ OldStart int `json:"oldStart"`
+ OldLines int `json:"oldLines"`
+ NewStart int `json:"newStart"`
+ NewLines int `json:"newLines"`
+ Lines []diffLineResp `json:"lines"`
+ }
+ type fileDiffResp struct {
+ Path string `json:"path"`
+ OldPath string `json:"oldPath,omitempty"`
+ IsBinary bool `json:"isBinary"`
+ IsNew bool `json:"isNew"`
+ IsDelete bool `json:"isDelete"`
+ Hunks []diffHunkResp `json:"hunks"`
+ }
+
+ hunks := make([]diffHunkResp, len(fd.Hunks))
+ for i, h := range fd.Hunks {
+ lines := make([]diffLineResp, len(h.Lines))
+ for j, l := range h.Lines {
+ lines[j] = diffLineResp{Type: l.Type, Content: l.Content, OldLine: l.OldLine, NewLine: l.NewLine}
+ }
+ hunks[i] = diffHunkResp{OldStart: h.OldStart, OldLines: h.OldLines, NewStart: h.NewStart, NewLines: h.NewLines, Lines: lines}
+ }
+
+ writeJSON(w, fileDiffResp{
+ Path: fd.Path,
+ OldPath: fd.OldPath,
+ IsBinary: fd.IsBinary,
+ IsNew: fd.IsNew,
+ IsDelete: fd.IsDelete,
+ Hunks: hunks,
+ })
+}
+
type changedFileResponse struct {
Path string `json:"path"`
OldPath string `json:"oldPath,omitempty"`
@@ -193,6 +193,7 @@ func runWebUI(env *execenv.Env, opts webUIOptions) error {
apiRepos.Path("/git/raw/{ref}/{path:.*}").Methods("GET").Handler(httpapi.NewGitRawHandler(mrc))
apiRepos.Path("/git/commits").Methods("GET").Handler(httpapi.NewGitCommitsHandler(mrc))
apiRepos.Path("/git/commits/{sha}").Methods("GET").Handler(httpapi.NewGitCommitHandler(mrc))
+ apiRepos.Path("/git/commits/{sha}/diff").Methods("GET").Handler(httpapi.NewGitCommitDiffHandler(mrc))
apiRepos.Path("/file/{hash}").Methods("GET").Handler(httpapi.NewGitFileHandler(mrc))
apiRepos.Path("/upload").Methods("POST").Handler(httpapi.NewGitUploadFileHandler(mrc))
@@ -19,6 +19,7 @@ import (
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
+ "github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/execabs"
@@ -1109,6 +1110,177 @@ func (repo *GoGitRepo) CommitDetail(hash Hash) (CommitDetail, error) {
return detail, nil
}
+// CommitFileDiff returns the structured diff for a single file in a commit.
+func (repo *GoGitRepo) CommitFileDiff(hash Hash, filePath string) (FileDiff, error) {
+ repo.rMutex.Lock()
+ defer repo.rMutex.Unlock()
+
+ commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
+ if err == plumbing.ErrObjectNotFound {
+ return FileDiff{}, ErrNotFound
+ }
+ if err != nil {
+ return FileDiff{}, err
+ }
+
+ tree, err := commit.Tree()
+ if err != nil {
+ return FileDiff{}, err
+ }
+
+ var parentTree *object.Tree
+ if len(commit.ParentHashes) > 0 {
+ if parent, err := commit.Parent(0); err == nil {
+ parentTree, _ = parent.Tree()
+ }
+ }
+ if parentTree == nil {
+ parentTree = &object.Tree{}
+ }
+
+ changes, err := object.DiffTree(parentTree, tree)
+ if err != nil {
+ return FileDiff{}, err
+ }
+
+ for _, change := range changes {
+ from, to := change.From.Name, change.To.Name
+ if to != filePath && from != filePath {
+ continue
+ }
+
+ patch, err := change.Patch()
+ if err != nil {
+ return FileDiff{}, err
+ }
+
+ fps := patch.FilePatches()
+ if len(fps) == 0 {
+ return FileDiff{}, ErrNotFound
+ }
+ fp := fps[0]
+
+ fromFile, toFile := fp.Files()
+ fd := FileDiff{
+ IsBinary: fp.IsBinary(),
+ IsNew: fromFile == nil,
+ IsDelete: toFile == nil,
+ }
+ if toFile != nil {
+ fd.Path = toFile.Path()
+ } else if fromFile != nil {
+ fd.Path = fromFile.Path()
+ }
+ if fromFile != nil && toFile != nil && fromFile.Path() != toFile.Path() {
+ fd.OldPath = fromFile.Path()
+ }
+
+ if !fd.IsBinary {
+ fd.Hunks = buildDiffHunks(fp.Chunks())
+ }
+ return fd, nil
+ }
+
+ return FileDiff{}, ErrNotFound
+}
+
+// buildDiffHunks converts go-git diff chunks into DiffHunks with context lines.
+func buildDiffHunks(chunks []diff.Chunk) []DiffHunk {
+ const ctx = 3
+
+ type line struct {
+ op diff.Operation
+ content string
+ oldLine int
+ newLine int
+ }
+
+ // Expand chunks into individual lines.
+ var lines []line
+ oldN, newN := 1, 1
+ for _, chunk := range chunks {
+ parts := strings.Split(chunk.Content(), "\n")
+ // Split always produces a trailing empty element if content ends with \n.
+ if len(parts) > 0 && parts[len(parts)-1] == "" {
+ parts = parts[:len(parts)-1]
+ }
+ for _, p := range parts {
+ l := line{op: chunk.Type(), content: p}
+ switch chunk.Type() {
+ case diff.Equal:
+ l.oldLine, l.newLine = oldN, newN
+ oldN++
+ newN++
+ case diff.Add:
+ l.newLine = newN
+ newN++
+ case diff.Delete:
+ l.oldLine = oldN
+ oldN++
+ }
+ lines = append(lines, l)
+ }
+ }
+
+ // Collect indices of changed lines.
+ var changed []int
+ for i, l := range lines {
+ if l.op != diff.Equal {
+ changed = append(changed, i)
+ }
+ }
+ if len(changed) == 0 {
+ return nil
+ }
+
+ // Merge overlapping/adjacent change windows into hunk ranges.
+ type hunkRange struct{ start, end int }
+ var ranges []hunkRange
+ i := 0
+ for i < len(changed) {
+ start := max(0, changed[i]-ctx)
+ end := changed[i]
+ j := i
+ for j < len(changed) && changed[j] <= end+ctx {
+ end = changed[j]
+ j++
+ }
+ end = min(len(lines)-1, end+ctx)
+ ranges = append(ranges, hunkRange{start, end})
+ i = j
+ }
+
+ // Build DiffHunks from ranges.
+ hunks := make([]DiffHunk, 0, len(ranges))
+ for _, r := range ranges {
+ hunk := DiffHunk{}
+ for _, l := range lines[r.start : r.end+1] {
+ if hunk.OldStart == 0 && l.oldLine > 0 {
+ hunk.OldStart = l.oldLine
+ }
+ if hunk.NewStart == 0 && l.newLine > 0 {
+ hunk.NewStart = l.newLine
+ }
+ dl := DiffLine{Content: l.content, OldLine: l.oldLine, NewLine: l.newLine}
+ switch l.op {
+ case diff.Equal:
+ dl.Type = "context"
+ hunk.OldLines++
+ hunk.NewLines++
+ case diff.Add:
+ dl.Type = "added"
+ hunk.NewLines++
+ case diff.Delete:
+ dl.Type = "deleted"
+ hunk.OldLines++
+ }
+ hunk.Lines = append(hunk.Lines, dl)
+ }
+ hunks = append(hunks, hunk)
+ }
+ return hunks
+}
+
func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
repo.clocksMutex.Lock()
defer repo.clocksMutex.Unlock()
@@ -235,6 +235,38 @@ type RepoBrowse interface {
// CommitDetail returns the full metadata for a single commit plus the list
// of files it changed relative to its first parent.
CommitDetail(hash Hash) (CommitDetail, error)
+
+ // CommitFileDiff returns the structured diff for a single file in a commit
+ // relative to its first parent. path is the current file path (or old path
+ // for deletions).
+ CommitFileDiff(hash Hash, path string) (FileDiff, error)
+}
+
+// DiffLine is a single line in a diff hunk.
+type DiffLine struct {
+ Type string // "context" | "added" | "deleted"
+ Content string
+ OldLine int // 0 for added lines
+ NewLine int // 0 for deleted lines
+}
+
+// DiffHunk is a contiguous block of changes with surrounding context.
+type DiffHunk struct {
+ OldStart int
+ OldLines int
+ NewStart int
+ NewLines int
+ Lines []DiffLine
+}
+
+// FileDiff holds the diff for a single file in a commit.
+type FileDiff struct {
+ Path string
+ OldPath string // non-empty for renames
+ IsBinary bool
+ IsNew bool
+ IsDelete bool
+ Hunks []DiffHunk
}
// ChangedFile describes a single file changed by a commit.
@@ -8,50 +8,94 @@ You need two processes running:
```bash
# 1. Go backend (from repo root)
-go run . webui --port 3000
+go run . webui --no-open --port 3000
# 2. Vite dev server (from this directory)
npm install
npm run dev
```
-Open http://localhost:5173. Vite proxies `/graphql`, `/api`, `/gitfile`, and `/upload` to the Go server on port 3000.
+Open http://localhost:5173. Vite proxies `/graphql`, `/api`, and `/auth` to the Go server on port 3000.
Node 22 is required. If you use asdf, `.tool-versions` pins the right version automatically.
+## Routes
+
+| Path | Page |
+|-------------------------|----------------------------------------------------------|
+| `/` | Repo picker — auto-redirects when there is only one repo |
+| `/_` | Default repo (issues + code browser) |
+| `/_/issues` | Issue list with search and label filtering |
+| `/_/issues/new` | New issue form |
+| `/_/issues/:id` | Issue detail and timeline |
+| `/_/user/:id` | User profile |
+| `/_/commit/:hash` | Commit detail with collapsible file diffs |
+| `/auth/select-identity` | OAuth identity adoption (first-time login) |
+
+`_` is the URL segment for the default (unnamed) repository. Named repositories use their registered name.
+
## Code structure
```
src/
-├── pages/ # One file per route (BugList, BugDetail, NewBug,
-│ # UserProfile, Code, Commit)
+├── pages/ # One file per route
├── components/
-│ ├── bugs/ # Issue components (BugRow, Timeline, CommentBox,
-│ │ # LabelEditor, TitleEditor, LabelBadge, StatusBadge)
-│ ├── code/ # Code browser (FileTree, FileViewer, CommitList,
-│ │ # RefSelector, CodeBreadcrumb)
+│ ├── bugs/ # Issue components (BugRow, Timeline, ...)
+│ ├── code/ # Code browser (FileTree, FileViewer, ...)
│ ├── content/ # Markdown renderer
│ ├── layout/ # Header + Shell
│ └── ui/ # shadcn/ui — never edit manually
│ # Update with: npx shadcn update <component>
├── graphql/ # .graphql source files — edit these, then run codegen
├── __generated__/ # Generated typed hooks — do not edit
-└── lib/ # apollo.ts, auth.tsx, theme.tsx, gitApi.ts, utils.ts
+└── lib/ # apollo.ts, auth.tsx, theme.tsx, gitApi.ts, repo.tsx, utils.ts
```
## Data flow
-**Bug tracking** uses GraphQL (`/graphql`). Queries and mutations are defined in `src/graphql/*.graphql` and codegen produces typed React hooks into `src/__generated__/graphql.ts`. After changing any `.graphql` file, run:
+**Bug tracking** uses GraphQL (`/graphql`). Queries and mutations are defined in `src/graphql/*.graphql` and codegen produces typed React hooks into `src/__generated__/graphql.ts`. After changing any `.graphql` file run:
```bash
npm run codegen
```
-**Code browser** uses REST endpoints at `/api/git/*` implemented in Go (`api/http/git_browse_handler.go`). The TypeScript client is `src/lib/gitApi.ts`.
+**Code browser** uses REST endpoints at `/api/repos/{owner}/{repo}/git/*` implemented in `api/http/git_browse_handler.go`. `_` is used for both owner and repo (local single-user setup). The TypeScript client is `src/lib/gitApi.ts`.
+
+| Endpoint | Description |
+|---------------------------------------|-----------------------------------------|
+| `GET /git/refs` | List branches and tags |
+| `GET /git/trees/{ref}?path=` | Directory listing with last-commit info |
+| `GET /git/blobs/{ref}?path=` | File content |
+| `GET /git/raw/{ref}/{path}` | Raw file download |
+| `GET /git/commits?ref=&limit=&after=` | Paginated commit log |
+| `GET /git/commits/{sha}` | Commit metadata + changed file list |
+| `GET /git/commits/{sha}/diff?path=` | Per-file structured diff (lazy-loaded) |
## Auth
-`AuthContext` (`src/lib/auth.tsx`) fetches `repository.userIdentity` on load. If the query returns null the UI enters read-only mode and all write actions are hidden. The context is designed for a future OAuth provider: swap `AuthProvider` for an `OAuthAuthProvider` without touching any other component.
+Three modes, configured at server start:
+
+- **`local`** — single user derived from git config; all writes enabled, no login UI.
+- **`oauth`** — multi-user via external providers; all API endpoints require a valid session; unauthenticated requests get 401.
+- **`readonly`** — no identity; all write actions hidden in the UI.
+
+`AuthContext` (`src/lib/auth.tsx`) fetches `serverConfig` + `userIdentity` on load and exposes `{ user, mode, oauthProviders }` to the whole tree.
+
+## Build for production
+
+The Go binary embeds the compiled frontend via `//go:embed all:dist` in `webui2/handler.go`. The Makefile `build-webui2` target runs the Vite build before compiling Go:
+
+```bash
+# From repo root
+make build
+```
+
+Or manually:
+
+```bash
+npm run build # outputs to webui2/dist/
+cd .. && go build . # embeds dist/ into the binary
+```
## Theming
@@ -0,0 +1,140 @@
+// Collapsible diff view for a single file in a commit.
+// Diff is fetched lazily on first expand to avoid loading large diffs upfront.
+
+import { useState } from 'react'
+import { ChevronRight, FilePlus, FileMinus, FileEdit } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { getCommitDiff } from '@/lib/gitApi'
+import type { FileDiff, DiffHunk } from '@/lib/gitApi'
+
+interface FileDiffViewProps {
+ sha: string
+ path: string
+ oldPath?: string
+ status: 'added' | 'modified' | 'deleted' | 'renamed'
+}
+
+const statusIcon = {
+ added: <FilePlus className="size-3.5 text-green-600 dark:text-green-400" />,
+ deleted: <FileMinus className="size-3.5 text-red-500 dark:text-red-400" />,
+ modified: <FileEdit className="size-3.5 text-yellow-500 dark:text-yellow-400" />,
+ renamed: <FileEdit className="size-3.5 text-blue-500 dark:text-blue-400" />,
+}
+
+const statusBadge = { added: 'A', deleted: 'D', modified: 'M', renamed: 'R' }
+
+export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps) {
+ const [open, setOpen] = useState(false)
+ const [diff, setDiff] = useState<FileDiff | null>(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState<string | null>(null)
+
+ function toggle() {
+ if (!open && diff === null && !loading) {
+ setLoading(true)
+ getCommitDiff(sha, path)
+ .then(setDiff)
+ .catch((e: Error) => setError(e.message))
+ .finally(() => setLoading(false))
+ }
+ setOpen((v) => !v)
+ }
+
+ return (
+ <div className="divide-y divide-border">
+ {/* File header row — always visible, click to toggle */}
+ <button
+ onClick={toggle}
+ className="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-muted/50 transition-colors"
+ >
+ <ChevronRight
+ className={cn(
+ 'size-3.5 shrink-0 text-muted-foreground transition-transform duration-150',
+ open && 'rotate-90',
+ )}
+ />
+ {statusIcon[status]}
+ <span className="min-w-0 flex-1 font-mono text-sm">
+ {status === 'renamed' ? (
+ <>
+ <span className="text-muted-foreground line-through">{oldPath}</span>
+ {' → '}
+ <span>{path}</span>
+ </>
+ ) : path}
+ </span>
+ <span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
+ {statusBadge[status]}
+ </span>
+ </button>
+
+ {/* Diff body */}
+ {open && (
+ <div className="overflow-x-auto">
+ {loading && (
+ <div className="px-4 py-3 text-xs text-muted-foreground">Loading diff…</div>
+ )}
+ {error && (
+ <div className="px-4 py-3 text-xs text-destructive">Failed to load diff: {error}</div>
+ )}
+ {diff && (
+ diff.isBinary ? (
+ <div className="px-4 py-3 text-xs text-muted-foreground">Binary file</div>
+ ) : diff.hunks.length === 0 ? (
+ <div className="px-4 py-3 text-xs text-muted-foreground">No changes</div>
+ ) : (
+ diff.hunks.map((hunk, i) => <Hunk key={i} hunk={hunk} />)
+ )
+ )}
+ </div>
+ )}
+ </div>
+ )
+}
+
+function Hunk({ hunk }: { hunk: DiffHunk }) {
+ return (
+ <div className="font-mono text-xs leading-5">
+ {/* Hunk header */}
+ <div className="bg-blue-50 px-4 py-0.5 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400 select-none">
+ @@ -{hunk.oldStart},{hunk.oldLines} +{hunk.newStart},{hunk.newLines} @@
+ </div>
+ {hunk.lines.map((line, i) => (
+ <div
+ key={i}
+ className={cn(
+ 'flex',
+ line.type === 'added' && 'bg-green-50 dark:bg-green-950/30',
+ line.type === 'deleted' && 'bg-red-50 dark:bg-red-950/30',
+ )}
+ >
+ {/* Old line number */}
+ <span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
+ {line.oldLine || ''}
+ </span>
+ {/* New line number */}
+ <span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
+ {line.newLine || ''}
+ </span>
+ {/* Sign */}
+ <span className={cn(
+ 'w-5 shrink-0 select-none text-center',
+ line.type === 'added' && 'text-green-600 dark:text-green-400',
+ line.type === 'deleted' && 'text-red-500 dark:text-red-400',
+ line.type === 'context' && 'text-muted-foreground/40',
+ )}>
+ {line.type === 'added' ? '+' : line.type === 'deleted' ? '-' : ' '}
+ </span>
+ {/* Content */}
+ <pre className={cn(
+ 'flex-1 overflow-visible whitespace-pre px-2',
+ line.type === 'added' && 'text-green-900 dark:text-green-200',
+ line.type === 'deleted' && 'text-red-900 dark:text-red-200',
+ )}>
+ {line.content}
+ </pre>
+ </div>
+ ))}
+ </div>
+ )
+}
@@ -53,6 +53,30 @@ export interface GitCommitDetail extends GitCommit {
}>
}
+export interface DiffLine {
+ type: 'context' | 'added' | 'deleted'
+ content: string
+ oldLine: number
+ newLine: number
+}
+
+export interface DiffHunk {
+ oldStart: number
+ oldLines: number
+ newStart: number
+ newLines: number
+ lines: DiffLine[]
+}
+
+export interface FileDiff {
+ path: string
+ oldPath?: string
+ isBinary: boolean
+ isNew: boolean
+ isDelete: boolean
+ hunks: DiffHunk[]
+}
+
// ── Fetch helpers ─────────────────────────────────────────────────────────────
async function get<T>(path: string, params: Record<string, string> = {}): Promise<T> {
@@ -97,3 +121,7 @@ export function getCommits(
export function getCommit(sha: string): Promise<GitCommitDetail> {
return get(`/git/commits/${sha}`)
}
+
+export function getCommitDiff(sha: string, path: string): Promise<FileDiff> {
+ return get(`/git/commits/${sha}/diff`, { path })
+}
@@ -1,22 +1,12 @@
import { useState, useEffect } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { format } from 'date-fns'
-import { ArrowLeft, FilePlus, FileMinus, FileEdit, GitCommit } from 'lucide-react'
+import { ArrowLeft, GitCommit } from 'lucide-react'
import { Skeleton } from '@/components/ui/skeleton'
import { getCommit } from '@/lib/gitApi'
import type { GitCommitDetail } from '@/lib/gitApi'
import { useRepo } from '@/lib/repo'
-
-const statusIcon = {
- added: <FilePlus className="size-4 text-green-600 dark:text-green-400" />,
- deleted: <FileMinus className="size-4 text-red-500 dark:text-red-400" />,
- modified: <FileEdit className="size-4 text-yellow-500 dark:text-yellow-400" />,
- renamed: <FileEdit className="size-4 text-blue-500 dark:text-blue-400" />,
-}
-
-const statusLabel = {
- added: 'A', deleted: 'D', modified: 'M', renamed: 'R',
-}
+import { FileDiffView } from '@/components/code/FileDiffView'
// Commit detail page (/:repo/commit/:hash). Shows commit metadata, full message,
// parent links, and the list of files changed with add/modify/delete/rename status.
@@ -104,7 +94,7 @@ export function CommitPage() {
</div>
</div>
- {/* Changed files */}
+ {/* Changed files — each row is collapsible and loads its diff lazily */}
<div>
<h2 className="mb-3 text-sm font-semibold text-muted-foreground">
{commit.files.length} file{commit.files.length !== 1 ? 's' : ''} changed
@@ -114,28 +104,13 @@ export function CommitPage() {
<p className="px-4 py-4 text-sm text-muted-foreground">No file changes.</p>
)}
{commit.files.map((file) => (
- <div key={file.path} className="flex items-center gap-3 px-4 py-2.5">
- <span
- className="w-4 shrink-0 text-center font-mono text-xs font-bold"
- title={file.status}
- >
- {statusIcon[file.status]}
- </span>
- <span className="min-w-0 flex-1 font-mono text-sm">
- {file.status === 'renamed' ? (
- <>
- <span className="text-muted-foreground line-through">{file.oldPath}</span>
- {' → '}
- <span>{file.path}</span>
- </>
- ) : (
- file.path
- )}
- </span>
- <span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
- {statusLabel[file.status]}
- </span>
- </div>
+ <FileDiffView
+ key={file.path}
+ sha={commit.hash}
+ path={file.path}
+ oldPath={file.oldPath}
+ status={file.status}
+ />
))}
</div>
</div>