diff --git a/stream/.gitignore b/stream/.gitignore index 0ac9580..f3c4288 100644 --- a/stream/.gitignore +++ b/stream/.gitignore @@ -13,8 +13,6 @@ dist-ssr *.local # Editor directories and files -.vscode/* -!.vscode/extensions.json .idea .DS_Store *.suo diff --git a/stream/.vscode/tasks.json b/stream/.vscode/tasks.json new file mode 100644 index 0000000..0ea6543 --- /dev/null +++ b/stream/.vscode/tasks.json @@ -0,0 +1,28 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check:fix", + "type": "shell", + "command": "pnpm check:fix", + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "dev", + "type": "shell", + "command": "pnpm tauri dev", + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated" + } + } + ] +} diff --git a/stream/src-tauri/tauri.conf.json b/stream/src-tauri/tauri.conf.json index bbb7987..b65d6aa 100644 --- a/stream/src-tauri/tauri.conf.json +++ b/stream/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "stream", - "version": "0.2.5", + "version": "0.2.6", "identifier": "com.marcelmarais.stream", "build": { "beforeDevCommand": "pnpm dev", diff --git a/stream/src/components/commit-overlay.tsx b/stream/src/components/commit-overlay.tsx index 288e459..952c976 100644 --- a/stream/src/components/commit-overlay.tsx +++ b/stream/src/components/commit-overlay.tsx @@ -1,6 +1,11 @@ "use client"; -import { ArrowSquareOutIcon, GitBranchIcon } from "@phosphor-icons/react"; +import { + ArrowSquareOutIcon, + CaretDownIcon, + DotOutlineIcon, + GitBranchIcon, +} from "@phosphor-icons/react"; import { openUrl } from "@tauri-apps/plugin-opener"; import { useState } from "react"; import { @@ -13,7 +18,11 @@ import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import type { GitCommit } from "@/ipc/git-reader"; -import { formatCommitAuthor, getShortCommitId } from "@/ipc/git-reader"; +import { + formatCommitAuthor, + getShortCommitId, + isMainBranch, +} from "@/ipc/git-reader"; interface CommitOverlayProps { commits: GitCommit[]; @@ -26,7 +35,48 @@ interface RepoCardProps { commits: GitCommit[]; } -function RepoCard({ repoName, commits }: RepoCardProps) { +interface BranchGroupProps { + branchName: string; + commits: GitCommit[]; +} + +function groupCommitsByBranch( + commits: GitCommit[], +): Record { + const byBranch: Record = {}; + + for (const commit of commits) { + for (const branch of commit.branches) { + const cleanBranch = branch.replace("origin/", ""); + if (!byBranch[cleanBranch]) { + byBranch[cleanBranch] = []; + } + if (!byBranch[cleanBranch].some((c) => c.id === commit.id)) { + byBranch[cleanBranch].push(commit); + } + } + } + + return byBranch; +} + +function sortBranchNames(branches: string[]): string[] { + return branches.sort((a, b) => { + const aIsMain = isMainBranch(a); + const bIsMain = isMainBranch(b); + if (aIsMain && !bIsMain) return -1; + if (!aIsMain && bIsMain) return 1; + return a.localeCompare(b); + }); +} + +function truncateFilePath(filePath: string): string { + const parts = filePath.split("/"); + if (parts.length <= 2) return filePath; + return `.../${parts.slice(-2).join("/")}`; +} + +function BranchGroup({ branchName, commits }: BranchGroupProps) { const [expanded, setExpanded] = useState(undefined); const [expandedFiles, setExpandedFiles] = useState>(new Set()); @@ -42,6 +92,185 @@ function RepoCard({ repoName, commits }: RepoCardProps) { }); }; + const sortedCommits = [...commits].sort((a, b) => b.timestamp - a.timestamp); + const firstCommit = sortedCommits[0]; + const remainingCommits = sortedCommits.slice(1); + const isExpanded = expanded === "branch"; + + const allCommitsForTimeline = isExpanded ? sortedCommits : [firstCommit]; + + return ( + + + +
+ {branchName} +
+ {remainingCommits.length > 0 && ( +
+ + {isExpanded ? "less" : `${remainingCommits.length} more`} + + +
+ )} +
+ +
+ {allCommitsForTimeline.map((commit, index) => ( + + ))} +
+ + {remainingCommits.length > 0 && } +
+
+ ); +} + +interface CommitItemProps { + commit: GitCommit; + expandedFiles: Set; + toggleFileExpansion: (commitId: string) => void; + compact?: boolean; + isLast?: boolean; +} + +function CommitItem({ + commit, + expandedFiles, + toggleFileExpansion, + compact = false, + isLast = false, +}: CommitItemProps) { + const time = new Date(commit.timestamp).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + const url = commit.url; + + return ( +
+ {/* Timeline dot */} +
+ {/* Timeline line */} + {!isLast && ( +
+ )} + +
+ {/* Row 1: Time + Message */} +
+ {time} + + {commit.message} + +
+ + {/* Row 2: Author + Commit ID */} +
+ {formatCommitAuthor(commit)} + + {url ? ( + + ) : ( + {getShortCommitId(commit.id)} + )} +
+ + {/* Row 3: File badges */} + {!compact && commit.files_changed.length > 0 && ( +
+ {(() => { + const isExpanded = expandedFiles.has(commit.id); + const filesToShow = isExpanded + ? commit.files_changed + : commit.files_changed.slice(0, 3); + const remainingCount = commit.files_changed.length - 3; + + return ( + <> + {filesToShow.map((file) => ( + + {truncateFilePath(file)} + + ))} + {!isExpanded && remainingCount > 0 && ( + { + e.stopPropagation(); + toggleFileExpansion(commit.id); + }} + > + +{remainingCount} more + + )} + {isExpanded && commit.files_changed.length > 3 && ( + { + e.stopPropagation(); + toggleFileExpansion(commit.id); + }} + > + less + + )} + + ); + })()} +
+ )} +
+
+ ); +} + +function RepoCard({ repoName, commits }: RepoCardProps) { + const [expanded, setExpanded] = useState(undefined); + + const commitsByBranch = groupCommitsByBranch(commits); + const sortedBranches = sortBranchNames(Object.keys(commitsByBranch)); + return ( - -
- {commits.map((commit) => { - const time = new Date(commit.timestamp).toLocaleTimeString(); - const url = commit.url; - return ( -
-
-
- {url ? ( - - ) : ( - - {getShortCommitId(commit.id)} - - )} - - {time} - -
- {commit.branches.map((branch) => { - const cleanBranch = branch.replace("origin/", ""); - const isMainBranch = [ - "main", - "master", - "develop", - ].includes(cleanBranch); - return ( - - {cleanBranch} - - ); - })} -
-
- - {formatCommitAuthor(commit)} - -
-
- {commit.message} -
- {commit.files_changed.length > 0 && ( -
-
- Files -
- -
- {(() => { - const isExpanded = expandedFiles.has(commit.id); - const filesToShow = isExpanded - ? commit.files_changed - : commit.files_changed.slice(0, 3); - const remainingCount = - commit.files_changed.length - 3; - - return ( - <> - {filesToShow.map((file) => ( - - {file} - - ))} - {!isExpanded && remainingCount > 0 && ( - - toggleFileExpansion(commit.id) - } - > - +{remainingCount} others - - )} - {isExpanded && - commit.files_changed.length > 3 && ( - - toggleFileExpansion(commit.id) - } - > - Show less - - )} - - ); - })()} -
-
-
- )} -
- ); - })} -
+ + +
+ {sortedBranches.map((branchName) => ( + + ))} +
+
@@ -205,7 +320,6 @@ export function CommitOverlay({ commits, className = "" }: CommitOverlayProps) { return null; } - // Group commits by repository const commitsByRepo = commits.reduce( (acc, commit) => { const repoName = commit.repo_path.split("/").pop() || commit.repo_path; diff --git a/stream/src/components/ui/accordion.tsx b/stream/src/components/ui/accordion.tsx index ec4490f..d73cc48 100644 --- a/stream/src/components/ui/accordion.tsx +++ b/stream/src/components/ui/accordion.tsx @@ -35,13 +35,13 @@ function AccordionTrigger({ svg]:rotate-180", + "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", className, )} {...props} > {children} - + ); @@ -55,7 +55,7 @@ function AccordionContent({ return (
{children}
diff --git a/stream/src/ipc/git-reader.ts b/stream/src/ipc/git-reader.ts index fa8dbc8..7741847 100644 --- a/stream/src/ipc/git-reader.ts +++ b/stream/src/ipc/git-reader.ts @@ -185,6 +185,19 @@ export function getShortCommitId(commitId: string): string { return commitId.substring(0, 7); } +const MAIN_BRANCH_NAMES = [ + "main", + "master", + "origin/main", + "origin/master", + "develop", + "origin/develop", +]; + +export function isMainBranch(branchName: string): boolean { + return MAIN_BRANCH_NAMES.includes(branchName); +} + /** * Filter commits based on author, repository, and search criteria */