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,36 @@ function readId(projectDir: string): string | null {
1515 }
1616}
1717
18- function recordedDir ( historyDir : string ) : string | null {
18+ type Recorded = { dir ?: string ; 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 ( was : Recorded , id : string , dir : string , ino : number ) : boolean {
29+ if ( ! was . dir || was . ino === ino ) return false ;
30+ return was . dir === dir ? was . ino !== undefined : existsSync ( was . dir ) && readId ( was . dir ) === id ;
31+ }
32+
2633/**
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.
34+ * The project's history id, kept in the project so a rename or move keeps its history. A folder carrying the id of
35+ * a recorded folder that still has it, or standing where another recorded folder was, is a copy: it gets its own id .
2936 */
3037export function projectHistoryId ( projectDir : string , historyRoot : string ) : string {
3138 const dir = resolve ( projectDir ) ;
39+ const { ino } = statSync ( dir ) ;
3240 let id = readId ( dir ) ;
33- const was = id && recordedDir ( join ( historyRoot , id ) ) ;
34- if ( ! id || ( was && was !== dir && existsSync ( was ) && readId ( was ) === id ) ) {
41+ const was = id && recorded ( join ( historyRoot , id ) ) ;
42+ if ( ! id || ( was && isCopy ( was , id , dir , ino ) ) ) {
3543 id = randomUUID ( ) ;
3644 mkdirSync ( join ( dir , ".hyperframes" ) , { recursive : true } ) ;
3745 writeFileSync ( join ( dir , ID_PATH ) , `${ id } \n` ) ;
3846 }
3947 mkdirSync ( join ( historyRoot , id ) , { recursive : true } ) ;
40- writeFileSync ( join ( historyRoot , id , "project.json" ) , JSON . stringify ( { dir } ) ) ;
48+ writeFileSync ( join ( historyRoot , id , "project.json" ) , JSON . stringify ( { dir, ino } ) ) ;
4149 return id ;
4250}
0 commit comments