11import { randomUUID } from "node:crypto" ;
2- import { existsSync , mkdirSync , readFileSync , writeFileSync } from "node:fs" ;
2+ import { existsSync , mkdirSync , readFileSync , statSync , writeFileSync } from "node:fs" ;
33import { join , resolve } from "node:path" ;
44
55export const ID_PATH = join ( ".hyperframes" , "history-id" ) ;
66/** The only shape minted here; the id is project content and becomes a path, so nothing else is trusted. */
77const ID_SHAPE = / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / ;
88
9- function readId ( projectDir : string ) : string | null {
9+ export function readId ( projectDir : string ) : string | null {
1010 try {
1111 const id = readFileSync ( join ( projectDir , ID_PATH ) , "utf-8" ) . trim ( ) ;
1212 return ID_SHAPE . test ( id ) ? id : null ;
@@ -15,28 +15,41 @@ function readId(projectDir: string): string | null {
1515 }
1616}
1717
18- function recordedDir ( historyDir : string ) : string | null {
18+ type Recorded = { dir ?: string ; dev ?: number ; ino ?: number } ;
19+
20+ function recorded ( historyDir : string ) : Recorded | null {
1921 try {
20- return JSON . parse ( readFileSync ( join ( historyDir , "project.json" ) , "utf-8" ) ) . dir ?? null ;
22+ return JSON . parse ( readFileSync ( join ( historyDir , "project.json" ) , "utf-8" ) ) ;
2123 } catch {
2224 return null ;
2325 }
2426}
2527
28+ function isCopy (
29+ was : Recorded ,
30+ id : string ,
31+ dir : string ,
32+ folder : { dev : number ; ino : number } ,
33+ ) : boolean {
34+ if ( ! was . dir || ( was . ino === folder . ino && was . dev === folder . dev ) ) return false ;
35+ return was . dir === dir ? was . ino !== undefined : existsSync ( was . dir ) && readId ( was . dir ) === id ;
36+ }
37+
2638/**
27- * The project's history id, kept in the project so a rename or move keeps its history. A folder whose id is
28- * still carried by the folder that history was recorded for is a copy, and gets an id of its own.
39+ * The project's history id, kept in the project so a rename or move keeps its history. A folder carrying the id of
40+ * a recorded folder that still has it, or standing where another recorded folder was, is a copy: it gets its own id .
2941 */
3042export function projectHistoryId ( projectDir : string , historyRoot : string ) : string {
3143 const dir = resolve ( projectDir ) ;
44+ const { dev, ino } = statSync ( dir ) ;
3245 let id = readId ( dir ) ;
33- const was = id && recordedDir ( join ( historyRoot , id ) ) ;
34- if ( ! id || ( was && was !== dir && existsSync ( was ) && readId ( was ) === id ) ) {
46+ const was = id && recorded ( join ( historyRoot , id ) ) ;
47+ if ( ! id || ( was && isCopy ( was , id , dir , { dev , ino } ) ) ) {
3548 id = randomUUID ( ) ;
3649 mkdirSync ( join ( dir , ".hyperframes" ) , { recursive : true } ) ;
3750 writeFileSync ( join ( dir , ID_PATH ) , `${ id } \n` ) ;
3851 }
3952 mkdirSync ( join ( historyRoot , id ) , { recursive : true } ) ;
40- writeFileSync ( join ( historyRoot , id , "project.json" ) , JSON . stringify ( { dir } ) ) ;
53+ writeFileSync ( join ( historyRoot , id , "project.json" ) , JSON . stringify ( { dir, dev , ino } ) ) ;
4154 return id ;
4255}
0 commit comments