[{"data":1,"prerenderedAt":5756},["ShallowReactive",2],{"/articles/git-repo":3},{"id":4,"title":5,"body":6,"date":5739,"description":5740,"extension":5741,"head":5742,"image":16,"meta":5743,"navigation":428,"ogImage":5742,"path":5744,"readingTime":5745,"robots":5742,"schemaOrg":5742,"seo":5746,"sitemap":5747,"stem":5748,"tags":5749,"__hash__":5755},"articles/articles/00.git-repo.md","The Git Repo Explorer: Building a Repository Viewer in the Browser",{"type":7,"value":8,"toc":5706},"minimark",[9,17,20,23,40,43,46,49,52,58,63,66,69,91,94,100,104,107,127,130,208,220,223,227,230,251,254,261,272,275,278,281,285,288,295,298,301,314,317,320,324,333,342,345,354,359,362,577,580,591,596,600,603,610,771,778,892,899,902,919,923,926,933,948,1222,1225,1309,1312,1315,1318,1337,1343,1347,1357,1368,1371,1473,1483,1902,1916,1923,1927,1936,2209,2212,2215,2221,2225,2231,2238,2241,2249,2252,2491,2506,2697,2700,2711,2715,2722,2886,2897,2901,2908,2911,2914,3165,3175,3178,3182,3185,3188,3574,3577,3585,3588,3592,3595,3598,3683,3686,3789,3796,3898,3901,3911,3915,3918,4112,4115,4122,4140,4146,4224,4233,4237,4251,4370,4373,4376,4387,4391,4394,4397,4403,4584,4587,4611,4729,4732,4739,4743,4750,4753,4756,4759,4763,4770,4773,4776,4779,5084,5095,5254,5261,5264,5271,5275,5278,5281,5284,5442,5453,5456,5460,5463,5466,5469,5472,5475,5479,5482,5485,5496,5499,5502,5509,5512,5516,5519,5551,5554,5557,5572,5575,5579,5582,5588,5594,5597,5601,5604,5607,5639,5642,5645,5649,5656,5659,5662,5665,5668,5671,5674,5702],[10,11,12],"p",{},[13,14],"img",{"alt":15,"src":16},"A smiling tree representing the read-only Git repository viewer","/articles/git-blog.png",[10,18,19],{},"I wanted people to browse my repositories without leaving this website.",[10,21,22],{},"That was the whole idea. A project card could explain what I had built, but the\nmoment someone wanted to read the README, inspect a commit, or open the source,\nthe portfolio handed them to GitHub. The interesting part of the project lived\nsomewhere else.",[10,24,25,26,33,34,39],{},"A lot of the inspiration came from the projects on\n",[27,28,32],"a",{"href":29,"rel":30},"https://www.zx2c4.com/",[31],"nofollow","zx2c4.com",", which open in\n",[27,35,38],{"href":36,"rel":37},"https://git.zx2c4.com/cgit/about/",[31],"cgit",". I love how direct that experience is.\nThere is no attempt to turn a repository into a social feed or a project\nmanagement suite. You open a repository, browse its tree, read its history,\ninspect a diff, and get on with your life.",[10,41,42],{},"I wanted that same directness here. I did not want to copy cgit's interface or\nport its C code to the browser. I wanted the repositories to feel like a native\npart of the portfolio instead of a collection of external links.",[10,44,45],{},"There was one hard constraint: this site already runs on Cloudflare for almost\nnothing, and I wanted to keep it that way. Adding a virtual machine, running a container,\nor introducing another stateful service to monitor would\nhave been a ridiculous price to pay for a read-only portfolio feature.",[10,47,48],{},"That constraint changed the question. I was no longer asking, \"How do I build a\nsmaller GitHub?\" I was asking, \"How little repository infrastructure can I get\naway with?\"",[10,50,51],{},"The answer turned out to be stranger than I expected. The server does not\nanswer questions such as \"which files are in this directory?\" or \"what changed\nin this commit?\" It serves Git's native files. The browser reconstructs the\nrepository in IndexedDB, a Web Worker interprets it, and Vue renders the result.",[53,54,55],"note",{},[10,56,57],{},"This is a cgit-inspired design, not a port of cgit. cgit reads\nrepositories on the server through a CGI application; this viewer ships the\nrepository's object database to a read-only Git client in the browser.",[59,60,62],"h2",{"id":61},"the-part-of-cgit-i-wanted","The part of cgit I wanted",[10,64,65],{},"cgit helped me separate repository browsing from everything that have been\ngrown around it. Its feature list is almost exactly the vocabulary I cared\nabout: logs, diffs, trees, refs, and cloneable URLs. For a personal portfolio,\nthat is already a remarkably complete product.",[10,67,68],{},"I did not need pull requests, issues, permissions, merge queues, actions, or\ncollaborative editing. I needed the part of Git that helps someone understand a\nproject:",[70,71,72,76,79,82,85,88],"ul",{},[73,74,75],"li",{},"Browse repository trees, files, branches, tags, and historical commits.",[73,77,78],{},"Show the commit that last changed each visible file or directory.",[73,80,81],{},"Render Markdown, highlighted source code, images, and unsupported-file states.",[73,83,84],{},"Build split and unified diffs without asking GitHub to calculate them.",[73,86,87],{},"Give every tree, file, history, and commit a URL that can be shared.",[73,89,90],{},"Export a snapshot when someone wants the code outside the viewer.",[10,92,93],{},"The result should still feel like this site. Nuxt would own the routes, layouts,\nand application state. Nuxt UI would provide the repository shell, menus,\nsidebars, tabs, timelines, and loading states. The repository itself would be\nthe unusual part.",[95,96,97],"tip",{},[10,98,99],{},"The viewer is intentionally read-only. It can browse branches and tags, inspect\nhistory, render files, calculate diffs, and export a ZIP, but it never writes to\nthe upstream repository.",[59,101,103],{"id":102},"the-constraint-that-shaped-the-design","The constraint that shaped the design",[10,105,106],{},"Keeping the feature cheap meant keeping the runtime small. More specifically, I\nwanted to:",[70,108,109,112,115,118,121,124],{},[73,110,111],{},"Keep the normal portfolio statically generated.",[73,113,114],{},"Avoid doing heavy Git work on the main browser thread.",[73,116,117],{},"Cache repository data so a second visit is significantly cheaper.",[73,119,120],{},"Support Cloudflare R2 in production and a local repository directory when\nself-hosting.",[73,122,123],{},"Avoid an always-on Git application server or repository database.",[73,125,126],{},"Keep the server-side read path small, cacheable, and read-only.",[10,128,129],{},"There were several reasonable ways to build the feature. They simply put the\ncost, complexity, and ownership in different places.",[131,132,133,149],"table",{},[134,135,136],"thead",{},[137,138,139,143,146],"tr",{},[140,141,142],"th",{},"Design",[140,144,145],{},"What I would gain",[140,147,148],{},"Why I did not choose it",[150,151,152,164,175,186,197],"tbody",{},[137,153,154,158,161],{},[155,156,157],"td",{},"Link to GitHub",[155,159,160],{},"No viewer to build or operate",[155,162,163],{},"The repository still leaves the portfolio",[137,165,166,169,172],{},[155,167,168],{},"Read through GitHub's API",[155,170,171],{},"Structured commits, trees, and blobs",[155,173,174],{},"The runtime would depend on GitHub's API model and limits",[137,176,177,180,183],{},[155,178,179],{},"Generate repository-shaped JSON",[155,181,182],{},"Static delivery and predictable rendering",[155,184,185],{},"I would create and maintain a second database derived from Git",[137,187,188,191,194],{},[155,189,190],{},"Run cgit or another Git service",[155,192,193],{},"A mature server-side repository browser",[155,195,196],{},"It needs a Git-aware server process and infrastructure around it",[137,198,199,202,205],{},[155,200,201],{},"Read Git in the browser",[155,203,204],{},"A thin backend and a locally cached repository",[155,206,207],{},"The visitor pays the first-load bandwidth and computation cost",[10,209,210,211,215,216,219],{},"The obvious custom design would expose endpoints such as\n",[212,213,214],"code",{},"/api/repos/:name/tree"," and ",[212,217,218],{},"/api/repos/:name/blob",". The server would open a\nrepository, turn Git objects into JSON, and repeat that interpretation for\nevery visitor.",[10,221,222],{},"That architecture is valid, and for very large repositories it is probably the\nbetter trade. It would also turn this portfolio into a Git application server.\nI wanted to find out how far I could get if the server only delivered the\nrepository and the client understood Git itself.",[59,224,226],{"id":225},"then-i-learned-what-a-git-repository-actually-is","Then I learned what a Git repository actually is",[10,228,229],{},"The first thing I learned was embarrassingly basic: Git is a fucking database.",[10,231,232,233,236,237,236,240,236,243,246,247,250],{},"Before building the viewer, I mostly interacted with Git through commands:\n",[212,234,235],{},"status",", ",[212,238,239],{},"add",[212,241,242],{},"commit",[212,244,245],{},"log",", and the occasional desperate ",[212,248,249],{},"reflog",". I knew\nGit stored history, obviously, but I still pictured a repository as a folder of\nfiles with commits attached to it. That mental model is useful when writing\ncode and almost useless when building a repository browser.",[10,252,253],{},"The working tree is only one view. Underneath it, Git stores an object graph.",[10,255,256,257,260],{},"A ref such as ",[212,258,259],{},"main"," points to a commit ID. The commit stores metadata, parent\ncommit IDs, and a root tree ID. A tree stores names, modes, and IDs for blobs or\nother trees, while a blob stores the raw bytes of one file.",[262,263,270],"pre",{"className":264,"code":266,"filename":267,"language":268,"meta":269},[265],"language-text","refs/heads/main\n      |\n      v\ncommit 8f62...  --parent--> commit 51ac...\n      |\n      v\ntree a91e...\n      |\n      +-- app/ --------> tree 0b7c...\n      |                     |\n      |                     +-- app.vue -> blob 312d...\n      |\n      +-- README.md ---> blob f6e4...\n","A simplified Git object graph","text","",[212,271,266],{"__ignoreMap":269},[10,273,274],{},"Objects are addressed by hashes derived from their contents. Change a file and\nits blob ID changes; that change produces a new tree ID and, eventually, a new\ncommit. Git can then compress many of those objects into packfiles without\nchanging the logical graph.",[10,276,277],{},"That changed the whole design. Git already contained nearly everything the UI\nneeded. I did not need a table of files, a table of directories, a table of\ncommit relationships, and another table of file versions. I needed a way to\npublish Git's existing object database and a client capable of asking it the\nright questions.",[10,279,280],{},"The cost constraint and the object model suddenly pointed in the same\ndirection. If a bare repository is already a database made from files, object\nstorage can publish those files. If JavaScript can read the database, the\nbrowser can derive the application views.",[59,282,284],{"id":283},"the-complete-design","The complete design",[10,286,287],{},"Once I stopped treating Git as something that had to live behind a Git server,\nthe architecture became two separate paths. A publishing path turns my GitHub\nrepositories into static, HTTP-readable Git data. A reading path rebuilds that\ndata in the visitor's browser.",[262,289,293],{"className":290,"code":291,"filename":292,"language":268,"meta":269},[265],"Publishing\n----------\n\nGitHub webhook\n      |\n      v\nNitro webhook route -- verifies HMAC signature\n      |\n      v\nGitHub Actions workflow\n      |\n      +-- git clone --bare / git fetch --all\n      +-- git update-server-info\n      |\n      v\nCloudflare R2: repos/\u003Crepository>.git/**\n\n\nReading\n-------\n\nNuxt repository route\n      |\n      v\n/api/git-proxy/\u003Crepository>.git/**\n      |\n      +-- Cloudflare R2 in production\n      +-- local repos directory when self-hosted\n      |\n      v\nHEAD + refs + pack indexes + packfiles\n      |\n      v\nLightningFS -> IndexedDB\n      |\n      v\nWeb Worker -> isomorphic-git -> commits / trees / blobs / diffs\n      |\n      v\nReactive Vue state -> Nuxt UI repository interface\n","The complete data flow",[212,294,291],{"__ignoreMap":269},[10,296,297],{},"The finished diagram hides how gradually those boundaries appeared. The\npublishing pipeline landed on July 4, 2026, and the first main-thread explorer\nfollowed two days later. Caching, local diffs, incremental pack updates, and\nbetter failure states arrived before Git moved into a worker on July 11; the\ntree-OID shortcuts and larger showcase repositories came later still.",[10,299,300],{},"I did not design every box up front. I first validated that raw Git data could\ntravel through static storage, then built the viewer, then discovered which\nparts of that viewer were too expensive to leave naïve.",[10,302,303,304,309,310,313],{},"When someone opens ",[27,305,307],{"href":306},"/repo/portfolio",[212,308,306],{},", the Nuxt page starts a\nworker, asks the proxy for the repository's discovery files and packs, stores\nthem beneath a virtual ",[212,311,312],{},".git"," directory, and resolves the selected branch. Only\nthen do commits, folders, files, and diffs become application data.",[10,315,316],{},"There is still server-side code in this architecture, but it does not\nunderstand Git objects. It understands file paths, storage, HTTP caching,\nwebhook signatures, and little else. The repository model lives in the browser.",[10,318,319],{},"That distinction is important. The Git proxy is not a repository API; it is a\ntransport layer for raw Git data.",[59,321,323],{"id":322},"turning-repositories-into-static-files","Turning repositories into static files",[10,325,326,327,332],{},"The first practical problem was making a repository available without running a\nsmart Git HTTP server. Git already had a beautifully simple publication model\nfor this: the\n",[27,328,331],{"href":329,"rel":330},"https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols.html",[31],"Dumb HTTP protocol",".",[10,334,335,336,215,339,332],{},"\"Dumb\" is not an insult here. It means the HTTP server doesn't negotiate a\ncustom Git session or generate a pack on demand. It serves an existing bare\nrepository like an ordinary directory, and the client discovers what to fetch\nthrough files such as ",[212,337,338],{},"info/refs",[212,340,341],{},"objects/info/packs",[10,343,344],{},"My browser client does not implement every part of Dumb HTTP. It uses the\nstatic-pack subset produced for these repositories: discovery metadata plus\nevery advertised pack index and packfile. It does not chase loose objects,\nalternates, or individual loose refs on demand.",[10,346,347,348,353],{},"That static layout maps unusually well to object storage.\n",[27,349,352],{"href":350,"rel":351},"https://www.cloudflare.com/products/r2/",[31],"Cloudflare R2"," can store and serve the\nfiles without knowing what a branch or commit is. It also fits the reason I\nstarted down this path: the repository data stays inside the Cloudflare setup I\nalready use, without adding an always-on machine or an egress bill that grows\nwith every packfile download.",[355,356,358],"h3",{"id":357},"preparing-a-bare-repository","Preparing a bare repository",[10,360,361],{},"The synchronization script fetches one page of my public GitHub repositories,\nignores forks, and creates a bare clone for each selected repository. If a bare\ncopy already exists, the script updates it instead.",[262,363,368],{"className":364,"code":365,"filename":366,"language":367,"meta":269,"style":269},"language-js shiki shiki-themes github-light github-dark github-dark","const repoPath = path.join(targetDir, `${repo.name.toLowerCase()}.git`);\n\nif (fs.existsSync(repoPath)) {\n  execSync('git fetch --all', {\n    cwd: repoPath,\n    stdio: 'inherit'\n  });\n} else {\n  execSync(`git clone --bare ${repo.clone_url} ${repoPath}`, {\n    stdio: 'inherit'\n  });\n}\n\nexecSync('git update-server-info', {\n  cwd: repoPath,\n  stdio: 'inherit'\n});\n","scripts/getPublicRepos.mjs","js",[212,369,370,423,430,445,460,466,475,481,493,521,528,533,539,544,557,563,571],{"__ignoreMap":269},[371,372,375,379,383,386,390,394,397,401,404,406,409,411,414,417,420],"span",{"class":373,"line":374},"line",1,[371,376,378],{"class":377},"so5gQ","const",[371,380,382],{"class":381},"suiK_"," repoPath",[371,384,385],{"class":377}," =",[371,387,389],{"class":388},"slsVL"," path.",[371,391,393],{"class":392},"shcOC","join",[371,395,396],{"class":388},"(targetDir, ",[371,398,400],{"class":399},"sfrk1","`${",[371,402,403],{"class":388},"repo",[371,405,332],{"class":399},[371,407,408],{"class":388},"name",[371,410,332],{"class":399},[371,412,413],{"class":392},"toLowerCase",[371,415,416],{"class":399},"()",[371,418,419],{"class":399},"}.git`",[371,421,422],{"class":388},");\n",[371,424,426],{"class":373,"line":425},2,[371,427,429],{"emptyLinePlaceholder":428},true,"\n",[371,431,433,436,439,442],{"class":373,"line":432},3,[371,434,435],{"class":377},"if",[371,437,438],{"class":388}," (fs.",[371,440,441],{"class":392},"existsSync",[371,443,444],{"class":388},"(repoPath)) {\n",[371,446,448,451,454,457],{"class":373,"line":447},4,[371,449,450],{"class":392},"  execSync",[371,452,453],{"class":388},"(",[371,455,456],{"class":399},"'git fetch --all'",[371,458,459],{"class":388},", {\n",[371,461,463],{"class":373,"line":462},5,[371,464,465],{"class":388},"    cwd: repoPath,\n",[371,467,469,472],{"class":373,"line":468},6,[371,470,471],{"class":388},"    stdio: ",[371,473,474],{"class":399},"'inherit'\n",[371,476,478],{"class":373,"line":477},7,[371,479,480],{"class":388},"  });\n",[371,482,484,487,490],{"class":373,"line":483},8,[371,485,486],{"class":388},"} ",[371,488,489],{"class":377},"else",[371,491,492],{"class":388}," {\n",[371,494,496,498,500,503,505,507,510,513,516,519],{"class":373,"line":495},9,[371,497,450],{"class":392},[371,499,453],{"class":388},[371,501,502],{"class":399},"`git clone --bare ${",[371,504,403],{"class":388},[371,506,332],{"class":399},[371,508,509],{"class":388},"clone_url",[371,511,512],{"class":399},"} ${",[371,514,515],{"class":388},"repoPath",[371,517,518],{"class":399},"}`",[371,520,459],{"class":388},[371,522,524,526],{"class":373,"line":523},10,[371,525,471],{"class":388},[371,527,474],{"class":399},[371,529,531],{"class":373,"line":530},11,[371,532,480],{"class":388},[371,534,536],{"class":373,"line":535},12,[371,537,538],{"class":388},"}\n",[371,540,542],{"class":373,"line":541},13,[371,543,429],{"emptyLinePlaceholder":428},[371,545,547,550,552,555],{"class":373,"line":546},14,[371,548,549],{"class":392},"execSync",[371,551,453],{"class":388},[371,553,554],{"class":399},"'git update-server-info'",[371,556,459],{"class":388},[371,558,560],{"class":373,"line":559},15,[371,561,562],{"class":388},"  cwd: repoPath,\n",[371,564,566,569],{"class":373,"line":565},16,[371,567,568],{"class":388},"  stdio: ",[371,570,474],{"class":399},[371,572,574],{"class":373,"line":573},17,[371,575,576],{"class":388},"});\n",[10,578,579],{},"A bare clone contains Git's database without a checked-out working tree. That\nis exactly what this viewer needs because files in Git are already stored as\nobjects; creating a second copy on disk would add space without adding useful\ninformation.",[10,581,582,583,590],{},"The important command is\n",[27,584,587],{"href":585,"rel":586},"https://git-scm.com/docs/git-update-server-info.html",[31],[212,588,589],{},"git update-server-info",".\nIt generates the auxiliary files that a dumb client needs to discover refs and\navailable packs. Without it, object storage would contain the repository, but a\nbrowser wouldn't know which objects exist or where to find them.",[95,592,593],{},[10,594,595],{},"The publishing stage does the only work that requires the native Git CLI. Once\nthe bare repository reaches storage, the production Git transport only needs\nordinary HTTP reads.",[355,597,599],{"id":598},"automating-synchronization","Automating synchronization",[10,601,602],{},"Manually rebuilding every repository after each push would produce a convincing\ndemo that went stale immediately. I connected a GitHub App webhook to a Nitro\nroute so a repository event can dispatch the sync workflow.",[10,604,605,606,609],{},"The route reads the raw request body, calculates an HMAC-SHA-256 signature with\nthe configured webhook secret, and compares it with the\n",[212,607,608],{},"x-hub-signature-256"," header. Private repositories are ignored before the route\ndispatches the workflow.",[262,611,616],{"className":612,"code":613,"filename":614,"language":615,"meta":269,"style":269},"language-ts shiki shiki-themes github-light github-dark github-dark","const signatureBuffer = await crypto.subtle.sign(\n  'HMAC',\n  key,\n  encoder.encode(payload)\n);\n\nconst expectedSignature = `sha256=${hashHex}`;\n\nif (signature !== expectedSignature) {\n  setResponseStatus(event, 401, 'Unauthorized');\n  return;\n}\n\nif (pushPayload.repository.private) {\n  return { status: 'ignored', reason: 'private repo' };\n}\n","server/routes/webhooks/repos-sync.post.ts","ts",[212,617,618,639,647,652,663,667,671,691,695,708,726,733,737,741,748,767],{"__ignoreMap":269},[371,619,620,622,625,627,630,633,636],{"class":373,"line":374},[371,621,378],{"class":377},[371,623,624],{"class":381}," signatureBuffer",[371,626,385],{"class":377},[371,628,629],{"class":377}," await",[371,631,632],{"class":388}," crypto.subtle.",[371,634,635],{"class":392},"sign",[371,637,638],{"class":388},"(\n",[371,640,641,644],{"class":373,"line":425},[371,642,643],{"class":399},"  'HMAC'",[371,645,646],{"class":388},",\n",[371,648,649],{"class":373,"line":432},[371,650,651],{"class":388},"  key,\n",[371,653,654,657,660],{"class":373,"line":447},[371,655,656],{"class":388},"  encoder.",[371,658,659],{"class":392},"encode",[371,661,662],{"class":388},"(payload)\n",[371,664,665],{"class":373,"line":462},[371,666,422],{"class":388},[371,668,669],{"class":373,"line":468},[371,670,429],{"emptyLinePlaceholder":428},[371,672,673,675,678,680,683,686,688],{"class":373,"line":477},[371,674,378],{"class":377},[371,676,677],{"class":381}," expectedSignature",[371,679,385],{"class":377},[371,681,682],{"class":399}," `sha256=${",[371,684,685],{"class":388},"hashHex",[371,687,518],{"class":399},[371,689,690],{"class":388},";\n",[371,692,693],{"class":373,"line":483},[371,694,429],{"emptyLinePlaceholder":428},[371,696,697,699,702,705],{"class":373,"line":495},[371,698,435],{"class":377},[371,700,701],{"class":388}," (signature ",[371,703,704],{"class":377},"!==",[371,706,707],{"class":388}," expectedSignature) {\n",[371,709,710,713,716,719,721,724],{"class":373,"line":523},[371,711,712],{"class":392},"  setResponseStatus",[371,714,715],{"class":388},"(event, ",[371,717,718],{"class":381},"401",[371,720,236],{"class":388},[371,722,723],{"class":399},"'Unauthorized'",[371,725,422],{"class":388},[371,727,728,731],{"class":373,"line":530},[371,729,730],{"class":377},"  return",[371,732,690],{"class":388},[371,734,735],{"class":373,"line":535},[371,736,538],{"class":388},[371,738,739],{"class":373,"line":541},[371,740,429],{"emptyLinePlaceholder":428},[371,742,743,745],{"class":373,"line":546},[371,744,435],{"class":377},[371,746,747],{"class":388}," (pushPayload.repository.private) {\n",[371,749,750,752,755,758,761,764],{"class":373,"line":559},[371,751,730],{"class":377},[371,753,754],{"class":388}," { status: ",[371,756,757],{"class":399},"'ignored'",[371,759,760],{"class":388},", reason: ",[371,762,763],{"class":399},"'private repo'",[371,765,766],{"class":388}," };\n",[371,768,769],{"class":373,"line":565},[371,770,538],{"class":388},[10,772,773,774,777],{},"The GitHub Actions workflow runs the same synchronization script and uploads\nthe resulting bare repository with ",[212,775,776],{},"rclone",":",[262,779,784],{"className":780,"code":781,"filename":782,"language":783,"meta":269,"style":269},"language-yaml shiki shiki-themes github-light github-dark github-dark","- name: Clone or update repositories\n  env:\n    SYNC_REPO: ${{ github.event.inputs.repo }}\n  run: node ./scripts/getPublicRepos.mjs\n\n- name: Upload to R2\n  run: |\n    for repo_dir in repos/*; do\n      if [ -d \"$repo_dir\" ]; then\n        repo_name=$(basename \"$repo_dir\")\n        rclone copy \"$repo_dir/\" \\\n          \"r2:${CLOUDFLARE_R2_BUCKET}/repos/${repo_name}\" \\\n          --no-traverse\n      fi\n    done\n",".github/workflows/sync-repos.yml","yaml",[212,785,786,800,808,818,828,832,843,852,857,862,867,872,877,882,887],{"__ignoreMap":269},[371,787,788,791,794,797],{"class":373,"line":374},[371,789,790],{"class":388},"- ",[371,792,408],{"class":793},"sByVh",[371,795,796],{"class":388},": ",[371,798,799],{"class":399},"Clone or update repositories\n",[371,801,802,805],{"class":373,"line":425},[371,803,804],{"class":793},"  env",[371,806,807],{"class":388},":\n",[371,809,810,813,815],{"class":373,"line":432},[371,811,812],{"class":793},"    SYNC_REPO",[371,814,796],{"class":388},[371,816,817],{"class":399},"${{ github.event.inputs.repo }}\n",[371,819,820,823,825],{"class":373,"line":447},[371,821,822],{"class":793},"  run",[371,824,796],{"class":388},[371,826,827],{"class":399},"node ./scripts/getPublicRepos.mjs\n",[371,829,830],{"class":373,"line":462},[371,831,429],{"emptyLinePlaceholder":428},[371,833,834,836,838,840],{"class":373,"line":468},[371,835,790],{"class":388},[371,837,408],{"class":793},[371,839,796],{"class":388},[371,841,842],{"class":399},"Upload to R2\n",[371,844,845,847,849],{"class":373,"line":477},[371,846,822],{"class":793},[371,848,796],{"class":388},[371,850,851],{"class":377},"|\n",[371,853,854],{"class":373,"line":483},[371,855,856],{"class":399},"    for repo_dir in repos/*; do\n",[371,858,859],{"class":373,"line":495},[371,860,861],{"class":399},"      if [ -d \"$repo_dir\" ]; then\n",[371,863,864],{"class":373,"line":523},[371,865,866],{"class":399},"        repo_name=$(basename \"$repo_dir\")\n",[371,868,869],{"class":373,"line":530},[371,870,871],{"class":399},"        rclone copy \"$repo_dir/\" \\\n",[371,873,874],{"class":373,"line":535},[371,875,876],{"class":399},"          \"r2:${CLOUDFLARE_R2_BUCKET}/repos/${repo_name}\" \\\n",[371,878,879],{"class":373,"line":541},[371,880,881],{"class":399},"          --no-traverse\n",[371,883,884],{"class":373,"line":546},[371,885,886],{"class":399},"      fi\n",[371,888,889],{"class":373,"line":559},[371,890,891],{"class":399},"    done\n",[10,893,894,895,898],{},"The script can update a bare copy when its directory already exists, which is\nuseful locally or in a persistent runner. The current GitHub Actions job does\nnot persist ",[212,896,897],{},"repos/",", however, so it ordinarily creates a fresh bare clone on\neach run. That detail becomes important when I discuss pack updates in the\nbrowser.",[10,900,901],{},"The workflow does not deploy a new application build. It can update the Git\ndata behind an existing repository route independently from the portfolio. A\nbrand-new repository still needs a matching project record and a Nuxt rebuild\nbefore the catch-all page will expose it.",[10,903,904,905,910,911,914,915,215,917,332],{},"Publishing is not atomic. R2 is\n",[27,906,909],{"href":907,"rel":908},"https://developers.cloudflare.com/r2/reference/consistency/",[31],"strongly consistent per object",",\nbut a repository generation contains many objects, and ",[212,912,913],{},"rclone copy"," writes\nthem directly into the live prefix. A reader can observe new discovery metadata\nbefore its pack finishes uploading, while overlapping workflow runs can race on\nmutable files such as ",[212,916,338],{},[212,918,341],{},[59,920,922],{"id":921},"keeping-the-proxy-deliberately-boring","Keeping the proxy deliberately boring",[10,924,925],{},"The browser cannot access the private R2 binding directly, and local\nself-hosting needs the same public URL shape. A catch-all Nitro endpoint gives\nboth storage modes one interface:",[262,927,931],{"className":928,"code":929,"filename":930,"language":268,"meta":269},[265],"/api/git-proxy/portfolio.git/HEAD\n/api/git-proxy/portfolio.git/info/refs\n/api/git-proxy/portfolio.git/objects/info/packs\n/api/git-proxy/portfolio.git/objects/pack/pack-\u003Chash>.idx\n/api/git-proxy/portfolio.git/objects/pack/pack-\u003Chash>.pack\n","Repository transport URLs",[212,932,929],{"__ignoreMap":269},[10,934,935,936,939,940,943,944,947],{},"In production, the route maps that path to ",[212,937,938],{},"repos/\u003Cpath>"," in R2. Without an R2\nbinding, it resolves the same path beneath ",[212,941,942],{},"NUXT_REPOS_DIR",", falling back to a\nlocal ",[212,945,946],{},"repos"," directory.",[262,949,952],{"className":612,"code":950,"filename":951,"language":615,"meta":269,"style":269},"const bucket = event.context.cloudflare?.env?.R2_BUCKET;\n\nif (!bucket) {\n  const baseDir = config.reposDir\n    ? path.resolve(config.reposDir)\n    : path.resolve(process.cwd(), 'repos');\n\n  const localPath = path.resolve(baseDir, filePath);\n\n  if (!localPath.startsWith(baseDir)) {\n    throw createError({ statusCode: 403, message: 'Forbidden' });\n  }\n\n  return sendStream(event, fs.createReadStream(localPath));\n}\n\nconst object = await bucket.get(`repos/${filePath}`);\n\nif (!object) {\n  throw createError({ statusCode: 404, message: 'Not Found' });\n}\n\nreturn sendStream(event, object.body);\n","server/api/git-proxy/[...path].ts",[212,953,954,971,975,988,1001,1014,1037,1041,1057,1061,1079,1102,1107,1111,1127,1131,1135,1164,1169,1181,1201,1206,1211],{"__ignoreMap":269},[371,955,956,958,961,963,966,969],{"class":373,"line":374},[371,957,378],{"class":377},[371,959,960],{"class":381}," bucket",[371,962,385],{"class":377},[371,964,965],{"class":388}," event.context.cloudflare?.env?.",[371,967,968],{"class":381},"R2_BUCKET",[371,970,690],{"class":388},[371,972,973],{"class":373,"line":425},[371,974,429],{"emptyLinePlaceholder":428},[371,976,977,979,982,985],{"class":373,"line":432},[371,978,435],{"class":377},[371,980,981],{"class":388}," (",[371,983,984],{"class":377},"!",[371,986,987],{"class":388},"bucket) {\n",[371,989,990,993,996,998],{"class":373,"line":447},[371,991,992],{"class":377},"  const",[371,994,995],{"class":381}," baseDir",[371,997,385],{"class":377},[371,999,1000],{"class":388}," config.reposDir\n",[371,1002,1003,1006,1008,1011],{"class":373,"line":462},[371,1004,1005],{"class":377},"    ?",[371,1007,389],{"class":388},[371,1009,1010],{"class":392},"resolve",[371,1012,1013],{"class":388},"(config.reposDir)\n",[371,1015,1016,1019,1021,1023,1026,1029,1032,1035],{"class":373,"line":468},[371,1017,1018],{"class":377},"    :",[371,1020,389],{"class":388},[371,1022,1010],{"class":392},[371,1024,1025],{"class":388},"(process.",[371,1027,1028],{"class":392},"cwd",[371,1030,1031],{"class":388},"(), ",[371,1033,1034],{"class":399},"'repos'",[371,1036,422],{"class":388},[371,1038,1039],{"class":373,"line":477},[371,1040,429],{"emptyLinePlaceholder":428},[371,1042,1043,1045,1048,1050,1052,1054],{"class":373,"line":483},[371,1044,992],{"class":377},[371,1046,1047],{"class":381}," localPath",[371,1049,385],{"class":377},[371,1051,389],{"class":388},[371,1053,1010],{"class":392},[371,1055,1056],{"class":388},"(baseDir, filePath);\n",[371,1058,1059],{"class":373,"line":495},[371,1060,429],{"emptyLinePlaceholder":428},[371,1062,1063,1066,1068,1070,1073,1076],{"class":373,"line":523},[371,1064,1065],{"class":377},"  if",[371,1067,981],{"class":388},[371,1069,984],{"class":377},[371,1071,1072],{"class":388},"localPath.",[371,1074,1075],{"class":392},"startsWith",[371,1077,1078],{"class":388},"(baseDir)) {\n",[371,1080,1081,1084,1087,1090,1093,1096,1099],{"class":373,"line":530},[371,1082,1083],{"class":377},"    throw",[371,1085,1086],{"class":392}," createError",[371,1088,1089],{"class":388},"({ statusCode: ",[371,1091,1092],{"class":381},"403",[371,1094,1095],{"class":388},", message: ",[371,1097,1098],{"class":399},"'Forbidden'",[371,1100,1101],{"class":388}," });\n",[371,1103,1104],{"class":373,"line":535},[371,1105,1106],{"class":388},"  }\n",[371,1108,1109],{"class":373,"line":541},[371,1110,429],{"emptyLinePlaceholder":428},[371,1112,1113,1115,1118,1121,1124],{"class":373,"line":546},[371,1114,730],{"class":377},[371,1116,1117],{"class":392}," sendStream",[371,1119,1120],{"class":388},"(event, fs.",[371,1122,1123],{"class":392},"createReadStream",[371,1125,1126],{"class":388},"(localPath));\n",[371,1128,1129],{"class":373,"line":559},[371,1130,538],{"class":388},[371,1132,1133],{"class":373,"line":565},[371,1134,429],{"emptyLinePlaceholder":428},[371,1136,1137,1139,1142,1144,1146,1149,1152,1154,1157,1160,1162],{"class":373,"line":573},[371,1138,378],{"class":377},[371,1140,1141],{"class":381}," object",[371,1143,385],{"class":377},[371,1145,629],{"class":377},[371,1147,1148],{"class":388}," bucket.",[371,1150,1151],{"class":392},"get",[371,1153,453],{"class":388},[371,1155,1156],{"class":399},"`repos/${",[371,1158,1159],{"class":388},"filePath",[371,1161,518],{"class":399},[371,1163,422],{"class":388},[371,1165,1167],{"class":373,"line":1166},18,[371,1168,429],{"emptyLinePlaceholder":428},[371,1170,1172,1174,1176,1178],{"class":373,"line":1171},19,[371,1173,435],{"class":377},[371,1175,981],{"class":388},[371,1177,984],{"class":377},[371,1179,1180],{"class":388},"object) {\n",[371,1182,1184,1187,1189,1191,1194,1196,1199],{"class":373,"line":1183},20,[371,1185,1186],{"class":377},"  throw",[371,1188,1086],{"class":392},[371,1190,1089],{"class":388},[371,1192,1193],{"class":381},"404",[371,1195,1095],{"class":388},[371,1197,1198],{"class":399},"'Not Found'",[371,1200,1101],{"class":388},[371,1202,1204],{"class":373,"line":1203},21,[371,1205,538],{"class":388},[371,1207,1209],{"class":373,"line":1208},22,[371,1210,429],{"emptyLinePlaceholder":428},[371,1212,1214,1217,1219],{"class":373,"line":1213},23,[371,1215,1216],{"class":377},"return",[371,1218,1117],{"class":392},[371,1220,1221],{"class":388},"(event, object.body);\n",[10,1223,1224],{},"The production R2 branch also applies different cache policies to mutable\ndiscovery files and immutable objects. Packfiles, indexes, reverse indexes, and\nloose objects can be cached for a year. References get a short lifetime with\nstale revalidation. The local-filesystem branch currently returns before those\nheaders are applied.",[262,1226,1228],{"className":612,"code":1227,"filename":951,"language":615,"meta":269,"style":269},"if (isPackFile || isLooseObject) {\n  setResponseHeader(\n    event,\n    'cache-control',\n    'public, max-age=31536000, immutable'\n  );\n} else {\n  setResponseHeader(\n    event,\n    'cache-control',\n    'public, max-age=60, stale-while-revalidate=86400'\n  );\n}\n",[212,1229,1230,1243,1250,1255,1262,1267,1272,1280,1286,1290,1296,1301,1305],{"__ignoreMap":269},[371,1231,1232,1234,1237,1240],{"class":373,"line":374},[371,1233,435],{"class":377},[371,1235,1236],{"class":388}," (isPackFile ",[371,1238,1239],{"class":377},"||",[371,1241,1242],{"class":388}," isLooseObject) {\n",[371,1244,1245,1248],{"class":373,"line":425},[371,1246,1247],{"class":392},"  setResponseHeader",[371,1249,638],{"class":388},[371,1251,1252],{"class":373,"line":432},[371,1253,1254],{"class":388},"    event,\n",[371,1256,1257,1260],{"class":373,"line":447},[371,1258,1259],{"class":399},"    'cache-control'",[371,1261,646],{"class":388},[371,1263,1264],{"class":373,"line":462},[371,1265,1266],{"class":399},"    'public, max-age=31536000, immutable'\n",[371,1268,1269],{"class":373,"line":468},[371,1270,1271],{"class":388},"  );\n",[371,1273,1274,1276,1278],{"class":373,"line":477},[371,1275,486],{"class":388},[371,1277,489],{"class":377},[371,1279,492],{"class":388},[371,1281,1282,1284],{"class":373,"line":483},[371,1283,1247],{"class":392},[371,1285,638],{"class":388},[371,1287,1288],{"class":373,"line":495},[371,1289,1254],{"class":388},[371,1291,1292,1294],{"class":373,"line":523},[371,1293,1259],{"class":399},[371,1295,646],{"class":388},[371,1297,1298],{"class":373,"line":530},[371,1299,1300],{"class":399},"    'public, max-age=60, stale-while-revalidate=86400'\n",[371,1302,1303],{"class":373,"line":535},[371,1304,1271],{"class":388},[371,1306,1307],{"class":373,"line":541},[371,1308,538],{"class":388},[10,1310,1311],{},"This works because a Git object's identity comes from its contents. Existing\nobjects don't change when a branch advances; the ref starts pointing at a new\ncommit, and new packfiles appear. Treating the two categories differently\nlets HTTP caches reuse an exact immutable object or pack URL while the viewer\ncontinues to revalidate mutable discovery files.",[10,1313,1314],{},"These are HTTP cache instructions, not a guarantee that R2 binding reads enter\nCloudflare's CDN cache. The current route lets browsers and any participating\nintermediaries reuse responses. Explicit edge caching would require another\nlayer, such as Cloudflare's Cache API.",[10,1316,1317],{},"The cleanest proof that this endpoint serves Git rather than viewer-specific\nJSON is that native Git can use the same URL:",[262,1319,1324],{"className":1320,"code":1321,"filename":1322,"language":1323,"meta":269,"style":269},"language-bash shiki shiki-themes github-light github-dark github-dark","git clone https://riavzon.com/api/git-proxy/portfolio.git\n","Terminal","bash",[212,1325,1326],{"__ignoreMap":269},[371,1327,1328,1331,1334],{"class":373,"line":374},[371,1329,1330],{"class":392},"git",[371,1332,1333],{"class":399}," clone",[371,1335,1336],{"class":399}," https://riavzon.com/api/git-proxy/portfolio.git\n",[10,1338,1339,1340,1342],{},"The browser takes a narrower route through the same published repository. It\ndownloads the advertised packs itself because its goal is to rebuild a local\n",[212,1341,312],{}," directory in IndexedDB, not negotiate a new pack with the server.",[59,1344,1346],{"id":1345},"reconstructing-git-inside-the-browser","Reconstructing Git inside the browser",[10,1348,1349,1350,1353,1354,332],{},"This was the point where the design stopped being a storage experiment and had\nto become an actual Git client. The browser could fetch a bare repository, but\n",[212,1351,1352],{},"isomorphic-git"," expected a filesystem-shaped repository, and browsers do not\nhave Node's ",[212,1355,1356],{},"fs",[10,1358,1359,1360,1367],{},"I use\n",[27,1361,1364],{"href":1362,"rel":1363},"https://github.com/isomorphic-git/lightning-fs",[31],[212,1365,1366],{},"LightningFS",", which exposes a\nfamiliar filesystem API backed by IndexedDB.",[10,1369,1370],{},"Every repository receives its own persistent filesystem name and internal\ndirectory:",[262,1372,1375],{"className":612,"code":1373,"filename":1374,"language":615,"meta":269,"style":269},"const fsName = `portfolio-${repoName}`;\nconst dir = `/${repoName}`;\nconst repoUrl = import.meta.client\n  ? `${window.location.origin}/api/git-proxy/${repoName}.git`\n  : '';\n","app/composables/repo/createRepo.ts",[212,1376,1377,1396,1414,1434,1463],{"__ignoreMap":269},[371,1378,1379,1381,1384,1386,1389,1392,1394],{"class":373,"line":374},[371,1380,378],{"class":377},[371,1382,1383],{"class":381}," fsName",[371,1385,385],{"class":377},[371,1387,1388],{"class":399}," `portfolio-${",[371,1390,1391],{"class":388},"repoName",[371,1393,518],{"class":399},[371,1395,690],{"class":388},[371,1397,1398,1400,1403,1405,1408,1410,1412],{"class":373,"line":425},[371,1399,378],{"class":377},[371,1401,1402],{"class":381}," dir",[371,1404,385],{"class":377},[371,1406,1407],{"class":399}," `/${",[371,1409,1391],{"class":388},[371,1411,518],{"class":399},[371,1413,690],{"class":388},[371,1415,1416,1418,1421,1423,1426,1428,1431],{"class":373,"line":432},[371,1417,378],{"class":377},[371,1419,1420],{"class":381}," repoUrl",[371,1422,385],{"class":377},[371,1424,1425],{"class":377}," import",[371,1427,332],{"class":388},[371,1429,1430],{"class":381},"meta",[371,1432,1433],{"class":388},".client\n",[371,1435,1436,1439,1442,1445,1447,1450,1452,1455,1458,1460],{"class":373,"line":447},[371,1437,1438],{"class":377},"  ?",[371,1440,1441],{"class":399}," `${",[371,1443,1444],{"class":388},"window",[371,1446,332],{"class":399},[371,1448,1449],{"class":388},"location",[371,1451,332],{"class":399},[371,1453,1454],{"class":388},"origin",[371,1456,1457],{"class":399},"}/api/git-proxy/${",[371,1459,1391],{"class":388},[371,1461,1462],{"class":399},"}.git`\n",[371,1464,1465,1468,1471],{"class":373,"line":462},[371,1466,1467],{"class":377},"  :",[371,1469,1470],{"class":399}," ''",[371,1472,690],{"class":388},[10,1474,1475,1476,1478,1479,1482],{},"The first visit creates a ",[212,1477,312],{}," directory in that virtual filesystem. The\nclient fetches ",[212,1480,1481],{},"HEAD",", configuration, packed refs, ref metadata, and the pack\nmanifest before downloading each index and pack.",[262,1484,1487],{"className":612,"code":1485,"filename":1486,"language":615,"meta":269,"style":269},"const packsRaw = await fetchGitFile(repoUrl, 'objects/info/packs');\nawait pfs.writeFile(`${dir}/.git/objects/info/packs`, packsRaw.data);\n\nconst packsText = new TextDecoder().decode(packsRaw.data);\n\nfor (const line of packsText.split('\\n')) {\n  if (!line.startsWith('P ')) continue;\n\n  const packName = line.substring(2).trim();\n  const idxName = packName.replace('.pack', '.idx');\n\n  const idxData = await fetchGitFile(\n    repoUrl,\n    `objects/pack/${idxName}`\n  );\n  if (!idxData.ok) {\n    return cleanupAndReturn(idxData.reason);\n  }\n\n  const packData = await fetchGitFile(\n    repoUrl,\n    `objects/pack/${packName}`\n  );\n  if (!packData.ok) {\n    return cleanupAndReturn(packData.reason);\n  }\n\n  await pfs.writeFile(\n    `${dir}/.git/objects/pack/${idxName}`,\n    idxData.data\n  );\n  await pfs.writeFile(\n    `${dir}/.git/objects/pack/${packName}`,\n    packData.data\n  );\n}\n","app/utils/useFs.ts",[212,1488,1489,1511,1535,1539,1563,1567,1601,1627,1631,1660,1687,1691,1706,1711,1722,1726,1737,1748,1752,1756,1771,1775,1784,1788,1800,1810,1815,1820,1832,1849,1855,1860,1871,1886,1892,1897],{"__ignoreMap":269},[371,1490,1491,1493,1496,1498,1500,1503,1506,1509],{"class":373,"line":374},[371,1492,378],{"class":377},[371,1494,1495],{"class":381}," packsRaw",[371,1497,385],{"class":377},[371,1499,629],{"class":377},[371,1501,1502],{"class":392}," fetchGitFile",[371,1504,1505],{"class":388},"(repoUrl, ",[371,1507,1508],{"class":399},"'objects/info/packs'",[371,1510,422],{"class":388},[371,1512,1513,1516,1519,1522,1524,1526,1529,1532],{"class":373,"line":425},[371,1514,1515],{"class":377},"await",[371,1517,1518],{"class":388}," pfs.",[371,1520,1521],{"class":392},"writeFile",[371,1523,453],{"class":388},[371,1525,400],{"class":399},[371,1527,1528],{"class":388},"dir",[371,1530,1531],{"class":399},"}/.git/objects/info/packs`",[371,1533,1534],{"class":388},", packsRaw.data);\n",[371,1536,1537],{"class":373,"line":432},[371,1538,429],{"emptyLinePlaceholder":428},[371,1540,1541,1543,1546,1548,1551,1554,1557,1560],{"class":373,"line":447},[371,1542,378],{"class":377},[371,1544,1545],{"class":381}," packsText",[371,1547,385],{"class":377},[371,1549,1550],{"class":377}," new",[371,1552,1553],{"class":392}," TextDecoder",[371,1555,1556],{"class":388},"().",[371,1558,1559],{"class":392},"decode",[371,1561,1562],{"class":388},"(packsRaw.data);\n",[371,1564,1565],{"class":373,"line":462},[371,1566,429],{"emptyLinePlaceholder":428},[371,1568,1569,1572,1574,1576,1579,1582,1585,1588,1590,1593,1596,1598],{"class":373,"line":468},[371,1570,1571],{"class":377},"for",[371,1573,981],{"class":388},[371,1575,378],{"class":377},[371,1577,1578],{"class":381}," line",[371,1580,1581],{"class":377}," of",[371,1583,1584],{"class":388}," packsText.",[371,1586,1587],{"class":392},"split",[371,1589,453],{"class":388},[371,1591,1592],{"class":399},"'",[371,1594,1595],{"class":381},"\\n",[371,1597,1592],{"class":399},[371,1599,1600],{"class":388},")) {\n",[371,1602,1603,1605,1607,1609,1612,1614,1616,1619,1622,1625],{"class":373,"line":477},[371,1604,1065],{"class":377},[371,1606,981],{"class":388},[371,1608,984],{"class":377},[371,1610,1611],{"class":388},"line.",[371,1613,1075],{"class":392},[371,1615,453],{"class":388},[371,1617,1618],{"class":399},"'P '",[371,1620,1621],{"class":388},")) ",[371,1623,1624],{"class":377},"continue",[371,1626,690],{"class":388},[371,1628,1629],{"class":373,"line":483},[371,1630,429],{"emptyLinePlaceholder":428},[371,1632,1633,1635,1638,1640,1643,1646,1648,1651,1654,1657],{"class":373,"line":495},[371,1634,992],{"class":377},[371,1636,1637],{"class":381}," packName",[371,1639,385],{"class":377},[371,1641,1642],{"class":388}," line.",[371,1644,1645],{"class":392},"substring",[371,1647,453],{"class":388},[371,1649,1650],{"class":381},"2",[371,1652,1653],{"class":388},").",[371,1655,1656],{"class":392},"trim",[371,1658,1659],{"class":388},"();\n",[371,1661,1662,1664,1667,1669,1672,1675,1677,1680,1682,1685],{"class":373,"line":523},[371,1663,992],{"class":377},[371,1665,1666],{"class":381}," idxName",[371,1668,385],{"class":377},[371,1670,1671],{"class":388}," packName.",[371,1673,1674],{"class":392},"replace",[371,1676,453],{"class":388},[371,1678,1679],{"class":399},"'.pack'",[371,1681,236],{"class":388},[371,1683,1684],{"class":399},"'.idx'",[371,1686,422],{"class":388},[371,1688,1689],{"class":373,"line":530},[371,1690,429],{"emptyLinePlaceholder":428},[371,1692,1693,1695,1698,1700,1702,1704],{"class":373,"line":535},[371,1694,992],{"class":377},[371,1696,1697],{"class":381}," idxData",[371,1699,385],{"class":377},[371,1701,629],{"class":377},[371,1703,1502],{"class":392},[371,1705,638],{"class":388},[371,1707,1708],{"class":373,"line":541},[371,1709,1710],{"class":388},"    repoUrl,\n",[371,1712,1713,1716,1719],{"class":373,"line":546},[371,1714,1715],{"class":399},"    `objects/pack/${",[371,1717,1718],{"class":388},"idxName",[371,1720,1721],{"class":399},"}`\n",[371,1723,1724],{"class":373,"line":559},[371,1725,1271],{"class":388},[371,1727,1728,1730,1732,1734],{"class":373,"line":565},[371,1729,1065],{"class":377},[371,1731,981],{"class":388},[371,1733,984],{"class":377},[371,1735,1736],{"class":388},"idxData.ok) {\n",[371,1738,1739,1742,1745],{"class":373,"line":573},[371,1740,1741],{"class":377},"    return",[371,1743,1744],{"class":392}," cleanupAndReturn",[371,1746,1747],{"class":388},"(idxData.reason);\n",[371,1749,1750],{"class":373,"line":1166},[371,1751,1106],{"class":388},[371,1753,1754],{"class":373,"line":1171},[371,1755,429],{"emptyLinePlaceholder":428},[371,1757,1758,1760,1763,1765,1767,1769],{"class":373,"line":1183},[371,1759,992],{"class":377},[371,1761,1762],{"class":381}," packData",[371,1764,385],{"class":377},[371,1766,629],{"class":377},[371,1768,1502],{"class":392},[371,1770,638],{"class":388},[371,1772,1773],{"class":373,"line":1203},[371,1774,1710],{"class":388},[371,1776,1777,1779,1782],{"class":373,"line":1208},[371,1778,1715],{"class":399},[371,1780,1781],{"class":388},"packName",[371,1783,1721],{"class":399},[371,1785,1786],{"class":373,"line":1213},[371,1787,1271],{"class":388},[371,1789,1791,1793,1795,1797],{"class":373,"line":1790},24,[371,1792,1065],{"class":377},[371,1794,981],{"class":388},[371,1796,984],{"class":377},[371,1798,1799],{"class":388},"packData.ok) {\n",[371,1801,1803,1805,1807],{"class":373,"line":1802},25,[371,1804,1741],{"class":377},[371,1806,1744],{"class":392},[371,1808,1809],{"class":388},"(packData.reason);\n",[371,1811,1813],{"class":373,"line":1812},26,[371,1814,1106],{"class":388},[371,1816,1818],{"class":373,"line":1817},27,[371,1819,429],{"emptyLinePlaceholder":428},[371,1821,1823,1826,1828,1830],{"class":373,"line":1822},28,[371,1824,1825],{"class":377},"  await",[371,1827,1518],{"class":388},[371,1829,1521],{"class":392},[371,1831,638],{"class":388},[371,1833,1835,1838,1840,1843,1845,1847],{"class":373,"line":1834},29,[371,1836,1837],{"class":399},"    `${",[371,1839,1528],{"class":388},[371,1841,1842],{"class":399},"}/.git/objects/pack/${",[371,1844,1718],{"class":388},[371,1846,518],{"class":399},[371,1848,646],{"class":388},[371,1850,1852],{"class":373,"line":1851},30,[371,1853,1854],{"class":388},"    idxData.data\n",[371,1856,1858],{"class":373,"line":1857},31,[371,1859,1271],{"class":388},[371,1861,1863,1865,1867,1869],{"class":373,"line":1862},32,[371,1864,1825],{"class":377},[371,1866,1518],{"class":388},[371,1868,1521],{"class":392},[371,1870,638],{"class":388},[371,1872,1874,1876,1878,1880,1882,1884],{"class":373,"line":1873},33,[371,1875,1837],{"class":399},[371,1877,1528],{"class":388},[371,1879,1842],{"class":399},[371,1881,1781],{"class":388},[371,1883,518],{"class":399},[371,1885,646],{"class":388},[371,1887,1889],{"class":373,"line":1888},34,[371,1890,1891],{"class":388},"    packData.data\n",[371,1893,1895],{"class":373,"line":1894},35,[371,1896,1271],{"class":388},[371,1898,1900],{"class":373,"line":1899},36,[371,1901,538],{"class":388},[10,1903,1904,1905,1908,1909,1912,1913,1915],{},"The ",[212,1906,1907],{},".idx"," file tells Git which object IDs are stored in a pack and where their\ncompressed data begins. The ",[212,1910,1911],{},".pack"," file contains the objects themselves. Once\nboth exist in IndexedDB, ",[212,1914,1352],{}," can resolve refs and walk the same\ncommit, tree, and blob graph that native Git reads.",[10,1917,1918,1919,1922],{},"The code writes a ",[212,1920,1921],{},".cloned"," marker only after every required file downloads\nsuccessfully. A known fetch failure wipes the partial directory immediately. If\nthe tab closes or another unexpected interruption occurs, the missing marker\ntells the next initialization to wipe the directory before trying again.",[355,1924,1926],{"id":1925},"updating-an-existing-browser-cache","Updating an existing browser cache",[10,1928,1929,1930,1932,1933,1935],{},"Getting the first visit to work was only half the feature. Returning visitors\nalready have the packfiles in IndexedDB, so downloading the whole repository\nagain would defeat the point. The update path refreshes ",[212,1931,1481],{},", refs, and\n",[212,1934,341],{},", then downloads only packfiles that do not exist locally.",[262,1937,1939],{"className":612,"code":1938,"filename":1486,"language":615,"meta":269,"style":269},"const packExists = await pfs\n  .stat(`${dir}/.git/objects/pack/${packName}`)\n  .catch(() => null);\n\nif (!packExists) {\n  const idxName = packName.replace('.pack', '.idx');\n  const idxData = await fetchGitFile(\n    repoUrl,\n    `objects/pack/${idxName}`\n  );\n  const packData = await fetchGitFile(\n    repoUrl,\n    `objects/pack/${packName}`\n  );\n\n  if (idxData.ok) {\n    await pfs.writeFile(\n      `${dir}/.git/objects/pack/${idxName}`,\n      idxData.data\n    );\n  }\n  if (packData.ok) {\n    await pfs.writeFile(\n      `${dir}/.git/objects/pack/${packName}`,\n      packData.data\n    );\n  }\n\n  downloadedSomething = true;\n}\n",[212,1940,1941,1955,1978,1996,2000,2011,2033,2047,2051,2059,2063,2077,2081,2089,2093,2097,2104,2115,2130,2135,2140,2144,2151,2161,2175,2180,2184,2188,2192,2205],{"__ignoreMap":269},[371,1942,1943,1945,1948,1950,1952],{"class":373,"line":374},[371,1944,378],{"class":377},[371,1946,1947],{"class":381}," packExists",[371,1949,385],{"class":377},[371,1951,629],{"class":377},[371,1953,1954],{"class":388}," pfs\n",[371,1956,1957,1960,1963,1965,1967,1969,1971,1973,1975],{"class":373,"line":425},[371,1958,1959],{"class":388},"  .",[371,1961,1962],{"class":392},"stat",[371,1964,453],{"class":388},[371,1966,400],{"class":399},[371,1968,1528],{"class":388},[371,1970,1842],{"class":399},[371,1972,1781],{"class":388},[371,1974,518],{"class":399},[371,1976,1977],{"class":388},")\n",[371,1979,1980,1982,1985,1988,1991,1994],{"class":373,"line":432},[371,1981,1959],{"class":388},[371,1983,1984],{"class":392},"catch",[371,1986,1987],{"class":388},"(() ",[371,1989,1990],{"class":377},"=>",[371,1992,1993],{"class":381}," null",[371,1995,422],{"class":388},[371,1997,1998],{"class":373,"line":447},[371,1999,429],{"emptyLinePlaceholder":428},[371,2001,2002,2004,2006,2008],{"class":373,"line":462},[371,2003,435],{"class":377},[371,2005,981],{"class":388},[371,2007,984],{"class":377},[371,2009,2010],{"class":388},"packExists) {\n",[371,2012,2013,2015,2017,2019,2021,2023,2025,2027,2029,2031],{"class":373,"line":468},[371,2014,992],{"class":377},[371,2016,1666],{"class":381},[371,2018,385],{"class":377},[371,2020,1671],{"class":388},[371,2022,1674],{"class":392},[371,2024,453],{"class":388},[371,2026,1679],{"class":399},[371,2028,236],{"class":388},[371,2030,1684],{"class":399},[371,2032,422],{"class":388},[371,2034,2035,2037,2039,2041,2043,2045],{"class":373,"line":477},[371,2036,992],{"class":377},[371,2038,1697],{"class":381},[371,2040,385],{"class":377},[371,2042,629],{"class":377},[371,2044,1502],{"class":392},[371,2046,638],{"class":388},[371,2048,2049],{"class":373,"line":483},[371,2050,1710],{"class":388},[371,2052,2053,2055,2057],{"class":373,"line":495},[371,2054,1715],{"class":399},[371,2056,1718],{"class":388},[371,2058,1721],{"class":399},[371,2060,2061],{"class":373,"line":523},[371,2062,1271],{"class":388},[371,2064,2065,2067,2069,2071,2073,2075],{"class":373,"line":530},[371,2066,992],{"class":377},[371,2068,1762],{"class":381},[371,2070,385],{"class":377},[371,2072,629],{"class":377},[371,2074,1502],{"class":392},[371,2076,638],{"class":388},[371,2078,2079],{"class":373,"line":535},[371,2080,1710],{"class":388},[371,2082,2083,2085,2087],{"class":373,"line":541},[371,2084,1715],{"class":399},[371,2086,1781],{"class":388},[371,2088,1721],{"class":399},[371,2090,2091],{"class":373,"line":546},[371,2092,1271],{"class":388},[371,2094,2095],{"class":373,"line":559},[371,2096,429],{"emptyLinePlaceholder":428},[371,2098,2099,2101],{"class":373,"line":565},[371,2100,1065],{"class":377},[371,2102,2103],{"class":388}," (idxData.ok) {\n",[371,2105,2106,2109,2111,2113],{"class":373,"line":573},[371,2107,2108],{"class":377},"    await",[371,2110,1518],{"class":388},[371,2112,1521],{"class":392},[371,2114,638],{"class":388},[371,2116,2117,2120,2122,2124,2126,2128],{"class":373,"line":1166},[371,2118,2119],{"class":399},"      `${",[371,2121,1528],{"class":388},[371,2123,1842],{"class":399},[371,2125,1718],{"class":388},[371,2127,518],{"class":399},[371,2129,646],{"class":388},[371,2131,2132],{"class":373,"line":1171},[371,2133,2134],{"class":388},"      idxData.data\n",[371,2136,2137],{"class":373,"line":1183},[371,2138,2139],{"class":388},"    );\n",[371,2141,2142],{"class":373,"line":1203},[371,2143,1106],{"class":388},[371,2145,2146,2148],{"class":373,"line":1208},[371,2147,1065],{"class":377},[371,2149,2150],{"class":388}," (packData.ok) {\n",[371,2152,2153,2155,2157,2159],{"class":373,"line":1213},[371,2154,2108],{"class":377},[371,2156,1518],{"class":388},[371,2158,1521],{"class":392},[371,2160,638],{"class":388},[371,2162,2163,2165,2167,2169,2171,2173],{"class":373,"line":1790},[371,2164,2119],{"class":399},[371,2166,1528],{"class":388},[371,2168,1842],{"class":399},[371,2170,1781],{"class":388},[371,2172,518],{"class":399},[371,2174,646],{"class":388},[371,2176,2177],{"class":373,"line":1802},[371,2178,2179],{"class":388},"      packData.data\n",[371,2181,2182],{"class":373,"line":1812},[371,2183,2139],{"class":388},[371,2185,2186],{"class":373,"line":1817},[371,2187,1106],{"class":388},[371,2189,2190],{"class":373,"line":1822},[371,2191,429],{"emptyLinePlaceholder":428},[371,2193,2194,2197,2200,2203],{"class":373,"line":1834},[371,2195,2196],{"class":388},"  downloadedSomething ",[371,2198,2199],{"class":377},"=",[371,2201,2202],{"class":381}," true",[371,2204,690],{"class":388},[371,2206,2207],{"class":373,"line":1851},[371,2208,538],{"class":388},[10,2210,2211],{},"Pack filenames are derived from pack contents. If the server still advertises a\nname that exists locally, that exact pack does not need to be downloaded again.\nA fresh clone or repack can produce a new pack name containing many objects the\nbrowser already has, so this is pack-level reuse rather than object-level\nincremental fetching.",[10,2213,2214],{},"When the initial metadata request fails, the worker usually continues with the\nexisting IndexedDB copy. The update is not transactional, though: it writes\nrefs and the pack manifest before all missing packs arrive, and individual pack\nfailures are not rolled back. The local fallback is useful on a simple offline\nvisit, but a connection failure halfway through an update can still leave an\nunresolvable ref until the next successful synchronization.",[2216,2217,2218],"warning",{},[10,2219,2220],{},"The current transport downloads complete packfiles. That is simple and fast for\nsmall and medium repositories, but it is not partial clone, shallow clone, or\nobject-on-demand fetching. Repository size still matters.",[59,2222,2224],{"id":2223},"moving-git-off-the-main-thread","Moving Git off the main thread",[10,2226,2227,2228,2230],{},"The first complete version worked, which made it tempting to call it done. It ran ",[212,2229,1352],{}," from a composable on the\nbrowser's main thread. As history, local diffs, blob reads, and tree metadata\naccumulated, all of that work competed with Vue for the thread responsible for\ninput, painting, and transitions.",[10,2232,2233,2234,2237],{},"The original query shape made the problem worse. It ran a separate\n",[212,2235,2236],{},"git.log({ filepath })"," lookup for every visible row and loaded an uncapped\nrepository history. The number of Git walks grew with the number of rows before\nthe tree could render useful metadata.",[10,2239,2240],{},"Nuxt and cURL came later as deliberate stress tests. They did not cause the\nworker migration; they showed how much expensive work still remained after\nmoving Git away from the main thread.",[10,2242,2243,2244,332],{},"Caching removed repeated work, but it could not make the first walk\nnon-blocking. Adding more loading skeletons does not fix a blocked event loop,\nso I moved the repository engine into a dedicated\n",[27,2245,2248],{"href":2246,"rel":2247},"https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers",[31],"Web Worker",[10,2250,2251],{},"The worker owns the virtual filesystem, Git cache, active ref, and most Git\nobject parsing and repository traversal. The Vue application owns reactive\npresentation state, while later stages still perform diff-row shaping in the\nclient and syntax highlighting through a Nitro route.",[262,2253,2256],{"className":612,"code":2254,"filename":2255,"language":615,"meta":269,"style":269},"export class GitWorkerClass {\n  fs: InstanceType\u003Ctypeof LightningFS>;\n  cache: ReturnType\u003Ctypeof useGitRepoCache>;\n  currentBranch: string | undefined;\n  LOG_DEPTH_CAP = 1000;\n\n  constructor(\n    fsName: string,\n    dir: string,\n    repoUrl: string,\n    initialBranch?: string\n  ) {\n    this.fs = new LightningFS(fsName);\n    this.cache = useGitRepoCache(dir, this.fs, this.LOG_DEPTH_CAP);\n    this.currentBranch = initialBranch;\n  }\n\n  // initRepo(), getFileBlob(), getAllCommits(), getCommitDiff(), ...\n}\n\nComlink.expose(GitWorkerClass);\n","app/workers/git.worker.ts",[212,2257,2258,2271,2291,2308,2326,2338,2342,2349,2360,2371,2382,2393,2398,2416,2446,2458,2462,2466,2472,2476,2480],{"__ignoreMap":269},[371,2259,2260,2263,2266,2269],{"class":373,"line":374},[371,2261,2262],{"class":377},"export",[371,2264,2265],{"class":377}," class",[371,2267,2268],{"class":392}," GitWorkerClass",[371,2270,492],{"class":388},[371,2272,2273,2277,2279,2282,2285,2288],{"class":373,"line":425},[371,2274,2276],{"class":2275},"sQHwn","  fs",[371,2278,777],{"class":377},[371,2280,2281],{"class":392}," InstanceType",[371,2283,2284],{"class":388},"\u003C",[371,2286,2287],{"class":377},"typeof",[371,2289,2290],{"class":388}," LightningFS>;\n",[371,2292,2293,2296,2298,2301,2303,2305],{"class":373,"line":432},[371,2294,2295],{"class":2275},"  cache",[371,2297,777],{"class":377},[371,2299,2300],{"class":392}," ReturnType",[371,2302,2284],{"class":388},[371,2304,2287],{"class":377},[371,2306,2307],{"class":388}," useGitRepoCache>;\n",[371,2309,2310,2313,2315,2318,2321,2324],{"class":373,"line":447},[371,2311,2312],{"class":2275},"  currentBranch",[371,2314,777],{"class":377},[371,2316,2317],{"class":381}," string",[371,2319,2320],{"class":377}," |",[371,2322,2323],{"class":381}," undefined",[371,2325,690],{"class":388},[371,2327,2328,2331,2333,2336],{"class":373,"line":462},[371,2329,2330],{"class":2275},"  LOG_DEPTH_CAP",[371,2332,385],{"class":377},[371,2334,2335],{"class":381}," 1000",[371,2337,690],{"class":388},[371,2339,2340],{"class":373,"line":468},[371,2341,429],{"emptyLinePlaceholder":428},[371,2343,2344,2347],{"class":373,"line":477},[371,2345,2346],{"class":377},"  constructor",[371,2348,638],{"class":388},[371,2350,2351,2354,2356,2358],{"class":373,"line":483},[371,2352,2353],{"class":2275},"    fsName",[371,2355,777],{"class":377},[371,2357,2317],{"class":381},[371,2359,646],{"class":388},[371,2361,2362,2365,2367,2369],{"class":373,"line":495},[371,2363,2364],{"class":2275},"    dir",[371,2366,777],{"class":377},[371,2368,2317],{"class":381},[371,2370,646],{"class":388},[371,2372,2373,2376,2378,2380],{"class":373,"line":523},[371,2374,2375],{"class":2275},"    repoUrl",[371,2377,777],{"class":377},[371,2379,2317],{"class":381},[371,2381,646],{"class":388},[371,2383,2384,2387,2390],{"class":373,"line":530},[371,2385,2386],{"class":2275},"    initialBranch",[371,2388,2389],{"class":377},"?:",[371,2391,2392],{"class":381}," string\n",[371,2394,2395],{"class":373,"line":535},[371,2396,2397],{"class":388},"  ) {\n",[371,2399,2400,2403,2406,2408,2410,2413],{"class":373,"line":541},[371,2401,2402],{"class":381},"    this",[371,2404,2405],{"class":388},".fs ",[371,2407,2199],{"class":377},[371,2409,1550],{"class":377},[371,2411,2412],{"class":392}," LightningFS",[371,2414,2415],{"class":388},"(fsName);\n",[371,2417,2418,2420,2423,2425,2428,2431,2434,2437,2439,2441,2444],{"class":373,"line":546},[371,2419,2402],{"class":381},[371,2421,2422],{"class":388},".cache ",[371,2424,2199],{"class":377},[371,2426,2427],{"class":392}," useGitRepoCache",[371,2429,2430],{"class":388},"(dir, ",[371,2432,2433],{"class":381},"this",[371,2435,2436],{"class":388},".fs, ",[371,2438,2433],{"class":381},[371,2440,332],{"class":388},[371,2442,2443],{"class":381},"LOG_DEPTH_CAP",[371,2445,422],{"class":388},[371,2447,2448,2450,2453,2455],{"class":373,"line":559},[371,2449,2402],{"class":381},[371,2451,2452],{"class":388},".currentBranch ",[371,2454,2199],{"class":377},[371,2456,2457],{"class":388}," initialBranch;\n",[371,2459,2460],{"class":373,"line":565},[371,2461,1106],{"class":388},[371,2463,2464],{"class":373,"line":573},[371,2465,429],{"emptyLinePlaceholder":428},[371,2467,2468],{"class":373,"line":1166},[371,2469,2471],{"class":2470},"sCsY4","  // initRepo(), getFileBlob(), getAllCommits(), getCommitDiff(), ...\n",[371,2473,2474],{"class":373,"line":1171},[371,2475,538],{"class":388},[371,2477,2478],{"class":373,"line":1183},[371,2479,429],{"emptyLinePlaceholder":428},[371,2481,2482,2485,2488],{"class":373,"line":1203},[371,2483,2484],{"class":388},"Comlink.",[371,2486,2487],{"class":392},"expose",[371,2489,2490],{"class":388},"(GitWorkerClass);\n",[10,2492,2493,2494,2497,2498,2505],{},"Raw ",[212,2495,2496],{},"postMessage"," calls become tedious when a worker has many operations and\ncallbacks. ",[27,2499,2502],{"href":2500,"rel":2501},"https://github.com/GoogleChromeLabs/comlink",[31],[212,2503,2504],{},"Comlink"," exposes the\nworker class through an RPC-like proxy, so the composable can call async\nmethods while keeping TypeScript's method signatures.",[262,2507,2509],{"className":612,"code":2508,"filename":1374,"language":615,"meta":269,"style":269},"const nativeWorker = new GitWorkerInstance();\nconst remoteWorker = Comlink.wrap\u003Ctypeof GitWorkerClass>(nativeWorker);\n\ngitRepoInstance = await new remoteWorker(\n  fsName,\n  dir,\n  repoUrl,\n  initialBranch\n);\n\nconst response = await gitRepoInstance.initRepo(\n  Comlink.proxy((updatedFiles, count, capped) => {\n    files.value = updatedFiles;\n    commitCount.value = count;\n    commitCountCapped.value = capped;\n    isResolvingCommits.value = false;\n  })\n);\n",[212,2510,2511,2527,2549,2553,2568,2573,2578,2583,2588,2592,2596,2615,2646,2656,2666,2676,2688,2693],{"__ignoreMap":269},[371,2512,2513,2515,2518,2520,2522,2525],{"class":373,"line":374},[371,2514,378],{"class":377},[371,2516,2517],{"class":381}," nativeWorker",[371,2519,385],{"class":377},[371,2521,1550],{"class":377},[371,2523,2524],{"class":392}," GitWorkerInstance",[371,2526,1659],{"class":388},[371,2528,2529,2531,2534,2536,2539,2542,2544,2546],{"class":373,"line":425},[371,2530,378],{"class":377},[371,2532,2533],{"class":381}," remoteWorker",[371,2535,385],{"class":377},[371,2537,2538],{"class":388}," Comlink.",[371,2540,2541],{"class":392},"wrap",[371,2543,2284],{"class":388},[371,2545,2287],{"class":377},[371,2547,2548],{"class":388}," GitWorkerClass>(nativeWorker);\n",[371,2550,2551],{"class":373,"line":432},[371,2552,429],{"emptyLinePlaceholder":428},[371,2554,2555,2558,2560,2562,2564,2566],{"class":373,"line":447},[371,2556,2557],{"class":388},"gitRepoInstance ",[371,2559,2199],{"class":377},[371,2561,629],{"class":377},[371,2563,1550],{"class":377},[371,2565,2533],{"class":392},[371,2567,638],{"class":388},[371,2569,2570],{"class":373,"line":462},[371,2571,2572],{"class":388},"  fsName,\n",[371,2574,2575],{"class":373,"line":468},[371,2576,2577],{"class":388},"  dir,\n",[371,2579,2580],{"class":373,"line":477},[371,2581,2582],{"class":388},"  repoUrl,\n",[371,2584,2585],{"class":373,"line":483},[371,2586,2587],{"class":388},"  initialBranch\n",[371,2589,2590],{"class":373,"line":495},[371,2591,422],{"class":388},[371,2593,2594],{"class":373,"line":523},[371,2595,429],{"emptyLinePlaceholder":428},[371,2597,2598,2600,2603,2605,2607,2610,2613],{"class":373,"line":530},[371,2599,378],{"class":377},[371,2601,2602],{"class":381}," response",[371,2604,385],{"class":377},[371,2606,629],{"class":377},[371,2608,2609],{"class":388}," gitRepoInstance.",[371,2611,2612],{"class":392},"initRepo",[371,2614,638],{"class":388},[371,2616,2617,2620,2623,2626,2629,2631,2634,2636,2639,2642,2644],{"class":373,"line":535},[371,2618,2619],{"class":388},"  Comlink.",[371,2621,2622],{"class":392},"proxy",[371,2624,2625],{"class":388},"((",[371,2627,2628],{"class":2275},"updatedFiles",[371,2630,236],{"class":388},[371,2632,2633],{"class":2275},"count",[371,2635,236],{"class":388},[371,2637,2638],{"class":2275},"capped",[371,2640,2641],{"class":388},") ",[371,2643,1990],{"class":377},[371,2645,492],{"class":388},[371,2647,2648,2651,2653],{"class":373,"line":541},[371,2649,2650],{"class":388},"    files.value ",[371,2652,2199],{"class":377},[371,2654,2655],{"class":388}," updatedFiles;\n",[371,2657,2658,2661,2663],{"class":373,"line":546},[371,2659,2660],{"class":388},"    commitCount.value ",[371,2662,2199],{"class":377},[371,2664,2665],{"class":388}," count;\n",[371,2667,2668,2671,2673],{"class":373,"line":559},[371,2669,2670],{"class":388},"    commitCountCapped.value ",[371,2672,2199],{"class":377},[371,2674,2675],{"class":388}," capped;\n",[371,2677,2678,2681,2683,2686],{"class":373,"line":565},[371,2679,2680],{"class":388},"    isResolvingCommits.value ",[371,2682,2199],{"class":377},[371,2684,2685],{"class":381}," false",[371,2687,690],{"class":388},[371,2689,2690],{"class":373,"line":573},[371,2691,2692],{"class":388},"  })\n",[371,2694,2695],{"class":373,"line":1166},[371,2696,422],{"class":388},[10,2698,2699],{},"The progress function travels in the opposite direction, from the worker back\nto Vue. That lets the worker return the initial repository view quickly and\npublish slower metadata later.",[10,2701,2702,2703,2706,2707,2710],{},"ZIP downloads use one more optimization. The worker creates a ",[212,2704,2705],{},"Uint8Array"," and\nreturns its underlying buffer with ",[212,2708,2709],{},"Comlink.transfer",", transferring ownership\ninstead of cloning a potentially large byte array between threads.",[59,2712,2714],{"id":2713},"how-the-client-code-is-divided","How the client code is divided",[10,2716,2717,2718,2721],{},"Moving the engine into a worker created a new risk: one enormous composable on\nthe Vue side and one enormous class on the worker side. The first explorer had\nalready put the virtual filesystem, Git operations, and Vue state inside one\n",[212,2719,2720],{},"useGitRepo"," composable. I kept the new boundary useful by splitting the client\ninto transport, interpretation, reactive state, derived views, and\npresentation.",[131,2723,2724,2737],{},[134,2725,2726],{},[137,2727,2728,2731,2734],{},[140,2729,2730],{},"Boundary",[140,2732,2733],{},"Responsibility",[140,2735,2736],{},"Main files",[150,2738,2739,2754,2766,2779,2792,2807,2826,2848,2867],{},[137,2740,2741,2744,2750],{},[155,2742,2743],{},"Transport and persistence",[155,2745,2746,2747,2749],{},"Fetch Git files and maintain the IndexedDB-backed ",[212,2748,312],{}," directory",[155,2751,2752],{},[212,2753,1486],{},[137,2755,2756,2759,2762],{},[155,2757,2758],{},"Git engine",[155,2760,2761],{},"Read refs, commits, trees, blobs, and create ZIP archives",[155,2763,2764],{},[212,2765,2255],{},[137,2767,2768,2771,2774],{},[155,2769,2770],{},"Derived Git caches",[155,2772,2773],{},"Cache logs, parsed trees, folder metadata, and commit diffs",[155,2775,2776],{},[212,2777,2778],{},"app/composables/repo/useGitRepoCache.ts",[137,2780,2781,2784,2787],{},[155,2782,2783],{},"Tree history",[155,2785,2786],{},"Resolve the latest commit touching each visible entry",[155,2788,2789],{},[212,2790,2791],{},"app/utils/resolveTreeCommitsAsync.ts",[137,2793,2794,2797,2800],{},[155,2795,2796],{},"Vue facade",[155,2798,2799],{},"Translate worker results into reactive application state",[155,2801,2802,236,2804],{},[212,2803,1374],{},[212,2805,2806],{},"app/composables/repo/useGitRepo.ts",[137,2808,2809,2812,2815],{},[155,2810,2811],{},"File interpretation",[155,2813,2814],{},"Classify blobs, create image URLs, rewrite Markdown assets, and download files",[155,2816,2817,236,2820,236,2823],{},[212,2818,2819],{},"app/composables/repo/useFileContent.ts",[212,2821,2822],{},"app/composables/repo/useDownload.ts",[212,2824,2825],{},"app/composables/repo/useMarkdownImageResolver.ts",[137,2827,2828,2831,2834],{},[155,2829,2830],{},"Diff derivation",[155,2832,2833],{},"Build unified and split rows, expand context, highlight source, and combine syntax tokens with changes",[155,2835,2836,236,2839,236,2842,236,2845],{},[212,2837,2838],{},"app/composables/repo/useDiffRows.ts",[212,2840,2841],{},"app/composables/repo/useFullFileDiff.ts",[212,2843,2844],{},"app/composables/repo/useSyntaxHighlighting.ts",[212,2846,2847],{},"app/composables/repo/useDiffRowsHighlighted.ts",[137,2849,2850,2853,2856],{},[155,2851,2852],{},"Navigation",[155,2854,2855],{},"Convert repository paths and changed files into sidebar links",[155,2857,2858,236,2861,236,2864],{},[212,2859,2860],{},"app/utils/useTreeLinks.ts",[212,2862,2863],{},"app/utils/useDiffTreeLinks.ts",[212,2865,2866],{},"app/utils/goBack.ts",[137,2868,2869,2872,2875],{},[155,2870,2871],{},"Presentation",[155,2873,2874],{},"Compose the overview, tree, blob, history, and diff interfaces",[155,2876,2877,236,2880,236,2883],{},[212,2878,2879],{},"app/components/repo/",[212,2881,2882],{},"app/layouts/tree.vue",[212,2884,2885],{},"app/layouts/diff.vue",[10,2887,2888,2889,2892,2893,2896],{},"The composables form the application-facing API. Components request operations\nsuch as ",[212,2890,2891],{},"getFilesInFolder()"," or ",[212,2894,2895],{},"getCommitDiff()"," and react to refs; they don't\nfetch packfiles or parse Git objects themselves. The utilities either handle\ntransport or perform focused transformations that don't belong in the view\nlayer.",[59,2898,2900],{"id":2899},"making-the-first-render-fast","Making the first render fast",[10,2902,2903,2904,2907],{},"The worker fixed the frozen interface, but it did not make wasteful Git queries\ncheap. Initialization still runs ",[212,2905,2906],{},"git.listFiles()"," for the entire repository\nand transfers that complete path list to Vue. Turning the list into root rows is\nmuch cheaper than resolving the latest commit beside every row, but it is not\nfree for repositories with enormous trees.",[10,2909,2910],{},"Git stores a commit's root tree, and each tree points to files and more trees.\nIt does not store a direct \"last changed by commit X\" property on each entry.\nTo calculate that label, the viewer must walk backward through history and find\nthe newest commit where an entry's object ID differs from its parent.",[10,2912,2913],{},"Waiting for that scan before rendering anything made the repository feel slow,\neven when the tree itself was already available. The worker now returns names\nand types first, then resolves commit metadata asynchronously.",[262,2915,2917],{"className":612,"code":2916,"filename":2255,"language":615,"meta":269,"style":269},"const items = Array.from(fileMap.values());\n\nvoid (async () => {\n  const allCommits = await this.cache.getCachedCommits(\n    this.currentBranch ?? ''\n  );\n  commitCount = allCommits.length;\n  commitCountCapped = allCommits.length >= this.LOG_DEPTH_CAP;\n\n  await resolveTreeCommitsAsync(\n    items,\n    allCommits,\n    this.fs,\n    this.dir,\n    this.cache.gitCache,\n    undefined,\n    undefined,\n    this.cache.parsedTreeCache\n  );\n\n  if (onProgress) {\n    onProgress(items, commitCount, commitCountCapped);\n  }\n})();\n\nreturn {\n  ok: true,\n  data: { files: items, branches, tags, lastCommit }\n};\n",[212,2918,2919,2943,2947,2964,2986,2998,3002,3017,3039,3043,3052,3057,3062,3069,3076,3083,3090,3096,3103,3107,3111,3118,3126,3130,3135,3139,3145,3155,3160],{"__ignoreMap":269},[371,2920,2921,2923,2926,2928,2931,2934,2937,2940],{"class":373,"line":374},[371,2922,378],{"class":377},[371,2924,2925],{"class":381}," items",[371,2927,385],{"class":377},[371,2929,2930],{"class":388}," Array.",[371,2932,2933],{"class":392},"from",[371,2935,2936],{"class":388},"(fileMap.",[371,2938,2939],{"class":392},"values",[371,2941,2942],{"class":388},"());\n",[371,2944,2945],{"class":373,"line":425},[371,2946,429],{"emptyLinePlaceholder":428},[371,2948,2949,2952,2954,2957,2960,2962],{"class":373,"line":432},[371,2950,2951],{"class":377},"void",[371,2953,981],{"class":388},[371,2955,2956],{"class":377},"async",[371,2958,2959],{"class":388}," () ",[371,2961,1990],{"class":377},[371,2963,492],{"class":388},[371,2965,2966,2968,2971,2973,2975,2978,2981,2984],{"class":373,"line":447},[371,2967,992],{"class":377},[371,2969,2970],{"class":381}," allCommits",[371,2972,385],{"class":377},[371,2974,629],{"class":377},[371,2976,2977],{"class":381}," this",[371,2979,2980],{"class":388},".cache.",[371,2982,2983],{"class":392},"getCachedCommits",[371,2985,638],{"class":388},[371,2987,2988,2990,2992,2995],{"class":373,"line":462},[371,2989,2402],{"class":381},[371,2991,2452],{"class":388},[371,2993,2994],{"class":377},"??",[371,2996,2997],{"class":399}," ''\n",[371,2999,3000],{"class":373,"line":468},[371,3001,1271],{"class":388},[371,3003,3004,3007,3009,3012,3015],{"class":373,"line":477},[371,3005,3006],{"class":388},"  commitCount ",[371,3008,2199],{"class":377},[371,3010,3011],{"class":388}," allCommits.",[371,3013,3014],{"class":381},"length",[371,3016,690],{"class":388},[371,3018,3019,3022,3024,3026,3028,3031,3033,3035,3037],{"class":373,"line":483},[371,3020,3021],{"class":388},"  commitCountCapped ",[371,3023,2199],{"class":377},[371,3025,3011],{"class":388},[371,3027,3014],{"class":381},[371,3029,3030],{"class":377}," >=",[371,3032,2977],{"class":381},[371,3034,332],{"class":388},[371,3036,2443],{"class":381},[371,3038,690],{"class":388},[371,3040,3041],{"class":373,"line":495},[371,3042,429],{"emptyLinePlaceholder":428},[371,3044,3045,3047,3050],{"class":373,"line":523},[371,3046,1825],{"class":377},[371,3048,3049],{"class":392}," resolveTreeCommitsAsync",[371,3051,638],{"class":388},[371,3053,3054],{"class":373,"line":530},[371,3055,3056],{"class":388},"    items,\n",[371,3058,3059],{"class":373,"line":535},[371,3060,3061],{"class":388},"    allCommits,\n",[371,3063,3064,3066],{"class":373,"line":541},[371,3065,2402],{"class":381},[371,3067,3068],{"class":388},".fs,\n",[371,3070,3071,3073],{"class":373,"line":546},[371,3072,2402],{"class":381},[371,3074,3075],{"class":388},".dir,\n",[371,3077,3078,3080],{"class":373,"line":559},[371,3079,2402],{"class":381},[371,3081,3082],{"class":388},".cache.gitCache,\n",[371,3084,3085,3088],{"class":373,"line":565},[371,3086,3087],{"class":381},"    undefined",[371,3089,646],{"class":388},[371,3091,3092,3094],{"class":373,"line":573},[371,3093,3087],{"class":381},[371,3095,646],{"class":388},[371,3097,3098,3100],{"class":373,"line":1166},[371,3099,2402],{"class":381},[371,3101,3102],{"class":388},".cache.parsedTreeCache\n",[371,3104,3105],{"class":373,"line":1171},[371,3106,1271],{"class":388},[371,3108,3109],{"class":373,"line":1183},[371,3110,429],{"emptyLinePlaceholder":428},[371,3112,3113,3115],{"class":373,"line":1203},[371,3114,1065],{"class":377},[371,3116,3117],{"class":388}," (onProgress) {\n",[371,3119,3120,3123],{"class":373,"line":1208},[371,3121,3122],{"class":392},"    onProgress",[371,3124,3125],{"class":388},"(items, commitCount, commitCountCapped);\n",[371,3127,3128],{"class":373,"line":1213},[371,3129,1106],{"class":388},[371,3131,3132],{"class":373,"line":1790},[371,3133,3134],{"class":388},"})();\n",[371,3136,3137],{"class":373,"line":1802},[371,3138,429],{"emptyLinePlaceholder":428},[371,3140,3141,3143],{"class":373,"line":1812},[371,3142,1216],{"class":377},[371,3144,492],{"class":388},[371,3146,3147,3150,3153],{"class":373,"line":1817},[371,3148,3149],{"class":388},"  ok: ",[371,3151,3152],{"class":381},"true",[371,3154,646],{"class":388},[371,3156,3157],{"class":373,"line":1822},[371,3158,3159],{"class":388},"  data: { files: items, branches, tags, lastCommit }\n",[371,3161,3162],{"class":373,"line":1834},[371,3163,3164],{"class":388},"};\n",[10,3166,3167,3168,3171,3172,332],{},"Folders use the same two-stage response. The UI receives the directory entries\nimmediately with ",[212,3169,3170],{},"isDone: false","; commit labels arrive through another progress\ncallback with ",[212,3173,3174],{},"isDone: true",[10,3176,3177],{},"A folder view narrows the later scan one more time. It first asks Git for the\nlatest commit affecting that folder, finds the commit inside the cached log,\nand discards every newer candidate before resolving its child rows. That\npreliminary filepath query can still walk deep history, but it replaces one\npath query per child with one path query for the folder.",[355,3179,3181],{"id":3180},"resolving-the-last-commit-for-a-tree-entry","Resolving the last commit for a tree entry",[10,3183,3184],{},"The resolution algorithm walks commits from newest to oldest. For a nested\nfolder, it opens the requested folder tree and the same folder in the first\nparent. If both tree IDs match, nothing inside that subtree changed, so the\nalgorithm skips the commit without comparing every entry. At the repository\nroot, the current implementation still compares the visible entry IDs for each\nvisited commit.",[10,3186,3187],{},"When the trees differ, it builds maps from entry name to object ID. The first\ncommit where the current and parent IDs differ is the newest commit touching\nthat item. Git's trees form a Merkle-style graph, so a change to a child also\nchanges the IDs of the trees above it.",[262,3189,3191],{"className":612,"code":3190,"filename":2791,"language":615,"meta":269,"style":269},"for (const commit of allCommits) {\n  if (unresolvedKeys.size === 0) break;\n\n  const currentTreeData = await getFolderTree(commit.oid);\n  const parentOid = commit.commit.parent[0];\n  const parentTreeData = parentOid\n    ? await getFolderTree(parentOid)\n    : null;\n\n  if (currentTreeData?.oid === parentTreeData?.oid) continue;\n\n  const currentMap = new Map(\n    currentTreeData?.tree.map(entry => [entry.path, entry.oid]) ?? []\n  );\n  const parentMap = new Map(\n    parentTreeData?.tree.map(entry => [entry.path, entry.oid]) ?? []\n  );\n\n  for (const key of unresolvedKeys) {\n    if (currentMap.get(key) !== parentMap.get(key)) {\n      const item = itemsByKey.get(key);\n      if (item) {\n        item.commit = {\n          hash: commit.oid,\n          message: commit.commit.message.trim(),\n          author: commit.commit.author.name,\n          email: commit.commit.author.email,\n          date: new Date(commit.commit.author.timestamp * 1000),\n          parentHash: commit.commit.parent[0]\n        };\n      }\n      unresolvedKeys.delete(key);\n    }\n  }\n}\n",[212,3192,3193,3209,3229,3233,3250,3268,3280,3291,3299,3303,3319,3323,3339,3363,3367,3382,3401,3405,3409,3426,3449,3467,3475,3484,3489,3499,3504,3509,3531,3541,3546,3551,3561,3566,3570],{"__ignoreMap":269},[371,3194,3195,3197,3199,3201,3204,3206],{"class":373,"line":374},[371,3196,1571],{"class":377},[371,3198,981],{"class":388},[371,3200,378],{"class":377},[371,3202,3203],{"class":381}," commit",[371,3205,1581],{"class":377},[371,3207,3208],{"class":388}," allCommits) {\n",[371,3210,3211,3213,3216,3219,3222,3224,3227],{"class":373,"line":425},[371,3212,1065],{"class":377},[371,3214,3215],{"class":388}," (unresolvedKeys.size ",[371,3217,3218],{"class":377},"===",[371,3220,3221],{"class":381}," 0",[371,3223,2641],{"class":388},[371,3225,3226],{"class":377},"break",[371,3228,690],{"class":388},[371,3230,3231],{"class":373,"line":432},[371,3232,429],{"emptyLinePlaceholder":428},[371,3234,3235,3237,3240,3242,3244,3247],{"class":373,"line":447},[371,3236,992],{"class":377},[371,3238,3239],{"class":381}," currentTreeData",[371,3241,385],{"class":377},[371,3243,629],{"class":377},[371,3245,3246],{"class":392}," getFolderTree",[371,3248,3249],{"class":388},"(commit.oid);\n",[371,3251,3252,3254,3257,3259,3262,3265],{"class":373,"line":462},[371,3253,992],{"class":377},[371,3255,3256],{"class":381}," parentOid",[371,3258,385],{"class":377},[371,3260,3261],{"class":388}," commit.commit.parent[",[371,3263,3264],{"class":381},"0",[371,3266,3267],{"class":388},"];\n",[371,3269,3270,3272,3275,3277],{"class":373,"line":468},[371,3271,992],{"class":377},[371,3273,3274],{"class":381}," parentTreeData",[371,3276,385],{"class":377},[371,3278,3279],{"class":388}," parentOid\n",[371,3281,3282,3284,3286,3288],{"class":373,"line":477},[371,3283,1005],{"class":377},[371,3285,629],{"class":377},[371,3287,3246],{"class":392},[371,3289,3290],{"class":388},"(parentOid)\n",[371,3292,3293,3295,3297],{"class":373,"line":483},[371,3294,1018],{"class":377},[371,3296,1993],{"class":381},[371,3298,690],{"class":388},[371,3300,3301],{"class":373,"line":495},[371,3302,429],{"emptyLinePlaceholder":428},[371,3304,3305,3307,3310,3312,3315,3317],{"class":373,"line":523},[371,3306,1065],{"class":377},[371,3308,3309],{"class":388}," (currentTreeData?.oid ",[371,3311,3218],{"class":377},[371,3313,3314],{"class":388}," parentTreeData?.oid) ",[371,3316,1624],{"class":377},[371,3318,690],{"class":388},[371,3320,3321],{"class":373,"line":530},[371,3322,429],{"emptyLinePlaceholder":428},[371,3324,3325,3327,3330,3332,3334,3337],{"class":373,"line":535},[371,3326,992],{"class":377},[371,3328,3329],{"class":381}," currentMap",[371,3331,385],{"class":377},[371,3333,1550],{"class":377},[371,3335,3336],{"class":392}," Map",[371,3338,638],{"class":388},[371,3340,3341,3344,3347,3349,3352,3355,3358,3360],{"class":373,"line":541},[371,3342,3343],{"class":388},"    currentTreeData?.tree.",[371,3345,3346],{"class":392},"map",[371,3348,453],{"class":388},[371,3350,3351],{"class":2275},"entry",[371,3353,3354],{"class":377}," =>",[371,3356,3357],{"class":388}," [entry.path, entry.oid]) ",[371,3359,2994],{"class":377},[371,3361,3362],{"class":388}," []\n",[371,3364,3365],{"class":373,"line":546},[371,3366,1271],{"class":388},[371,3368,3369,3371,3374,3376,3378,3380],{"class":373,"line":559},[371,3370,992],{"class":377},[371,3372,3373],{"class":381}," parentMap",[371,3375,385],{"class":377},[371,3377,1550],{"class":377},[371,3379,3336],{"class":392},[371,3381,638],{"class":388},[371,3383,3384,3387,3389,3391,3393,3395,3397,3399],{"class":373,"line":565},[371,3385,3386],{"class":388},"    parentTreeData?.tree.",[371,3388,3346],{"class":392},[371,3390,453],{"class":388},[371,3392,3351],{"class":2275},[371,3394,3354],{"class":377},[371,3396,3357],{"class":388},[371,3398,2994],{"class":377},[371,3400,3362],{"class":388},[371,3402,3403],{"class":373,"line":573},[371,3404,1271],{"class":388},[371,3406,3407],{"class":373,"line":1166},[371,3408,429],{"emptyLinePlaceholder":428},[371,3410,3411,3414,3416,3418,3421,3423],{"class":373,"line":1171},[371,3412,3413],{"class":377},"  for",[371,3415,981],{"class":388},[371,3417,378],{"class":377},[371,3419,3420],{"class":381}," key",[371,3422,1581],{"class":377},[371,3424,3425],{"class":388}," unresolvedKeys) {\n",[371,3427,3428,3431,3434,3436,3439,3441,3444,3446],{"class":373,"line":1183},[371,3429,3430],{"class":377},"    if",[371,3432,3433],{"class":388}," (currentMap.",[371,3435,1151],{"class":392},[371,3437,3438],{"class":388},"(key) ",[371,3440,704],{"class":377},[371,3442,3443],{"class":388}," parentMap.",[371,3445,1151],{"class":392},[371,3447,3448],{"class":388},"(key)) {\n",[371,3450,3451,3454,3457,3459,3462,3464],{"class":373,"line":1203},[371,3452,3453],{"class":377},"      const",[371,3455,3456],{"class":381}," item",[371,3458,385],{"class":377},[371,3460,3461],{"class":388}," itemsByKey.",[371,3463,1151],{"class":392},[371,3465,3466],{"class":388},"(key);\n",[371,3468,3469,3472],{"class":373,"line":1208},[371,3470,3471],{"class":377},"      if",[371,3473,3474],{"class":388}," (item) {\n",[371,3476,3477,3480,3482],{"class":373,"line":1213},[371,3478,3479],{"class":388},"        item.commit ",[371,3481,2199],{"class":377},[371,3483,492],{"class":388},[371,3485,3486],{"class":373,"line":1790},[371,3487,3488],{"class":388},"          hash: commit.oid,\n",[371,3490,3491,3494,3496],{"class":373,"line":1802},[371,3492,3493],{"class":388},"          message: commit.commit.message.",[371,3495,1656],{"class":392},[371,3497,3498],{"class":388},"(),\n",[371,3500,3501],{"class":373,"line":1812},[371,3502,3503],{"class":388},"          author: commit.commit.author.name,\n",[371,3505,3506],{"class":373,"line":1817},[371,3507,3508],{"class":388},"          email: commit.commit.author.email,\n",[371,3510,3511,3514,3517,3520,3523,3526,3528],{"class":373,"line":1822},[371,3512,3513],{"class":388},"          date: ",[371,3515,3516],{"class":377},"new",[371,3518,3519],{"class":392}," Date",[371,3521,3522],{"class":388},"(commit.commit.author.timestamp ",[371,3524,3525],{"class":377},"*",[371,3527,2335],{"class":381},[371,3529,3530],{"class":388},"),\n",[371,3532,3533,3536,3538],{"class":373,"line":1834},[371,3534,3535],{"class":388},"          parentHash: commit.commit.parent[",[371,3537,3264],{"class":381},[371,3539,3540],{"class":388},"]\n",[371,3542,3543],{"class":373,"line":1851},[371,3544,3545],{"class":388},"        };\n",[371,3547,3548],{"class":373,"line":1857},[371,3549,3550],{"class":388},"      }\n",[371,3552,3553,3556,3559],{"class":373,"line":1862},[371,3554,3555],{"class":388},"      unresolvedKeys.",[371,3557,3558],{"class":392},"delete",[371,3560,3466],{"class":388},[371,3562,3563],{"class":373,"line":1873},[371,3564,3565],{"class":388},"    }\n",[371,3567,3568],{"class":373,"line":1888},[371,3569,1106],{"class":388},[371,3571,3572],{"class":373,"line":1894},[371,3573,538],{"class":388},[10,3575,3576],{},"Tree parsing is cached by object ID, and folder lookups are cached per commit\nduring a scan. The loop also yields periodically so the worker can process its\nevent queue during large histories.",[10,3578,3579,3580,3584],{},"The initial scan stops at 1,000 commits. If an entry hasn't changed within that\nwindow, the tree shows a ",[3581,3582,3583],"strong",{},"View History"," action instead of pretending the\nmetadata is complete. Opening that path's history runs the targeted, uncapped\nquery.",[10,3586,3587],{},"This is one of the most important compromises in the viewer. Exact metadata for\nevery entry is possible, but making every visitor pay for it up front is not a\ngood trade.",[59,3589,3591],{"id":3590},"caching-at-more-than-one-layer","Caching at more than one layer",[10,3593,3594],{},"Moving the work to a worker keeps the interface responsive, but it doesn't make\nthe work free. Without caching, every route change would reopen the same Git\nobjects and every visit would download the same packs.",[10,3596,3597],{},"The viewer uses several caches because each one removes a different kind of\nrepetition:",[131,3599,3600,3613],{},[134,3601,3602],{},[137,3603,3604,3607,3610],{},[140,3605,3606],{},"Layer",[140,3608,3609],{},"Stores",[140,3611,3612],{},"Avoids",[150,3614,3615,3626,3639,3650,3661,3672],{},[137,3616,3617,3620,3623],{},[155,3618,3619],{},"Browser and intermediary HTTP caches",[155,3621,3622],{},"Immutable packs and Git objects",[155,3624,3625],{},"Repeated network reads when caches honor the headers",[137,3627,3628,3631,3636],{},[155,3629,3630],{},"LightningFS / IndexedDB",[155,3632,3633,3634,2749],{},"The reconstructed ",[212,3635,312],{},[155,3637,3638],{},"Re-downloading a repository",[137,3640,3641,3644,3647],{},[155,3642,3643],{},"IndexedDB JSON cache",[155,3645,3646],{},"Commit logs and per-entry commit metadata",[155,3648,3649],{},"Rewalking history after reload",[137,3651,3652,3655,3658],{},[155,3653,3654],{},"Worker memory maps",[155,3656,3657],{},"Parsed trees, folder results, logs, and diffs",[155,3659,3660],{},"Repeating work during one session",[137,3662,3663,3666,3669],{},[155,3664,3665],{},"Vue repository cache",[155,3667,3668],{},"Worker-backed repository instances",[155,3670,3671],{},"Reinitializing between Nuxt routes",[137,3673,3674,3677,3680],{},[155,3675,3676],{},"Highlight caches",[155,3678,3679],{},"Shiki HTML and merged diff tokens",[155,3681,3682],{},"Re-highlighting identical source",[10,3684,3685],{},"The persistent commit cache includes the branch head object ID. Cached metadata\nis reused only when the current head matches the stored head:",[262,3687,3689],{"className":612,"code":3688,"filename":2778,"language":615,"meta":269,"style":269},"const cachePath = `${dir}/.commit-cache-${branch}.json`;\nconst cached = JSON.parse(await pfs.readFile(cachePath, 'utf8'));\n\nif (cached.headOid === headOid) {\n  return cached;\n}\n\nreturn null;\n",[212,3690,3691,3715,3750,3754,3766,3773,3777,3781],{"__ignoreMap":269},[371,3692,3693,3695,3698,3700,3702,3704,3707,3710,3713],{"class":373,"line":374},[371,3694,378],{"class":377},[371,3696,3697],{"class":381}," cachePath",[371,3699,385],{"class":377},[371,3701,1441],{"class":399},[371,3703,1528],{"class":388},[371,3705,3706],{"class":399},"}/.commit-cache-${",[371,3708,3709],{"class":388},"branch",[371,3711,3712],{"class":399},"}.json`",[371,3714,690],{"class":388},[371,3716,3717,3719,3722,3724,3727,3729,3732,3734,3736,3738,3741,3744,3747],{"class":373,"line":425},[371,3718,378],{"class":377},[371,3720,3721],{"class":381}," cached",[371,3723,385],{"class":377},[371,3725,3726],{"class":381}," JSON",[371,3728,332],{"class":388},[371,3730,3731],{"class":392},"parse",[371,3733,453],{"class":388},[371,3735,1515],{"class":377},[371,3737,1518],{"class":388},[371,3739,3740],{"class":392},"readFile",[371,3742,3743],{"class":388},"(cachePath, ",[371,3745,3746],{"class":399},"'utf8'",[371,3748,3749],{"class":388},"));\n",[371,3751,3752],{"class":373,"line":432},[371,3753,429],{"emptyLinePlaceholder":428},[371,3755,3756,3758,3761,3763],{"class":373,"line":447},[371,3757,435],{"class":377},[371,3759,3760],{"class":388}," (cached.headOid ",[371,3762,3218],{"class":377},[371,3764,3765],{"class":388}," headOid) {\n",[371,3767,3768,3770],{"class":373,"line":462},[371,3769,730],{"class":377},[371,3771,3772],{"class":388}," cached;\n",[371,3774,3775],{"class":373,"line":468},[371,3776,538],{"class":388},[371,3778,3779],{"class":373,"line":477},[371,3780,429],{"emptyLinePlaceholder":428},[371,3782,3783,3785,3787],{"class":373,"line":483},[371,3784,1216],{"class":377},[371,3786,1993],{"class":381},[371,3788,690],{"class":388},[10,3790,3791,3792,3795],{},"At the Vue layer, ",[212,3793,3794],{},"useGitRepo()"," keeps up to ten repository controllers in a\nsmall global cache. Moving from a tree to a blob, then to history, reuses the\nsame worker and reactive state instead of creating a second browser clone.",[262,3797,3799],{"className":612,"code":3798,"filename":2806,"language":615,"meta":269,"style":269},"gitRepoCache ??= new MiniCache(10);\n\nlet repo = gitRepoCache.get(repoName);\nif (!repo) {\n  repo = createGitRepo(repoName, initialBranch);\n  gitRepoCache.set(repoName, repo, Infinity);\n}\n\nreturn repo;\n",[212,3800,3801,3821,3825,3843,3854,3867,3883,3887,3891],{"__ignoreMap":269},[371,3802,3803,3806,3809,3811,3814,3816,3819],{"class":373,"line":374},[371,3804,3805],{"class":388},"gitRepoCache ",[371,3807,3808],{"class":377},"??=",[371,3810,1550],{"class":377},[371,3812,3813],{"class":392}," MiniCache",[371,3815,453],{"class":388},[371,3817,3818],{"class":381},"10",[371,3820,422],{"class":388},[371,3822,3823],{"class":373,"line":425},[371,3824,429],{"emptyLinePlaceholder":428},[371,3826,3827,3830,3833,3835,3838,3840],{"class":373,"line":432},[371,3828,3829],{"class":377},"let",[371,3831,3832],{"class":388}," repo ",[371,3834,2199],{"class":377},[371,3836,3837],{"class":388}," gitRepoCache.",[371,3839,1151],{"class":392},[371,3841,3842],{"class":388},"(repoName);\n",[371,3844,3845,3847,3849,3851],{"class":373,"line":447},[371,3846,435],{"class":377},[371,3848,981],{"class":388},[371,3850,984],{"class":377},[371,3852,3853],{"class":388},"repo) {\n",[371,3855,3856,3859,3861,3864],{"class":373,"line":462},[371,3857,3858],{"class":388},"  repo ",[371,3860,2199],{"class":377},[371,3862,3863],{"class":392}," createGitRepo",[371,3865,3866],{"class":388},"(repoName, initialBranch);\n",[371,3868,3869,3872,3875,3878,3881],{"class":373,"line":468},[371,3870,3871],{"class":388},"  gitRepoCache.",[371,3873,3874],{"class":392},"set",[371,3876,3877],{"class":388},"(repoName, repo, ",[371,3879,3880],{"class":381},"Infinity",[371,3882,422],{"class":388},[371,3884,3885],{"class":373,"line":477},[371,3886,538],{"class":388},[371,3888,3889],{"class":373,"line":483},[371,3890,429],{"emptyLinePlaceholder":428},[371,3892,3893,3895],{"class":373,"line":495},[371,3894,1216],{"class":377},[371,3896,3897],{"class":388}," repo;\n",[10,3899,3900],{},"Caching also creates failure modes. IndexedDB can be interrupted or corrupted,\nand old metadata can outlive the objects it describes. During initialization,\nthe worker performs a small log read; if it detects the known broken-object\nshape, it wipes that repository's virtual directory and retries once from the\nnetwork.",[10,3902,3903,3904,3907,3908,332],{},"That recovery is intentionally scoped to one repository. A broken cache for\n",[212,3905,3906],{},"curl"," should not delete the cached copy of ",[212,3909,3910],{},"portfolio",[59,3912,3914],{"id":3913},"mapping-git-concepts-to-nuxt-routes","Mapping Git concepts to Nuxt routes",[10,3916,3917],{},"The viewer still needs ordinary URLs that can be copied, refreshed, and opened\ndirectly. One catch-all Nuxt page parses the slug and selects the repository\nview.",[262,3919,3922],{"className":612,"code":3920,"filename":3921,"language":615,"meta":269,"style":269},"const viewType = computed(() => {\n  if (slug.value.length === 1) return 'main';\n  if (slug.value[1] === 'tree') {\n    if (slug.value.length === 3) return 'main';\n    return 'tree';\n  }\n  if (slug.value[1] === 'commits') return 'commits';\n  if (slug.value[1] === 'commit' && slug.value[2]) return 'diff';\n  if (slug.value[1] === 'blob') return 'file';\n  return 'main';\n});\n","app/pages/repo/[...slug].vue",[212,3923,3924,3942,3966,3987,4008,4016,4020,4043,4076,4100,4108],{"__ignoreMap":269},[371,3925,3926,3928,3931,3933,3936,3938,3940],{"class":373,"line":374},[371,3927,378],{"class":377},[371,3929,3930],{"class":381}," viewType",[371,3932,385],{"class":377},[371,3934,3935],{"class":392}," computed",[371,3937,1987],{"class":388},[371,3939,1990],{"class":377},[371,3941,492],{"class":388},[371,3943,3944,3946,3949,3951,3954,3957,3959,3961,3964],{"class":373,"line":425},[371,3945,1065],{"class":377},[371,3947,3948],{"class":388}," (slug.value.",[371,3950,3014],{"class":381},[371,3952,3953],{"class":377}," ===",[371,3955,3956],{"class":381}," 1",[371,3958,2641],{"class":388},[371,3960,1216],{"class":377},[371,3962,3963],{"class":399}," 'main'",[371,3965,690],{"class":388},[371,3967,3968,3970,3973,3976,3979,3981,3984],{"class":373,"line":432},[371,3969,1065],{"class":377},[371,3971,3972],{"class":388}," (slug.value[",[371,3974,3975],{"class":381},"1",[371,3977,3978],{"class":388},"] ",[371,3980,3218],{"class":377},[371,3982,3983],{"class":399}," 'tree'",[371,3985,3986],{"class":388},") {\n",[371,3988,3989,3991,3993,3995,3997,4000,4002,4004,4006],{"class":373,"line":447},[371,3990,3430],{"class":377},[371,3992,3948],{"class":388},[371,3994,3014],{"class":381},[371,3996,3953],{"class":377},[371,3998,3999],{"class":381}," 3",[371,4001,2641],{"class":388},[371,4003,1216],{"class":377},[371,4005,3963],{"class":399},[371,4007,690],{"class":388},[371,4009,4010,4012,4014],{"class":373,"line":462},[371,4011,1741],{"class":377},[371,4013,3983],{"class":399},[371,4015,690],{"class":388},[371,4017,4018],{"class":373,"line":468},[371,4019,1106],{"class":388},[371,4021,4022,4024,4026,4028,4030,4032,4035,4037,4039,4041],{"class":373,"line":477},[371,4023,1065],{"class":377},[371,4025,3972],{"class":388},[371,4027,3975],{"class":381},[371,4029,3978],{"class":388},[371,4031,3218],{"class":377},[371,4033,4034],{"class":399}," 'commits'",[371,4036,2641],{"class":388},[371,4038,1216],{"class":377},[371,4040,4034],{"class":399},[371,4042,690],{"class":388},[371,4044,4045,4047,4049,4051,4053,4055,4058,4061,4064,4066,4069,4071,4074],{"class":373,"line":483},[371,4046,1065],{"class":377},[371,4048,3972],{"class":388},[371,4050,3975],{"class":381},[371,4052,3978],{"class":388},[371,4054,3218],{"class":377},[371,4056,4057],{"class":399}," 'commit'",[371,4059,4060],{"class":377}," &&",[371,4062,4063],{"class":388}," slug.value[",[371,4065,1650],{"class":381},[371,4067,4068],{"class":388},"]) ",[371,4070,1216],{"class":377},[371,4072,4073],{"class":399}," 'diff'",[371,4075,690],{"class":388},[371,4077,4078,4080,4082,4084,4086,4088,4091,4093,4095,4098],{"class":373,"line":495},[371,4079,1065],{"class":377},[371,4081,3972],{"class":388},[371,4083,3975],{"class":381},[371,4085,3978],{"class":388},[371,4087,3218],{"class":377},[371,4089,4090],{"class":399}," 'blob'",[371,4092,2641],{"class":388},[371,4094,1216],{"class":377},[371,4096,4097],{"class":399}," 'file'",[371,4099,690],{"class":388},[371,4101,4102,4104,4106],{"class":373,"line":523},[371,4103,730],{"class":377},[371,4105,3963],{"class":399},[371,4107,690],{"class":388},[371,4109,4110],{"class":373,"line":530},[371,4111,576],{"class":388},[10,4113,4114],{},"The resulting URL model is close to what Git users already recognize:",[262,4116,4120],{"className":4117,"code":4118,"filename":4119,"language":268,"meta":269},[265],"/repo/\u003Cname>\n/repo/\u003Cname>/tree/\u003Cref>/\u003Cpath>\n/repo/\u003Cname>/blob/\u003Cref>/\u003Cpath>\n/repo/\u003Cname>/commits/\u003Cref>/\u003Cpath?>\n/repo/\u003Cname>/commit/\u003Chash>\n","Repository routes",[212,4121,4118],{"__ignoreMap":269},[10,4123,4124,4127,4128,4131,4132,4135,4136,4139],{},[212,4125,4126],{},"\u003Cref>"," can be a single-segment branch name, a tag, or a commit hash anywhere\nthe underlying Git operation supports it. This is what makes ",[3581,4129,4130],{},"Browse\nRepository"," from a history entry useful: the viewer can open the tree exactly\nas it existed at that commit. Refs containing ",[212,4133,4134],{},"/",", such as ",[212,4137,4138],{},"feature/search",", do\nnot round-trip through the current route grammar because the next segment is\ninterpreted as a file path.",[10,4141,4142,4143,332],{},"Detailed tree, blob, history, and commit routes are client-only because\nIndexedDB, Web Workers, and browser object URLs do not exist during server\nrendering. The repository overview can still be prerendered, with its\nGit-dependent interface wrapped in ",[212,4144,4145],{},"\u003CClientOnly>",[262,4147,4150],{"className":612,"code":4148,"filename":4149,"language":615,"meta":269,"style":269},"routeRules: {\n  '/': { prerender: true },\n  '/repo/*/tree/**': { ssr: false },\n  '/repo/*/blob/**': { ssr: false },\n  '/repo/*/commits/**': { ssr: false },\n  '/repo/*/commit/**': { ssr: false }\n}\n","nuxt.config.ts",[212,4151,4152,4160,4173,4186,4197,4208,4220],{"__ignoreMap":269},[371,4153,4154,4157],{"class":373,"line":374},[371,4155,4156],{"class":392},"routeRules",[371,4158,4159],{"class":388},": {\n",[371,4161,4162,4165,4168,4170],{"class":373,"line":425},[371,4163,4164],{"class":399},"  '/'",[371,4166,4167],{"class":388},": { prerender: ",[371,4169,3152],{"class":381},[371,4171,4172],{"class":388}," },\n",[371,4174,4175,4178,4181,4184],{"class":373,"line":432},[371,4176,4177],{"class":399},"  '/repo/*/tree/**'",[371,4179,4180],{"class":388},": { ssr: ",[371,4182,4183],{"class":381},"false",[371,4185,4172],{"class":388},[371,4187,4188,4191,4193,4195],{"class":373,"line":447},[371,4189,4190],{"class":399},"  '/repo/*/blob/**'",[371,4192,4180],{"class":388},[371,4194,4183],{"class":381},[371,4196,4172],{"class":388},[371,4198,4199,4202,4204,4206],{"class":373,"line":462},[371,4200,4201],{"class":399},"  '/repo/*/commits/**'",[371,4203,4180],{"class":388},[371,4205,4183],{"class":381},[371,4207,4172],{"class":388},[371,4209,4210,4213,4215,4217],{"class":373,"line":468},[371,4211,4212],{"class":399},"  '/repo/*/commit/**'",[371,4214,4180],{"class":388},[371,4216,4183],{"class":381},[371,4218,4219],{"class":388}," }\n",[371,4221,4222],{"class":373,"line":477},[371,4223,538],{"class":388},[10,4225,4226,4227,4229,4230,4232],{},"The page also wraps repository components in ",[212,4228,4145],{}," and supplies\nskeletons for the fallback. This keeps the architectural boundary explicit\ninstead of scattering ",[212,4231,1444],{}," and IndexedDB checks through every component.",[59,4234,4236],{"id":4235},"reading-trees-and-blobs","Reading trees and blobs",[10,4238,4239,4240,4242,4243,4246,4247,4250],{},"Once the worker has a valid repository, most file operations become small\n",[212,4241,1352],{}," calls. A directory uses ",[212,4244,4245],{},"readTree",", while a file resolves the\nselected ref to a commit and uses ",[212,4248,4249],{},"readBlob"," with a path.",[262,4252,4254],{"className":612,"code":4253,"filename":2255,"language":615,"meta":269,"style":269},"const commitOid = await git.resolveRef({\n  fs: this.fs,\n  dir: this.dir,\n  ref\n});\n\nconst { blob } = await git.readBlob({\n  fs: this.fs,\n  dir: this.dir,\n  oid: commitOid,\n  filepath,\n  cache: this.cache.gitCache\n});\n",[212,4255,4256,4276,4285,4294,4299,4303,4307,4330,4338,4346,4351,4356,4366],{"__ignoreMap":269},[371,4257,4258,4260,4263,4265,4267,4270,4273],{"class":373,"line":374},[371,4259,378],{"class":377},[371,4261,4262],{"class":381}," commitOid",[371,4264,385],{"class":377},[371,4266,629],{"class":377},[371,4268,4269],{"class":388}," git.",[371,4271,4272],{"class":392},"resolveRef",[371,4274,4275],{"class":388},"({\n",[371,4277,4278,4281,4283],{"class":373,"line":425},[371,4279,4280],{"class":388},"  fs: ",[371,4282,2433],{"class":381},[371,4284,3068],{"class":388},[371,4286,4287,4290,4292],{"class":373,"line":432},[371,4288,4289],{"class":388},"  dir: ",[371,4291,2433],{"class":381},[371,4293,3075],{"class":388},[371,4295,4296],{"class":373,"line":447},[371,4297,4298],{"class":388},"  ref\n",[371,4300,4301],{"class":373,"line":462},[371,4302,576],{"class":388},[371,4304,4305],{"class":373,"line":468},[371,4306,429],{"emptyLinePlaceholder":428},[371,4308,4309,4311,4314,4317,4320,4322,4324,4326,4328],{"class":373,"line":477},[371,4310,378],{"class":377},[371,4312,4313],{"class":388}," { ",[371,4315,4316],{"class":381},"blob",[371,4318,4319],{"class":388}," } ",[371,4321,2199],{"class":377},[371,4323,629],{"class":377},[371,4325,4269],{"class":388},[371,4327,4249],{"class":392},[371,4329,4275],{"class":388},[371,4331,4332,4334,4336],{"class":373,"line":483},[371,4333,4280],{"class":388},[371,4335,2433],{"class":381},[371,4337,3068],{"class":388},[371,4339,4340,4342,4344],{"class":373,"line":495},[371,4341,4289],{"class":388},[371,4343,2433],{"class":381},[371,4345,3075],{"class":388},[371,4347,4348],{"class":373,"line":523},[371,4349,4350],{"class":388},"  oid: commitOid,\n",[371,4352,4353],{"class":373,"line":530},[371,4354,4355],{"class":388},"  filepath,\n",[371,4357,4358,4361,4363],{"class":373,"line":535},[371,4359,4360],{"class":388},"  cache: ",[371,4362,2433],{"class":381},[371,4364,4365],{"class":388},".cache.gitCache\n",[371,4367,4368],{"class":373,"line":541},[371,4369,576],{"class":388},[10,4371,4372],{},"The worker returns bytes rather than assuming every blob is UTF-8 text. That\nkeeps Git access independent from presentation: the file composable can decode\ntext, preserve image bytes, or stop before an unsuitable blob reaches the code\nrenderer.",[10,4374,4375],{},"Markdown receives special treatment. The viewer can show either rendered MDC\nor highlighted raw Markdown. Relative image paths are rewritten to the\nrepository owner's jsDelivr URL so README badges, screenshots, and banners\nstill work outside GitHub.",[10,4377,4378,4379,4382,4383,4386],{},"The root repository page uses the same pipeline to find and render ",[212,4380,4381],{},"README.md","\nand ",[212,4384,4385],{},"LICENSE",". In other words, the README shown under the tree is read from the\nbrowser's local Git object database, not copied into the portfolio's Nuxt\nContent collection.",[59,4388,4390],{"id":4389},"unsupported-content-is-still-a-valid-view","Unsupported content is still a valid view",[10,4392,4393],{},"The happy path made every blob look like source code. Real repositories quickly\nbroke that illusion: they contain images, archives, generated files, Git LFS\npointers, binaries, and text files large enough to punish a browser tab.",[10,4395,4396],{},"Forcing all of them through a text highlighter would produce slow or misleading\nresults. I treat unsupported content as a deliberate presentation state rather\nthan a failed request.",[10,4398,4399,4402],{},[212,4400,4401],{},"useFileContent()"," inspects the first 8,000 bytes for a null byte as a practical\nbinary heuristic. Text that starts with the Git LFS pointer signature also\nenters the unsupported path because the repository stores only the pointer, not\nthe externally managed LFS object.",[262,4404,4406],{"className":612,"code":4405,"filename":2819,"language":615,"meta":269,"style":269},"const isBinary = blob\n  .slice(0, 8000)\n  .some(byte => byte === 0);\n\nif (isBinary) {\n  isBinaryFile.value = true;\n  fileContent.value = null;\n} else {\n  const text = new TextDecoder().decode(blob);\n  const isLfsPointer = text.startsWith(\n    'version https://git-lfs.github.com/spec/v1'\n  );\n\n  isBinaryFile.value = isLfsPointer;\n  fileContent.value = isLfsPointer ? null : text;\n}\n",[212,4407,4408,4420,4438,4461,4465,4472,4483,4494,4502,4522,4538,4543,4547,4551,4560,4580],{"__ignoreMap":269},[371,4409,4410,4412,4415,4417],{"class":373,"line":374},[371,4411,378],{"class":377},[371,4413,4414],{"class":381}," isBinary",[371,4416,385],{"class":377},[371,4418,4419],{"class":388}," blob\n",[371,4421,4422,4424,4427,4429,4431,4433,4436],{"class":373,"line":425},[371,4423,1959],{"class":388},[371,4425,4426],{"class":392},"slice",[371,4428,453],{"class":388},[371,4430,3264],{"class":381},[371,4432,236],{"class":388},[371,4434,4435],{"class":381},"8000",[371,4437,1977],{"class":388},[371,4439,4440,4442,4445,4447,4450,4452,4455,4457,4459],{"class":373,"line":432},[371,4441,1959],{"class":388},[371,4443,4444],{"class":392},"some",[371,4446,453],{"class":388},[371,4448,4449],{"class":2275},"byte",[371,4451,3354],{"class":377},[371,4453,4454],{"class":388}," byte ",[371,4456,3218],{"class":377},[371,4458,3221],{"class":381},[371,4460,422],{"class":388},[371,4462,4463],{"class":373,"line":447},[371,4464,429],{"emptyLinePlaceholder":428},[371,4466,4467,4469],{"class":373,"line":462},[371,4468,435],{"class":377},[371,4470,4471],{"class":388}," (isBinary) {\n",[371,4473,4474,4477,4479,4481],{"class":373,"line":468},[371,4475,4476],{"class":388},"  isBinaryFile.value ",[371,4478,2199],{"class":377},[371,4480,2202],{"class":381},[371,4482,690],{"class":388},[371,4484,4485,4488,4490,4492],{"class":373,"line":477},[371,4486,4487],{"class":388},"  fileContent.value ",[371,4489,2199],{"class":377},[371,4491,1993],{"class":381},[371,4493,690],{"class":388},[371,4495,4496,4498,4500],{"class":373,"line":483},[371,4497,486],{"class":388},[371,4499,489],{"class":377},[371,4501,492],{"class":388},[371,4503,4504,4506,4509,4511,4513,4515,4517,4519],{"class":373,"line":495},[371,4505,992],{"class":377},[371,4507,4508],{"class":381}," text",[371,4510,385],{"class":377},[371,4512,1550],{"class":377},[371,4514,1553],{"class":392},[371,4516,1556],{"class":388},[371,4518,1559],{"class":392},[371,4520,4521],{"class":388},"(blob);\n",[371,4523,4524,4526,4529,4531,4534,4536],{"class":373,"line":523},[371,4525,992],{"class":377},[371,4527,4528],{"class":381}," isLfsPointer",[371,4530,385],{"class":377},[371,4532,4533],{"class":388}," text.",[371,4535,1075],{"class":392},[371,4537,638],{"class":388},[371,4539,4540],{"class":373,"line":530},[371,4541,4542],{"class":399},"    'version https://git-lfs.github.com/spec/v1'\n",[371,4544,4545],{"class":373,"line":535},[371,4546,1271],{"class":388},[371,4548,4549],{"class":373,"line":541},[371,4550,429],{"emptyLinePlaceholder":428},[371,4552,4553,4555,4557],{"class":373,"line":546},[371,4554,4476],{"class":388},[371,4556,2199],{"class":377},[371,4558,4559],{"class":388}," isLfsPointer;\n",[371,4561,4562,4564,4566,4569,4572,4574,4577],{"class":373,"line":559},[371,4563,4487],{"class":388},[371,4565,2199],{"class":377},[371,4567,4568],{"class":388}," isLfsPointer ",[371,4570,4571],{"class":377},"?",[371,4573,1993],{"class":381},[371,4575,4576],{"class":377}," :",[371,4578,4579],{"class":388}," text;\n",[371,4581,4582],{"class":373,"line":565},[371,4583,538],{"class":388},[10,4585,4586],{},"The file view then chooses an explicit renderer:",[70,4588,4589,4592,4599,4605,4608],{},[73,4590,4591],{},"Known image extensions become object URLs and render as images.",[73,4593,4594,4595,4598],{},"Regular text enters ",[212,4596,4597],{},"BlobViewer",", with wrapping and raw/rendered Markdown\ncontrols.",[73,4600,4601,4602,332],{},"Binary files and Git LFS pointers enter ",[212,4603,4604],{},"RepoUnsupportedFile",[73,4606,4607],{},"Files larger than 5 MB skip normal code rendering and expose a download\naction.",[73,4609,4610],{},"Direct image diffs render the old and new images instead of manufacturing a\ntextual patch.",[262,4612,4617],{"className":4613,"code":4614,"filename":4615,"language":4616,"meta":269,"style":269},"language-vue shiki shiki-themes github-light github-dark github-dark","\u003CRepoUnsupportedFile\n  v-else-if=\"isBinaryFile || (fileStats?.size ?? 0) > 5e6\"\n  :repo-name=\"repoName\"\n  :file-path=\"filePath || ''\"\n  :branch=\"branch\"\n  @download=\"downloadFile\"\n/>\n","app/components/repo/RepoFileViewer.vue","vue",[212,4618,4619,4626,4659,4674,4693,4707,4724],{"__ignoreMap":269},[371,4620,4621,4623],{"class":373,"line":374},[371,4622,2284],{"class":388},[371,4624,4625],{"class":793},"RepoUnsupportedFile\n",[371,4627,4628,4631,4633,4636,4639,4641,4644,4646,4648,4650,4653,4656],{"class":373,"line":425},[371,4629,4630],{"class":377},"  v-else-if",[371,4632,2199],{"class":388},[371,4634,4635],{"class":399},"\"",[371,4637,4638],{"class":388},"isBinaryFile ",[371,4640,1239],{"class":377},[371,4642,4643],{"class":388}," (fileStats?.size ",[371,4645,2994],{"class":377},[371,4647,3221],{"class":381},[371,4649,2641],{"class":388},[371,4651,4652],{"class":377},">",[371,4654,4655],{"class":381}," 5e6",[371,4657,4658],{"class":399},"\"\n",[371,4660,4661,4663,4666,4668,4670,4672],{"class":373,"line":432},[371,4662,1467],{"class":388},[371,4664,4665],{"class":392},"repo-name",[371,4667,2199],{"class":388},[371,4669,4635],{"class":399},[371,4671,1391],{"class":388},[371,4673,4658],{"class":399},[371,4675,4676,4678,4681,4683,4685,4688,4690],{"class":373,"line":447},[371,4677,1467],{"class":388},[371,4679,4680],{"class":392},"file-path",[371,4682,2199],{"class":388},[371,4684,4635],{"class":399},[371,4686,4687],{"class":388},"filePath ",[371,4689,1239],{"class":377},[371,4691,4692],{"class":399}," ''\"\n",[371,4694,4695,4697,4699,4701,4703,4705],{"class":373,"line":462},[371,4696,1467],{"class":388},[371,4698,3709],{"class":392},[371,4700,2199],{"class":388},[371,4702,4635],{"class":399},[371,4704,3709],{"class":388},[371,4706,4658],{"class":399},[371,4708,4709,4712,4715,4717,4719,4722],{"class":373,"line":468},[371,4710,4711],{"class":388},"  @",[371,4713,4714],{"class":392},"download",[371,4716,2199],{"class":388},[371,4718,4635],{"class":399},[371,4720,4721],{"class":388},"downloadFile",[371,4723,4658],{"class":399},[371,4725,4726],{"class":373,"line":477},[371,4727,4728],{"class":388},"/>\n",[10,4730,4731],{},"Repository-level absence receives the same treatment. When a project has\nneither a README nor a license, the overview renders a designed empty state\ninstead of an empty card or a parsing error. Loading skeletons and route-level\nerror views cover temporary and unrecoverable failures separately.",[10,4733,4734,4735,4738],{},"These boundaries protect both the reader and the browser. ",[3581,4736,4737],{},"Unsupported"," means\nthe viewer can identify the object and offer a useful next action without\nadding expensive highlighting, diff rows, and DOM nodes for a representation it\ncannot render well.",[59,4740,4742],{"id":4741},"commit-history-without-a-history-database","Commit history without a history database",[10,4744,4745,4746,4749],{},"Commit history also comes directly from Git objects. The worker calls\n",[212,4747,4748],{},"git.log()",", normalizes author and parent data, and sends serializable objects\nback to Vue.",[10,4751,4752],{},"Repository and folder tree metadata use the capped cached log, while the\ndedicated history page asks for the full log. A filepath narrows the Git walk to\ncommits that affect that path.",[10,4754,4755],{},"The UI then performs author and date filtering locally. Pagination only changes\nwhich ten already-loaded commits are rendered; it doesn't create another API\ncontract or server query.",[10,4757,4758],{},"This design makes the history page simple, but it also makes the browser pay the\ncost of a full history when the user explicitly asks for one. That is acceptable\nfor the repositories this portfolio is meant to showcase, but it is another\nreason this architecture isn't intended for something the size of the Linux\nkernel.",[59,4760,4762],{"id":4761},"building-the-diff-pipeline","Building the diff pipeline",[10,4764,4765,4766,4769],{},"The first repository browser still cheated. Its commit page showed the author\nand message, then offered a ",[3581,4767,4768],{},"View Full Diff on GitHub"," button. I had rebuilt\nthe door and left one of the most interesting rooms somewhere else.",[10,4771,4772],{},"Browsing a tree made the repository useful, but diffs were the feature that made\nit feel like a repository viewer instead of a file explorer. They were also the\npoint where Git traversal, text comparison, syntax highlighting, and rendering\nall tried to become one problem.",[10,4774,4775],{},"I split a commit diff across three representations: Git objects in the worker,\nstructured hunks in the client, and highlighted rows in the interface. Keeping\nthose stages separate prevents the whole pipeline from collapsing into one\nlarge component.",[10,4777,4778],{},"For a normal commit, the diff layout requests two log entries beginning at the\nselected hash. It treats the first as the selected commit and the second as the\ncomparison base. The worker walks both trees together, ignores directories and\nunchanged objects, and classifies each remaining path as added, removed, or\nmodified.",[262,4780,4782],{"className":612,"code":4781,"filename":2255,"language":615,"meta":269,"style":269},"const fileStates = await git.walk({\n  fs: this.fs,\n  dir: this.dir,\n  trees: [\n    git.TREE({ ref: oldCommitHash }),\n    git.TREE({ ref: newCommitHash })\n  ],\n  map: async (filepath, [oldEntry, newEntry]) => {\n    if (filepath === '.') return;\n    if (\n      (await oldEntry?.type()) === 'tree' ||\n      (await newEntry?.type()) === 'tree'\n    ) return;\n\n    const oldOid = await oldEntry?.oid();\n    const newOid = await newEntry?.oid();\n\n    if (oldOid === newOid) return;\n    if (oldOid === undefined) return { filepath, type: 'add', newOid };\n    if (newOid === undefined) return { filepath, type: 'remove', oldOid };\n\n    return { filepath, type: 'modify', oldOid, newOid };\n  }\n});\n",[212,4783,4784,4802,4810,4818,4823,4834,4843,4848,4879,4897,4904,4927,4945,4954,4958,4977,4994,4998,5014,5037,5060,5064,5076,5080],{"__ignoreMap":269},[371,4785,4786,4788,4791,4793,4795,4797,4800],{"class":373,"line":374},[371,4787,378],{"class":377},[371,4789,4790],{"class":381}," fileStates",[371,4792,385],{"class":377},[371,4794,629],{"class":377},[371,4796,4269],{"class":388},[371,4798,4799],{"class":392},"walk",[371,4801,4275],{"class":388},[371,4803,4804,4806,4808],{"class":373,"line":425},[371,4805,4280],{"class":388},[371,4807,2433],{"class":381},[371,4809,3068],{"class":388},[371,4811,4812,4814,4816],{"class":373,"line":432},[371,4813,4289],{"class":388},[371,4815,2433],{"class":381},[371,4817,3075],{"class":388},[371,4819,4820],{"class":373,"line":447},[371,4821,4822],{"class":388},"  trees: [\n",[371,4824,4825,4828,4831],{"class":373,"line":462},[371,4826,4827],{"class":388},"    git.",[371,4829,4830],{"class":392},"TREE",[371,4832,4833],{"class":388},"({ ref: oldCommitHash }),\n",[371,4835,4836,4838,4840],{"class":373,"line":468},[371,4837,4827],{"class":388},[371,4839,4830],{"class":392},[371,4841,4842],{"class":388},"({ ref: newCommitHash })\n",[371,4844,4845],{"class":373,"line":477},[371,4846,4847],{"class":388},"  ],\n",[371,4849,4850,4853,4855,4857,4859,4862,4865,4868,4870,4873,4875,4877],{"class":373,"line":483},[371,4851,4852],{"class":392},"  map",[371,4854,796],{"class":388},[371,4856,2956],{"class":377},[371,4858,981],{"class":388},[371,4860,4861],{"class":2275},"filepath",[371,4863,4864],{"class":388},", [",[371,4866,4867],{"class":2275},"oldEntry",[371,4869,236],{"class":388},[371,4871,4872],{"class":2275},"newEntry",[371,4874,4068],{"class":388},[371,4876,1990],{"class":377},[371,4878,492],{"class":388},[371,4880,4881,4883,4886,4888,4891,4893,4895],{"class":373,"line":495},[371,4882,3430],{"class":377},[371,4884,4885],{"class":388}," (filepath ",[371,4887,3218],{"class":377},[371,4889,4890],{"class":399}," '.'",[371,4892,2641],{"class":388},[371,4894,1216],{"class":377},[371,4896,690],{"class":388},[371,4898,4899,4901],{"class":373,"line":523},[371,4900,3430],{"class":377},[371,4902,4903],{"class":388}," (\n",[371,4905,4906,4909,4911,4914,4917,4920,4922,4924],{"class":373,"line":530},[371,4907,4908],{"class":388},"      (",[371,4910,1515],{"class":377},[371,4912,4913],{"class":388}," oldEntry?.",[371,4915,4916],{"class":392},"type",[371,4918,4919],{"class":388},"()) ",[371,4921,3218],{"class":377},[371,4923,3983],{"class":399},[371,4925,4926],{"class":377}," ||\n",[371,4928,4929,4931,4933,4936,4938,4940,4942],{"class":373,"line":535},[371,4930,4908],{"class":388},[371,4932,1515],{"class":377},[371,4934,4935],{"class":388}," newEntry?.",[371,4937,4916],{"class":392},[371,4939,4919],{"class":388},[371,4941,3218],{"class":377},[371,4943,4944],{"class":399}," 'tree'\n",[371,4946,4947,4950,4952],{"class":373,"line":541},[371,4948,4949],{"class":388},"    ) ",[371,4951,1216],{"class":377},[371,4953,690],{"class":388},[371,4955,4956],{"class":373,"line":546},[371,4957,429],{"emptyLinePlaceholder":428},[371,4959,4960,4963,4966,4968,4970,4972,4975],{"class":373,"line":559},[371,4961,4962],{"class":377},"    const",[371,4964,4965],{"class":381}," oldOid",[371,4967,385],{"class":377},[371,4969,629],{"class":377},[371,4971,4913],{"class":388},[371,4973,4974],{"class":392},"oid",[371,4976,1659],{"class":388},[371,4978,4979,4981,4984,4986,4988,4990,4992],{"class":373,"line":565},[371,4980,4962],{"class":377},[371,4982,4983],{"class":381}," newOid",[371,4985,385],{"class":377},[371,4987,629],{"class":377},[371,4989,4935],{"class":388},[371,4991,4974],{"class":392},[371,4993,1659],{"class":388},[371,4995,4996],{"class":373,"line":573},[371,4997,429],{"emptyLinePlaceholder":428},[371,4999,5000,5002,5005,5007,5010,5012],{"class":373,"line":1166},[371,5001,3430],{"class":377},[371,5003,5004],{"class":388}," (oldOid ",[371,5006,3218],{"class":377},[371,5008,5009],{"class":388}," newOid) ",[371,5011,1216],{"class":377},[371,5013,690],{"class":388},[371,5015,5016,5018,5020,5022,5024,5026,5028,5031,5034],{"class":373,"line":1171},[371,5017,3430],{"class":377},[371,5019,5004],{"class":388},[371,5021,3218],{"class":377},[371,5023,2323],{"class":381},[371,5025,2641],{"class":388},[371,5027,1216],{"class":377},[371,5029,5030],{"class":388}," { filepath, type: ",[371,5032,5033],{"class":399},"'add'",[371,5035,5036],{"class":388},", newOid };\n",[371,5038,5039,5041,5044,5046,5048,5050,5052,5054,5057],{"class":373,"line":1183},[371,5040,3430],{"class":377},[371,5042,5043],{"class":388}," (newOid ",[371,5045,3218],{"class":377},[371,5047,2323],{"class":381},[371,5049,2641],{"class":388},[371,5051,1216],{"class":377},[371,5053,5030],{"class":388},[371,5055,5056],{"class":399},"'remove'",[371,5058,5059],{"class":388},", oldOid };\n",[371,5061,5062],{"class":373,"line":1203},[371,5063,429],{"emptyLinePlaceholder":428},[371,5065,5066,5068,5070,5073],{"class":373,"line":1208},[371,5067,1741],{"class":377},[371,5069,5030],{"class":388},[371,5071,5072],{"class":399},"'modify'",[371,5074,5075],{"class":388},", oldOid, newOid };\n",[371,5077,5078],{"class":373,"line":1213},[371,5079,1106],{"class":388},[371,5081,5082],{"class":373,"line":1790},[371,5083,576],{"class":388},[10,5085,5086,5087,5094],{},"For each changed text file, the worker reads the old and new blobs and uses the\n",[27,5088,5091],{"href":5089,"rel":5090},"https://github.com/kpdecker/jsdiff",[31],[212,5092,5093],{},"diff"," package to create a patch, structured\nhunks, and line statistics. Binary files and blobs larger than 5 MB get metadata\nonly; trying to build a giant text patch would waste memory and produce an\nunusable interface anyway.",[262,5096,5098],{"className":612,"code":5097,"filename":2255,"language":615,"meta":269,"style":269},"const fileIsBinary = isBinary(oldBlob) || isBinary(newBlob);\nconst isTooLarge =\n  (oldBlob?.byteLength ?? 0) > 5e6 ||\n  (newBlob?.byteLength ?? 0) > 5e6;\n\nif (!fileIsBinary && !isTooLarge) {\n  const structured = Diff.structuredPatch(\n    file.filepath,\n    file.filepath,\n    oldText,\n    newText\n  );\n\n  const diffLines = Diff.diffLines(oldText, newText);\n}\n",[212,5099,5100,5121,5131,5148,5165,5169,5189,5206,5211,5215,5220,5225,5229,5233,5250],{"__ignoreMap":269},[371,5101,5102,5104,5107,5109,5111,5114,5116,5118],{"class":373,"line":374},[371,5103,378],{"class":377},[371,5105,5106],{"class":381}," fileIsBinary",[371,5108,385],{"class":377},[371,5110,4414],{"class":392},[371,5112,5113],{"class":388},"(oldBlob) ",[371,5115,1239],{"class":377},[371,5117,4414],{"class":392},[371,5119,5120],{"class":388},"(newBlob);\n",[371,5122,5123,5125,5128],{"class":373,"line":425},[371,5124,378],{"class":377},[371,5126,5127],{"class":381}," isTooLarge",[371,5129,5130],{"class":377}," =\n",[371,5132,5133,5136,5138,5140,5142,5144,5146],{"class":373,"line":432},[371,5134,5135],{"class":388},"  (oldBlob?.byteLength ",[371,5137,2994],{"class":377},[371,5139,3221],{"class":381},[371,5141,2641],{"class":388},[371,5143,4652],{"class":377},[371,5145,4655],{"class":381},[371,5147,4926],{"class":377},[371,5149,5150,5153,5155,5157,5159,5161,5163],{"class":373,"line":447},[371,5151,5152],{"class":388},"  (newBlob?.byteLength ",[371,5154,2994],{"class":377},[371,5156,3221],{"class":381},[371,5158,2641],{"class":388},[371,5160,4652],{"class":377},[371,5162,4655],{"class":381},[371,5164,690],{"class":388},[371,5166,5167],{"class":373,"line":462},[371,5168,429],{"emptyLinePlaceholder":428},[371,5170,5171,5173,5175,5177,5180,5183,5186],{"class":373,"line":468},[371,5172,435],{"class":377},[371,5174,981],{"class":388},[371,5176,984],{"class":377},[371,5178,5179],{"class":388},"fileIsBinary ",[371,5181,5182],{"class":377},"&&",[371,5184,5185],{"class":377}," !",[371,5187,5188],{"class":388},"isTooLarge) {\n",[371,5190,5191,5193,5196,5198,5201,5204],{"class":373,"line":477},[371,5192,992],{"class":377},[371,5194,5195],{"class":381}," structured",[371,5197,385],{"class":377},[371,5199,5200],{"class":388}," Diff.",[371,5202,5203],{"class":392},"structuredPatch",[371,5205,638],{"class":388},[371,5207,5208],{"class":373,"line":483},[371,5209,5210],{"class":388},"    file.filepath,\n",[371,5212,5213],{"class":373,"line":495},[371,5214,5210],{"class":388},[371,5216,5217],{"class":373,"line":523},[371,5218,5219],{"class":388},"    oldText,\n",[371,5221,5222],{"class":373,"line":530},[371,5223,5224],{"class":388},"    newText\n",[371,5226,5227],{"class":373,"line":535},[371,5228,1271],{"class":388},[371,5230,5231],{"class":373,"line":541},[371,5232,429],{"emptyLinePlaceholder":428},[371,5234,5235,5237,5240,5242,5244,5247],{"class":373,"line":546},[371,5236,992],{"class":377},[371,5238,5239],{"class":381}," diffLines",[371,5241,385],{"class":377},[371,5243,5200],{"class":388},[371,5245,5246],{"class":392},"diffLines",[371,5248,5249],{"class":388},"(oldText, newText);\n",[371,5251,5252],{"class":373,"line":559},[371,5253,538],{"class":388},[10,5255,5256,5257,5260],{},"The worker processes changed files sequentially and yields to its event loop\nafter every ten files. The earlier implementation launched the whole batch with\n",[212,5258,5259],{},"Promise.all",", which traded a shorter-looking function for an uncontrolled burst\nof blob reads and diff work.",[10,5262,5263],{},"The finished result is cached by the old and new commit hashes. Reopening the\nsame commit can reuse its file classification, hunks, and aggregate additions\nand deletions without walking both trees again.",[10,5265,5266,5267,5270],{},"The UI derives both unified and split rows from the same hunks. Consecutive\nremoved and added lines are paired, then ",[212,5268,5269],{},"Diff.diffChars()"," adds the darker\ncharacter-level highlights inside the normal red and green lines.",[355,5272,5274],{"id":5273},"combining-syntax-highlighting-with-diff-highlighting","Combining syntax highlighting with diff highlighting",[10,5276,5277],{},"A plain diff is easy to color. A syntax-highlighted diff with character-level\nchanges is more awkward because two systems want to control the same text.",[10,5279,5280],{},"Shiki knows that a token is a keyword, string, or comment. The diff algorithm\nknows that characters 14 through 22 changed. If I let either renderer replace\nthe other, I lose useful information.",[10,5282,5283],{},"The solution is to highlight the complete old and new source files with Shiki,\nextract the HTML for each line, and weave the diff ranges through those tokens.\nThe merge function consumes the syntax token text and diff segments together,\nwrapping changed chunks while preserving Shiki's token attributes.",[262,5285,5287],{"className":612,"code":5286,"filename":2847,"language":615,"meta":269,"style":269},"const charsToConsume = Math.min(tokenCharsLeft, wordCharsLeft);\nconst chunk = token.text.substring(\n  tokenOffset,\n  tokenOffset + charsToConsume\n);\n\nlet chunkHtml = escapeHtml(chunk);\n\nif (token.attrs) {\n  chunkHtml = `\u003Cspan ${token.attrs}>${chunkHtml}\u003C/span>`;\n}\n\nif (isTargetChange) {\n  chunkHtml = `\u003Cspan class=\"${bgClass}\">${chunkHtml}\u003C/span>`;\n}\n",[212,5288,5289,5307,5323,5328,5339,5343,5347,5362,5366,5373,5402,5406,5410,5417,5438],{"__ignoreMap":269},[371,5290,5291,5293,5296,5298,5301,5304],{"class":373,"line":374},[371,5292,378],{"class":377},[371,5294,5295],{"class":381}," charsToConsume",[371,5297,385],{"class":377},[371,5299,5300],{"class":388}," Math.",[371,5302,5303],{"class":392},"min",[371,5305,5306],{"class":388},"(tokenCharsLeft, wordCharsLeft);\n",[371,5308,5309,5311,5314,5316,5319,5321],{"class":373,"line":425},[371,5310,378],{"class":377},[371,5312,5313],{"class":381}," chunk",[371,5315,385],{"class":377},[371,5317,5318],{"class":388}," token.text.",[371,5320,1645],{"class":392},[371,5322,638],{"class":388},[371,5324,5325],{"class":373,"line":432},[371,5326,5327],{"class":388},"  tokenOffset,\n",[371,5329,5330,5333,5336],{"class":373,"line":447},[371,5331,5332],{"class":388},"  tokenOffset ",[371,5334,5335],{"class":377},"+",[371,5337,5338],{"class":388}," charsToConsume\n",[371,5340,5341],{"class":373,"line":462},[371,5342,422],{"class":388},[371,5344,5345],{"class":373,"line":468},[371,5346,429],{"emptyLinePlaceholder":428},[371,5348,5349,5351,5354,5356,5359],{"class":373,"line":477},[371,5350,3829],{"class":377},[371,5352,5353],{"class":388}," chunkHtml ",[371,5355,2199],{"class":377},[371,5357,5358],{"class":392}," escapeHtml",[371,5360,5361],{"class":388},"(chunk);\n",[371,5363,5364],{"class":373,"line":483},[371,5365,429],{"emptyLinePlaceholder":428},[371,5367,5368,5370],{"class":373,"line":495},[371,5369,435],{"class":377},[371,5371,5372],{"class":388}," (token.attrs) {\n",[371,5374,5375,5378,5380,5383,5386,5388,5391,5394,5397,5400],{"class":373,"line":523},[371,5376,5377],{"class":388},"  chunkHtml ",[371,5379,2199],{"class":377},[371,5381,5382],{"class":399}," `\u003Cspan ${",[371,5384,5385],{"class":388},"token",[371,5387,332],{"class":399},[371,5389,5390],{"class":388},"attrs",[371,5392,5393],{"class":399},"}>${",[371,5395,5396],{"class":388},"chunkHtml",[371,5398,5399],{"class":399},"}\u003C/span>`",[371,5401,690],{"class":388},[371,5403,5404],{"class":373,"line":530},[371,5405,538],{"class":388},[371,5407,5408],{"class":373,"line":535},[371,5409,429],{"emptyLinePlaceholder":428},[371,5411,5412,5414],{"class":373,"line":541},[371,5413,435],{"class":377},[371,5415,5416],{"class":388}," (isTargetChange) {\n",[371,5418,5419,5421,5423,5426,5429,5432,5434,5436],{"class":373,"line":546},[371,5420,5377],{"class":388},[371,5422,2199],{"class":377},[371,5424,5425],{"class":399}," `\u003Cspan class=\"${",[371,5427,5428],{"class":388},"bgClass",[371,5430,5431],{"class":399},"}\">${",[371,5433,5396],{"class":388},[371,5435,5399],{"class":399},[371,5437,690],{"class":388},[371,5439,5440],{"class":373,"line":559},[371,5441,538],{"class":388},[10,5443,5444,5445,5448,5449,5452],{},"Shiki runs behind ",[212,5446,5447],{},"/api/highlight",". That endpoint lazily creates one highlighter\nwith the site's supported languages, keeps up to 500 generated documents in a\nper-instance memory cache, and returns an empty result instead of breaking the\nwhole diff if highlighting fails. Because the client sends a ",[212,5450,5451],{},"POST",", the\nresponse's immutable cache header is not a dependable browser or CDN cache.",[10,5454,5455],{},"This creates a deliberately hybrid boundary: Git object parsing and diff\ncalculation happen in the browser worker, while syntax highlighting happens in\na small cached server route. The source is already public repository data, so\nthe request doesn't cross a new privacy boundary for this use case.",[355,5457,5459],{"id":5458},"rendering-only-what-the-reader-can-use","Rendering only what the reader can use",[10,5461,5462],{},"A commit may change hundreds of files. Creating a Vue component, two highlighted\ndocuments, and thousands of line rows for every file at once would undo the\nworker's performance gains.",[10,5464,5465],{},"The diff viewer initially renders six files. An intersection observer increases\nthe visible slice as the reader approaches the bottom, while URL hashes can\nexpand the slice far enough to reach a selected file from the sidebar.",[10,5467,5468],{},"This optimization controls DOM size and highlighting work. The worker still\ncalculates the changed-file data for the complete commit before the first diff\ncard appears.",[10,5470,5471],{},"Mobile devices default to the unified view because a split diff needs horizontal\nspace. Readers can switch layouts, wrap long lines, collapse files, or request\nthe full-file context only when a diff card is expanded.",[10,5473,5474],{},"The theme here is the same as the tree metadata: calculate and render expensive\ndetails when they become useful, not merely because they exist.",[59,5476,5478],{"id":5477},"nuxt-is-still-the-application-framework","Nuxt is still the application framework",[10,5480,5481],{},"For all the unusual machinery in the middle, I did not want the repository\nviewer to become a separate application hidden inside the portfolio. The\nsurrounding application is normal Nuxt: typed Nuxt Content collections,\nfile-based routing, layouts, composables, and Nuxt UI.",[10,5483,5484],{},"The repository route selects one of three layout styles:",[70,5486,5487,5490,5493],{},[73,5488,5489],{},"The default portfolio layout for repository overviews and history.",[73,5491,5492],{},"A resizable tree layout for directories and files.",[73,5494,5495],{},"A diff layout whose sidebar lists files changed by the selected commit.",[10,5497,5498],{},"The tree and diff layouts provide the shared repository controller to child\ncomponents. File viewers, branch selectors, breadcrumbs, and action menus all\nconsume the same reactive state instead of creating their own worker.",[10,5500,5501],{},"A reader can select a ref, search the complete file tree, open a file, inspect\nits last commit, toggle raw Markdown, or download a snapshot. The history view\nfilters locally by author and date, and a commit can reopen either the exact\nrepository snapshot or its changed-file sidebar and split or unified diff.",[10,5503,5504,5505,5508],{},"Nuxt UI supplies the shell and responsive controls, while Nuxt Content's\n",[212,5506,5507],{},"\u003CMDC>"," renderer gives repository Markdown the same prose styling as the rest of\nthe site. The framework pieces make the browser Git engine feel like part of\nthe portfolio instead of a second application mounted beside it.",[10,5510,5511],{},"This split also keeps the regular site cheap. Someone reading an article never\ninitializes LightningFS, downloads a packfile, or starts a Git worker. The heavy\nclient architecture only exists under repository routes.",[59,5513,5515],{"id":5514},"why-this-boundary-keeps-the-feature-cheap","Why this boundary keeps the feature cheap",[10,5517,5518],{},"This design fits my portfolio because the repositories are curated,\npublic, and read-only. More importantly, each layer owns one narrow\nresponsibility instead of sharing a second repository model across the stack.",[70,5520,5521,5527,5533,5539,5545],{},[73,5522,5523,5526],{},[3581,5524,5525],{},"GitHub Actions prepares bare copies."," It runs native Git and publishes the\nstatic metadata used by the transport.",[73,5528,5529,5532],{},[3581,5530,5531],{},"R2 or local storage owns bytes."," Neither storage mode needs to interpret a\ncommit, tree, or blob.",[73,5534,5535,5538],{},[3581,5536,5537],{},"Nitro owns transport."," The proxy provides one same-origin URL and applies\nmutability-aware cache rules to production R2 responses.",[73,5540,5541,5544],{},[3581,5542,5543],{},"The worker owns Git."," It reconstructs the repository and derives the data\nstructures required by the interface.",[73,5546,5547,5550],{},[3581,5548,5549],{},"Nuxt and Vue own interaction."," Routes, layouts, composables, and components\nturn worker results into a navigable application.",[10,5552,5553],{},"No synchronization job converts every file and commit into database rows, and\nno read endpoint invents a second tree or history schema. Publishing updates\nthe native Git data; the browser interprets that data when a reader needs it.",[10,5555,5556],{},"The work that requires native Git runs during synchronization, after a\nrepository changes, rather than inside an always-on service. Runtime reads are\nobject storage requests through a thin edge route, while the visitor's worker\nperforms the repository queries. At this portfolio's current traffic and\nstorage footprint, the design can remain inside the Cloudflare allowances I was\nalready using.",[10,5558,5559,5560,5565,5566,5571],{},"That is not the same as claiming the architecture is free in every context.\n",[27,5561,5564],{"href":5562,"rel":5563},"https://developers.cloudflare.com/r2/pricing/",[31],"R2 does not charge for egress",",\nbut storage, operations, and\n",[27,5567,5570],{"href":5568,"rel":5569},"https://developers.cloudflare.com/pages/functions/pricing/",[31],"function invocations","\nare still metered beyond their allowances. Browser CPU and development time\nalso exist. The point is that I do not operate a dedicated Git-aware server\nbetween visits.",[10,5573,5574],{},"Repeat visits also benefit from where the state lives. IndexedDB preserves the\nrepository, in-memory caches preserve derived objects, and the Vue facade\nsurvives navigation between overview, tree, blob, history, and diff routes.",[59,5576,5578],{"id":5577},"read-only-does-not-mean-private","Read-only does not mean private",[10,5580,5581],{},"The viewer's security depends on every published repository being public.\nRead-only prevents browser writes, but it does not turn raw Git objects into an\naccess-control system.",[10,5583,5584,5585,5587],{},"The browser never receives R2 or GitHub credentials, and the bucket remains\nbehind its Cloudflare binding. The public Nitro proxy can still expose any\nuploaded file or object key beneath its fixed ",[212,5586,897],{}," prefix; it does not apply\na repository allowlist. A self-hosted bare repository must therefore avoid\ncredential-bearing remote URLs in its configuration.",[10,5589,5590,5591,5593],{},"The project collection is a catalog, not authorization. It decides which\nrepositories receive a polished route, but it does not protect the storage\nprefix. The webhook also ignores a repository after it becomes private instead\nof retracting its old copy, and ",[212,5592,913],{}," does not remove destination-only\nfiles. A previously public or deleted repository remains downloadable until its\npublished prefix is explicitly purged.",[10,5595,5596],{},"The webhook signature protects the automation path from arbitrary workflow\ndispatches. It does not change the read path, which is intentionally public and\ncacheable.",[59,5598,5600],{"id":5599},"limitations","Limitations",[10,5602,5603],{},"This architecture moves server work to the client; it does not make that work\ndisappear. The visitor pays in bandwidth, IndexedDB storage, memory, and CPU.",[10,5605,5606],{},"The main limitations are:",[70,5608,5609,5612,5619,5622,5625,5628,5633,5636],{},[73,5610,5611],{},"A first visit downloads complete packfiles, even if the reader opens one file.",[73,5613,5614,5615,5618],{},"Each response becomes a complete ",[212,5616,5617],{},"ArrayBuffer"," before its IndexedDB write, so\npeak first-load memory grows with the largest pack as well as the parsed Git\ndata.",[73,5620,5621],{},"Very large histories make uncapped history and path searches expensive.",[73,5623,5624],{},"Very large file counts also matter: initialization transfers the complete\npath list and Vue builds an in-memory navigation and search model from it.",[73,5626,5627],{},"The 1,000-commit tree scan can leave old entries without an immediate commit\nlabel.",[73,5629,5630,5632],{},[212,5631,913],{}," does not delete obsolete pack objects or removed repositories\nfrom R2, so published storage can accumulate too.",[73,5634,5635],{},"Browser storage can be cleared, evicted, or corrupted.",[73,5637,5638],{},"It has no server-side search index, blame engine, or cross-repository query\nsystem.",[10,5640,5641],{},"Some Git semantics are simplified too. Rename detection is not implemented, so\na rename appears as one removed path and one added path, and Git LFS\nobjects are not fetched from their external storage.",[10,5643,5644],{},"The separately published Nuxt and cURL showcase copies are useful stress tests.\nThey show that the design can handle repositories much larger than my own, but\nthey also make the compromises visible. The automation shown above excludes\nforks, so those repositories are prepared outside that discovery path.\nSomething enormous like QEMU or the Linux kernel belongs behind cgit, or a proper\nGit service.",[59,5646,5648],{"id":5647},"finally","Finally",[10,5650,5651,5652,5655],{},"I did not begin this project with a grand theory about browser-native Git. I saw\nhow naturally cgit made repositories part of ",[27,5653,32],{"href":29,"rel":5654},[31],", looked at the outbound\nGitHub links in my own portfolio, and wanted that experience here without\nadding infrastructure I would have to keep paying for and maintaining.",[10,5657,5658],{},"I did not recreate cgit either. cgit keeps Git beside the server process and\nrenders repository views there. This viewer keeps Git in object storage, moves\nthe database into IndexedDB, and asks a worker to render those same concepts in\nthe browser. The shared idea is simpler than either implementation: the\nrepository should be the center of the experience.",[10,5660,5661],{},"The biggest architectural simplification came from the most basic discovery.\nOnce I understood that files, folders, histories, and diffs were already views\nover Git's refs, commits, trees, and blobs, a custom repository API stopped\nlooking necessary. I could publish the original data model instead of inventing\nanother one.",[10,5663,5664],{},"That realization removed an application API; it did not remove the\nimplementation work. The worker was not enough on its own. I still had to stop\nrunning one history query per tree row, compare tree object IDs, cap broad\nscans, publish metadata progressively, cache at several layers, reject files\nthe browser cannot render well, and delay diff DOM work until someone can see\nit. The object model made the design possible; the less glamorous boundaries\nmade it usable.",[10,5666,5667],{},"Most importantly, the project now does the thing I genuinely wanted. A project\ncard no longer has to eject a curious reader at the most interesting moment.\nThe README, source tree, branches, history, and diffs can all remain part of the\nstory this portfolio is telling.",[10,5669,5670],{},"It is not a replacement for GitHub, and it is definitely not the right viewer\nfor every repository. It is my small, read-only answer to the question that\nstarted the project: how much Git infrastructure does a portfolio actually\nneed?",[10,5672,5673],{},"You can inspect the portfolio with the viewer itself, then compare it with the\nlarger Nuxt and cURL showcase copies to see both the design and its limits in\npractice.",[5675,5676,5682,5689,5696],"div",{"className":5677},[5678,5679,5680,5681],"flex","flex-col","sm:flex-row","gap-3",[5683,5684],"u-button",{":block":3152,"className":5685,"icon":5687,"label":5688,"to":306},[5686],"sm:w-auto","i-lucide-git-branch","Browse the portfolio source",[5683,5690],{":block":3152,"className":5691,"icon":5692,"label":5693,"to":5694,"variant":5695},[5686],"i-lucide-tree-pine","Open the Nuxt repository","/repo/nuxt","subtle",[5683,5697],{":block":3152,"className":5698,"icon":5699,"label":5700,"to":5701,"variant":5695},[5686],"i-lucide-terminal","Open the cURL repository","/repo/curl",[5703,5704,5705],"style",{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sByVh, html code.shiki .sByVh{--shiki-light:#22863A;--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":269,"searchDepth":425,"depth":425,"links":5707},[5708,5709,5710,5711,5712,5716,5717,5720,5721,5722,5725,5726,5727,5728,5729,5730,5734,5735,5736,5737,5738],{"id":61,"depth":425,"text":62},{"id":102,"depth":425,"text":103},{"id":225,"depth":425,"text":226},{"id":283,"depth":425,"text":284},{"id":322,"depth":425,"text":323,"children":5713},[5714,5715],{"id":357,"depth":432,"text":358},{"id":598,"depth":432,"text":599},{"id":921,"depth":425,"text":922},{"id":1345,"depth":425,"text":1346,"children":5718},[5719],{"id":1925,"depth":432,"text":1926},{"id":2223,"depth":425,"text":2224},{"id":2713,"depth":425,"text":2714},{"id":2899,"depth":425,"text":2900,"children":5723},[5724],{"id":3180,"depth":432,"text":3181},{"id":3590,"depth":425,"text":3591},{"id":3913,"depth":425,"text":3914},{"id":4235,"depth":425,"text":4236},{"id":4389,"depth":425,"text":4390},{"id":4741,"depth":425,"text":4742},{"id":4761,"depth":425,"text":4762,"children":5731},[5732,5733],{"id":5273,"depth":432,"text":5274},{"id":5458,"depth":432,"text":5459},{"id":5477,"depth":425,"text":5478},{"id":5514,"depth":425,"text":5515},{"id":5577,"depth":425,"text":5578},{"id":5599,"depth":425,"text":5600},{"id":5647,"depth":425,"text":5648},"15/07/2026","How I built a repository viewer with bare Git repositories, Cloudflare R2, IndexedDB, isomorphic-git, and a Web Worker.","md",null,{},"/articles/git-repo","40",{"title":5,"description":5740},{"loc":5744},"articles/00.git-repo",[5750,5751,5752,5753,5754],"Git","Architecture","Nuxt","Cloudflare","Web Workers","4PhcWVK4mTpUYR4vK9FoweMAJLbWbEZsBHF36QUyidM",1784306721699]