33 getHistory ,
44 getCommitFiles ,
55 getCommitParents ,
6+ getStaleBranches ,
67 type CommitInfo ,
78 type CommitFile ,
89 type SelectedDiffInfo ,
@@ -40,10 +41,10 @@ function parseRefNames(refNames: string) {
4041}
4142
4243const REF_BADGE : Record < string , string > = {
43- head : "bg-green-600 text-white" ,
44+ head : "bg-green-600 text-white" ,
4445 branch : "bg-blue-500 text-white" ,
4546 remote : "bg-gray-500 text-white" ,
46- tag : "bg-yellow-500 text-white" ,
47+ tag : "bg-yellow-500 text-white" ,
4748} ;
4849
4950function formatDate ( ts : number ) {
@@ -97,32 +98,38 @@ function ContextMenu({ x, y, commit, repoPath, onClose, onRefresh }: ContextMenu
9798 } ;
9899
99100 const items : { label : string ; action : ( ) => void ; danger ?: boolean } [ ] = [
100- { label : "Checkout" , action : ( ) => run ( [ "checkout" , commit . hash ] ) } ,
101- { label : "Create Branch here…" , action : async ( ) => {
101+ { label : "Checkout" , action : ( ) => run ( [ "checkout" , commit . hash ] ) } ,
102+ {
103+ label : "Create Branch here…" , action : async ( ) => {
102104 onClose ( ) ;
103105 const name = prompt ( "New branch name:" ) ; if ( ! name ?. trim ( ) ) return ;
104106 try { await invoke ( "run_git_simple" , { args : [ "checkout" , "-b" , name . trim ( ) , commit . hash ] , cwd : repoPath } ) ; onRefresh ( ) ; }
105107 catch ( e ) { alert ( String ( e ) ) ; }
106- } } ,
107- { label : "Create Tag here…" , action : async ( ) => {
108+ }
109+ } ,
110+ {
111+ label : "Create Tag here…" , action : async ( ) => {
108112 onClose ( ) ;
109113 const name = prompt ( "Tag name:" ) ; if ( ! name ?. trim ( ) ) return ;
110114 try { await invoke ( "run_git_simple" , { args : [ "tag" , name . trim ( ) , commit . hash ] , cwd : repoPath } ) ; onRefresh ( ) ; }
111115 catch ( e ) { alert ( String ( e ) ) ; }
112- } } ,
113- { label : "Cherry-pick" , action : async ( ) => {
116+ }
117+ } ,
118+ {
119+ label : "Cherry-pick" , action : async ( ) => {
114120 onClose ( ) ;
115121 try { await invoke ( "run_git" , { args : [ "cherry-pick" , commit . hash ] , cwd : repoPath } ) ; }
116122 catch { /* run_git only fails if git is not found; conflicts are non-throwing */ }
117123 onRefresh ( ) ;
118- } } ,
119- { label : "──" , action : ( ) => { } } ,
120- { label : "Reset Soft" , action : ( ) => { if ( confirm ( `Reset soft to ${ commit . short_hash } ?` ) ) run ( [ "reset" , "--soft" , commit . hash ] ) ; } } ,
121- { label : "Reset Mixed" , action : ( ) => { if ( confirm ( `Reset mixed to ${ commit . short_hash } ?` ) ) run ( [ "reset" , "--mixed" , commit . hash ] ) ; } } ,
124+ }
125+ } ,
126+ { label : "──" , action : ( ) => { } } ,
127+ { label : "Reset Soft" , action : ( ) => { if ( confirm ( `Reset soft to ${ commit . short_hash } ?` ) ) run ( [ "reset" , "--soft" , commit . hash ] ) ; } } ,
128+ { label : "Reset Mixed" , action : ( ) => { if ( confirm ( `Reset mixed to ${ commit . short_hash } ?` ) ) run ( [ "reset" , "--mixed" , commit . hash ] ) ; } } ,
122129 { label : "Reset Hard" , danger : true , action : ( ) => { if ( confirm ( `Reset HARD to ${ commit . short_hash } ?` ) ) run ( [ "reset" , "--hard" , commit . hash ] ) ; } } ,
123- { label : "──" , action : ( ) => { } } ,
124- { label : "Copy Commit ID" , action : ( ) => { onClose ( ) ; navigator . clipboard . writeText ( commit . hash ) . catch ( ( ) => { } ) ; } } ,
125- { label : "Copy Short ID" , action : ( ) => { onClose ( ) ; navigator . clipboard . writeText ( commit . short_hash ) . catch ( ( ) => { } ) ; } } ,
130+ { label : "──" , action : ( ) => { } } ,
131+ { label : "Copy Commit ID" , action : ( ) => { onClose ( ) ; navigator . clipboard . writeText ( commit . hash ) . catch ( ( ) => { } ) ; } } ,
132+ { label : "Copy Short ID" , action : ( ) => { onClose ( ) ; navigator . clipboard . writeText ( commit . short_hash ) . catch ( ( ) => { } ) ; } } ,
126133 ] ;
127134
128135 return (
@@ -132,9 +139,9 @@ function ContextMenu({ x, y, commit, repoPath, onClose, onRefresh }: ContextMenu
132139 item . label === "──"
133140 ? < div key = { i } className = "border-t border-gray-200 dark:border-gray-700 my-1" />
134141 : < button key = { i } onClick = { item . action }
135- className = { `w-full text-left px-4 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 ${ item . danger ? "text-red-600 dark:text-red-400" : "" } ` } >
136- { item . label }
137- </ button >
142+ className = { `w-full text-left px-4 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 ${ item . danger ? "text-red-600 dark:text-red-400" : "" } ` } >
143+ { item . label }
144+ </ button >
138145 ) }
139146 </ div >
140147 ) ;
@@ -195,6 +202,7 @@ export function HistoryTab({ repoPath, filePath, refreshKey, onRefresh, onFileSe
195202 const [ contextMenu , setContextMenu ] = useState < { x : number ; y : number ; commit : CommitInfo } | null > ( null ) ;
196203 const [ fileCtxMenu , setFileCtxMenu ] = useState < { x : number ; y : number ; absolutePath : string } | null > ( null ) ;
197204 const [ allBranches , setAllBranches ] = useState ( true ) ;
205+ const [ staleBranches , setStaleBranches ] = useState < Set < string > > ( new Set ( ) ) ;
198206
199207 // ── load history ──────────────────────────────────────────────────────────
200208
@@ -222,6 +230,13 @@ export function HistoryTab({ repoPath, filePath, refreshKey, onRefresh, onFileSe
222230 loadHistory ( 0 ) ;
223231 } , [ repoPath , refreshKey , loadHistory ] ) ;
224232
233+ useEffect ( ( ) => {
234+ if ( ! repoPath ) return ;
235+ getStaleBranches ( repoPath )
236+ . then ( ( branches ) => setStaleBranches ( new Set ( branches ) ) )
237+ . catch ( ( ) => setStaleBranches ( new Set ( ) ) ) ;
238+ } , [ repoPath , refreshKey ] ) ;
239+
225240 // ── select commit → load files + parents ─────────────────────────────────
226241
227242 const handleSelectCommit = useCallback ( async ( commit : CommitInfo ) => {
@@ -321,30 +336,40 @@ export function HistoryTab({ repoPath, filePath, refreshKey, onRefresh, onFileSe
321336 key = { commit . hash }
322337 onClick = { ( ) => handleSelectCommit ( commit ) }
323338 onContextMenu = { ( e ) => { e . preventDefault ( ) ; setContextMenu ( { x : e . clientX , y : e . clientY , commit } ) ; } }
324- className = { `flex items-center gap-2 px-2 cursor-pointer border-b border-gray-100 dark:border-gray-800 ${
325- isSelected ? "bg-blue-50 dark:bg-blue-900/30" : "hover:bg-gray-50 dark:hover:bg-gray-800"
326- } `}
339+ className = { `flex items-center gap-2 px-2 cursor-pointer border-b border-gray-100 dark:border-gray-800 ${ isSelected ? "bg-blue-50 dark:bg-blue-900/30" : "hover:bg-gray-50 dark:hover:bg-gray-800"
340+ } `}
327341 style = { { height : ROW_H } }
328342 >
329343 { /* Graph (SVG) */ }
330344 { graphRow && < GraphSvg row = { graphRow } /> }
331345 { /* Message (with inline refs) */ }
332346 < div className = "flex-1 min-w-0 flex items-center gap-1" >
333- { refs . map ( ( r , i ) => (
334- < span key = { i } className = { `text-[10px] px-1 rounded leading-4 shrink-0 inline-flex items-center gap-0.5 ${ REF_BADGE [ r . kind ] } ` } >
335- { ( r . kind === "head" || r . kind === "branch" || r . kind === "remote" ) && (
336- < svg width = "10" height = "10" viewBox = "0 0 16 16" fill = "currentColor" className = "shrink-0" >
337- < path d = "M11.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zm-2.25.75a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25zM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0z" />
338- </ svg >
339- ) }
340- { r . kind === "tag" && (
341- < svg width = "10" height = "10" viewBox = "0 0 16 16" fill = "currentColor" className = "shrink-0" >
342- < path d = "M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z" />
343- </ svg >
344- ) }
345- { r . label }
346- </ span >
347- ) ) }
347+ { refs . map ( ( r , i ) => {
348+ const isStale = r . kind === "remote" && staleBranches . has ( r . label ) ;
349+ return (
350+ < span key = { i } className = { `inline-flex items-center shrink-0 leading-4 overflow-hidden ${ isStale ? "rounded border border-red-950 bg-red-600" : "rounded" } ` } >
351+ < span className = { `text-[10px] px-1 leading-4 inline-flex items-center gap-0.5 ${ isStale ? "bg-red-800 text-white" : REF_BADGE [ r . kind ] } ` } >
352+ { ( r . kind === "head" || r . kind === "branch" || r . kind === "remote" ) && (
353+ < svg width = "10" height = "10" viewBox = "0 0 16 16" fill = "currentColor" className = "shrink-0" >
354+ < path d = "M11.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zm-2.25.75a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25zM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0z" />
355+ </ svg >
356+ ) }
357+ { r . kind === "tag" && (
358+ < svg width = "10" height = "10" viewBox = "0 0 16 16" fill = "currentColor" className = "shrink-0" >
359+ < path d = "M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z" />
360+ </ svg >
361+ ) }
362+ { r . label }
363+ </ span >
364+ { isStale && (
365+ < >
366+ < span className = "self-stretch w-px bg-red-950" />
367+ < span className = "text-[10px] px-1 leading-4 text-white" > deleted on remote</ span >
368+ </ >
369+ ) }
370+ </ span >
371+ ) ;
372+ } ) }
348373 < span className = "text-xs text-gray-800 dark:text-gray-200 truncate" > { commit . message } </ span >
349374 </ div >
350375 { /* Author */ }
@@ -421,9 +446,8 @@ export function HistoryTab({ repoPath, filePath, refreshKey, onRefresh, onFileSe
421446 < div key = { i }
422447 onClick = { ( ) => handleSelectFile ( file ) }
423448 onContextMenu = { ( e ) => { e . preventDefault ( ) ; setFileCtxMenu ( { x : e . clientX , y : e . clientY , absolutePath : absPath } ) ; } }
424- className = { `flex items-center gap-2 px-3 py-1 text-xs cursor-pointer border-b border-gray-50 dark:border-gray-800 ${
425- isSelected ? "bg-blue-50 dark:bg-blue-900/30" : "hover:bg-gray-50 dark:hover:bg-gray-800"
426- } `}
449+ className = { `flex items-center gap-2 px-3 py-1 text-xs cursor-pointer border-b border-gray-50 dark:border-gray-800 ${ isSelected ? "bg-blue-50 dark:bg-blue-900/30" : "hover:bg-gray-50 dark:hover:bg-gray-800"
450+ } `}
427451 >
428452 < span className = { `font-mono font-bold w-4 shrink-0 ${ si . cls } ` } > { si . icon } </ span >
429453 < span className = "truncate" title = { file . display } > { file . display } </ span >
0 commit comments