git-drive 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +77 -0
- package/.planning/codebase/ARCHITECTURE.md +151 -0
- package/.planning/codebase/CONCERNS.md +191 -0
- package/.planning/codebase/CONVENTIONS.md +169 -0
- package/.planning/codebase/INTEGRATIONS.md +94 -0
- package/.planning/codebase/STACK.md +77 -0
- package/.planning/codebase/STRUCTURE.md +157 -0
- package/.planning/codebase/TESTING.md +156 -0
- package/Dockerfile.cli +30 -0
- package/Dockerfile.server +32 -0
- package/README.md +95 -0
- package/docker-compose.yml +48 -0
- package/package.json +25 -0
- package/packages/cli/Dockerfile +26 -0
- package/packages/cli/package.json +57 -0
- package/packages/cli/src/commands/archive.ts +39 -0
- package/packages/cli/src/commands/init.ts +34 -0
- package/packages/cli/src/commands/link.ts +115 -0
- package/packages/cli/src/commands/list.ts +94 -0
- package/packages/cli/src/commands/push.ts +64 -0
- package/packages/cli/src/commands/restore.ts +36 -0
- package/packages/cli/src/commands/status.ts +127 -0
- package/packages/cli/src/config.ts +73 -0
- package/packages/cli/src/errors.ts +23 -0
- package/packages/cli/src/git.ts +55 -0
- package/packages/cli/src/index.ts +97 -0
- package/packages/cli/src/server.ts +514 -0
- package/packages/cli/tsconfig.json +13 -0
- package/packages/cli/ui/assets/index-Cc2q1t5k.js +17 -0
- package/packages/cli/ui/assets/index-DrL7ojPA.css +1 -0
- package/packages/cli/ui/index.html +14 -0
- package/packages/cli/ui/vite.svg +1 -0
- package/packages/git-drive-docker/package.json +15 -0
- package/packages/server/package.json +44 -0
- package/packages/server/src/index.ts +569 -0
- package/packages/server/tsconfig.json +9 -0
- package/packages/ui/README.md +73 -0
- package/packages/ui/eslint.config.js +23 -0
- package/packages/ui/index.html +13 -0
- package/packages/ui/package.json +42 -0
- package/packages/ui/postcss.config.js +6 -0
- package/packages/ui/public/vite.svg +1 -0
- package/packages/ui/src/App.css +23 -0
- package/packages/ui/src/App.tsx +726 -0
- package/packages/ui/src/assets/react.svg +8 -0
- package/packages/ui/src/assets/vite.svg +3 -0
- package/packages/ui/src/index.css +37 -0
- package/packages/ui/src/main.tsx +14 -0
- package/packages/ui/tailwind.config.js +11 -0
- package/packages/ui/tsconfig.app.json +28 -0
- package/packages/ui/tsconfig.json +26 -0
- package/packages/ui/tsconfig.node.json +12 -0
- package/packages/ui/vite.config.ts +7 -0
- package/pnpm-workspace.yaml +4 -0
- package/rewrite_app.js +731 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 222.2 84% 4.9%;--card: 0 0% 100%;--card-foreground: 222.2 84% 4.9%;--popover: 0 0% 100%;--popover-foreground: 222.2 84% 4.9%;--primary: 222.2 47.4% 11.2%;--primary-foreground: 210 40% 98%;--secondary: 210 40% 96.1%;--secondary-foreground: 222.2 47.4% 11.2%;--muted: 210 40% 96.1%;--muted-foreground: 215.4 16.3% 46.9%;--accent: 210 40% 96.1%;--accent-foreground: 222.2 47.4% 11.2%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 210 40% 98%;--border: 214.3 31.8% 91.4%;--input: 214.3 31.8% 91.4%;--ring: 222.2 84% 4.9%;--radius: .5rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.grid{display:grid}.h-10{height:2.5rem}.h-12{height:3rem}.h-20{height:5rem}.h-24{height:6rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-fit{height:-moz-fit-content;height:fit-content}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-10{min-width:2.5rem}.min-w-12{min-width:3rem}.max-w-5xl{max-width:64rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-800\/50>:not([hidden])~:not([hidden]){border-color:#1f293780}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-clip{text-overflow:clip}.whitespace-normal{white-space:normal}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-amber-500\/20{border-color:#f59e0b33}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-green-500\/20{border-color:#22c55e33}.bg-\[\#0d1117\]{--tw-bg-opacity: 1;background-color:rgb(13 17 23 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/5{background-color:#3b82f60d}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-600\/20{background-color:#2563eb33}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-800\/80{background-color:#1f2937cc}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/50{background-color:#11182780}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-red-500\/10{background-color:#ef44441a}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.fill-blue-400\/20{fill:#60a5fa33}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pl-1{padding-left:.25rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-widest{letter-spacing:.1em}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.placeholder-gray-600::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(75 85 99 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-600::placeholder{--tw-placeholder-opacity: 1;color:rgb(75 85 99 / var(--tw-placeholder-opacity, 1))}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/20{--tw-shadow-color: rgb(0 0 0 / .2);--tw-shadow: var(--tw-shadow-colored)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:border-gray-700:hover{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800\/30:hover{background-color:#1f29374d}.hover\:bg-gray-800\/50:hover{background-color:#1f293780}.hover\:bg-red-500\/10:hover{background-color:#ef44441a}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-500:focus{--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:border-blue-500\/30{border-color:#3b82f64d}.group:hover .group-hover\:bg-blue-500\/10{background-color:#3b82f61a}.group:hover .group-hover\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.group:hover .group-hover\:text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:opacity-100{opacity:1}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Vite + React + TS</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Cc2q1t5k.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DrL7ojPA.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-drive-docker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dockerized Git Drive environment and CLI wrapper",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build:images": "docker compose build",
|
|
8
|
+
"up": "docker compose up -d",
|
|
9
|
+
"down": "docker compose down",
|
|
10
|
+
"cli": "docker compose run git-drive-cli"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC"
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-drive-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server component for git-drive",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git",
|
|
7
|
+
"backup",
|
|
8
|
+
"server"
|
|
9
|
+
],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/josmanvis/git-drive.git",
|
|
15
|
+
"directory": "packages/server"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/josmanvis/git-drive/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/josmanvis/git-drive#readme",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"start": "node dist/index.js",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"express": "^4.19.2",
|
|
34
|
+
"node-disk-info": "^1.3.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/express": "^4.17.21",
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.7.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { readdirSync, existsSync, mkdirSync, statSync, readFileSync } from 'fs';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { getDiskInfo } from 'node-disk-info';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const app = express();
|
|
13
|
+
const port = 4483;
|
|
14
|
+
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
app.use(express.static(path.join(__dirname, '../../../packages/ui/dist')));
|
|
17
|
+
|
|
18
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function git(args: string, cwd?: string): string {
|
|
21
|
+
return execSync(`git ${args}`, {
|
|
22
|
+
cwd,
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
+
}).trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getGitDrivePath(mountpoint: string): string {
|
|
29
|
+
return path.join(mountpoint, '.git-drive');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureGitDriveDir(mountpoint: string): string {
|
|
33
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
34
|
+
if (!existsSync(gitDrivePath)) {
|
|
35
|
+
try {
|
|
36
|
+
execSync(`mkdir -p "${gitDrivePath}"`);
|
|
37
|
+
} catch (err: any) {
|
|
38
|
+
throw new Error(`Failed to write to drive. Please ensure Terminal/Node has "Removable Volumes" access in macOS Privacy settings. Details: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return gitDrivePath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listRepos(gitDrivePath: string): Array<{ name: string; path: string; lastModified: string }> {
|
|
45
|
+
if (!existsSync(gitDrivePath)) return [];
|
|
46
|
+
|
|
47
|
+
return readdirSync(gitDrivePath)
|
|
48
|
+
.filter((entry) => {
|
|
49
|
+
const entryPath = path.join(gitDrivePath, entry);
|
|
50
|
+
// Accept both bare repos (name.git) and directories with HEAD file
|
|
51
|
+
return (
|
|
52
|
+
statSync(entryPath).isDirectory() &&
|
|
53
|
+
(entry.endsWith('.git') || existsSync(path.join(entryPath, 'HEAD')))
|
|
54
|
+
);
|
|
55
|
+
})
|
|
56
|
+
.map((entry) => {
|
|
57
|
+
const entryPath = path.join(gitDrivePath, entry);
|
|
58
|
+
const stat = statSync(entryPath);
|
|
59
|
+
return {
|
|
60
|
+
name: entry.replace(/\.git$/, ''),
|
|
61
|
+
path: entryPath,
|
|
62
|
+
lastModified: stat.mtime.toISOString(),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadLinks(): Record<string, { mountpoint: string; repoName: string; linkedAt: string }> {
|
|
68
|
+
const linksFile = path.join(homedir(), '.config', 'git-drive', 'links.json');
|
|
69
|
+
if (!existsSync(linksFile)) return {};
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(linksFile, 'utf-8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── API Routes ───────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
// List all connected drives
|
|
80
|
+
app.get('/api/drives', async (_req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const drives = await getDiskInfo();
|
|
83
|
+
const result = drives
|
|
84
|
+
.filter((d: any) => {
|
|
85
|
+
const mp = d.mounted;
|
|
86
|
+
if (!mp) return false;
|
|
87
|
+
if (mp === "/" || mp === "100%") return false;
|
|
88
|
+
|
|
89
|
+
if (process.platform === "darwin") {
|
|
90
|
+
return mp.startsWith("/Volumes/") && !mp.startsWith("/Volumes/Recovery");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (mp.startsWith("/sys") || mp.startsWith("/proc") || mp.startsWith("/run") || mp.startsWith("/snap") || mp.startsWith("/boot")) return false;
|
|
94
|
+
if (d.filesystem === "tmpfs" || d.filesystem === "devtmpfs" || d.filesystem === "udev" || d.filesystem === "overlay") return false;
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
})
|
|
98
|
+
.map((d: any) => ({
|
|
99
|
+
device: d.filesystem,
|
|
100
|
+
description: d.mounted,
|
|
101
|
+
size: d.blocks ? parseInt(d.blocks) * 1024 : 0,
|
|
102
|
+
isRemovable: true, // simplified assumption
|
|
103
|
+
isSystem: d.mounted === '/',
|
|
104
|
+
mountpoints: [d.mounted],
|
|
105
|
+
hasGitDrive: existsSync(getGitDrivePath(d.mounted)),
|
|
106
|
+
}));
|
|
107
|
+
res.json(result);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
res.status(500).json({ error: 'Failed to list drives' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// List repos on a specific drive
|
|
114
|
+
app.get('/api/drives/:mountpoint/repos', (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
117
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
118
|
+
|
|
119
|
+
if (!existsSync(mountpoint)) {
|
|
120
|
+
res.status(404).json({ error: 'Drive not found or not mounted' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const repos = listRepos(gitDrivePath);
|
|
125
|
+
res.json({
|
|
126
|
+
mountpoint,
|
|
127
|
+
gitDrivePath,
|
|
128
|
+
initialized: existsSync(gitDrivePath),
|
|
129
|
+
repos,
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
res.status(500).json({ error: 'Failed to list repos' });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Initialize git-drive on a drive (create .git-drive directory)
|
|
137
|
+
app.post('/api/drives/:mountpoint/init', (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
140
|
+
|
|
141
|
+
if (!existsSync(mountpoint)) {
|
|
142
|
+
res.status(404).json({ error: 'Drive not found or not mounted' });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const gitDrivePath = ensureGitDriveDir(mountpoint);
|
|
147
|
+
res.json({
|
|
148
|
+
mountpoint,
|
|
149
|
+
gitDrivePath,
|
|
150
|
+
message: 'Git Drive initialized on this drive',
|
|
151
|
+
});
|
|
152
|
+
} catch (err: any) {
|
|
153
|
+
console.error("Init Error:", err);
|
|
154
|
+
res.status(500).json({ error: err.message || 'Failed to initialize drive' });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Create a new bare repo on a drive
|
|
159
|
+
app.post('/api/drives/:mountpoint/repos', (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
162
|
+
const { name } = req.body;
|
|
163
|
+
|
|
164
|
+
if (!name || typeof name !== 'string') {
|
|
165
|
+
res.status(400).json({ error: 'Repo name is required' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Sanitize name
|
|
170
|
+
const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
171
|
+
|
|
172
|
+
if (!existsSync(mountpoint)) {
|
|
173
|
+
res.status(404).json({ error: 'Drive not found or not mounted' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const gitDrivePath = ensureGitDriveDir(mountpoint);
|
|
178
|
+
const repoName = safeName.endsWith('.git') ? safeName : `${safeName}.git`;
|
|
179
|
+
const repoPath = path.join(gitDrivePath, repoName);
|
|
180
|
+
|
|
181
|
+
if (existsSync(repoPath)) {
|
|
182
|
+
res.status(409).json({ error: 'Repository already exists' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
git(`init --bare "${repoPath}"`);
|
|
187
|
+
|
|
188
|
+
res.status(201).json({
|
|
189
|
+
name: safeName.replace(/\.git$/, ''),
|
|
190
|
+
path: repoPath,
|
|
191
|
+
message: `Bare repository created: ${repoName}`,
|
|
192
|
+
remoteUrl: repoPath,
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
res.status(500).json({ error: 'Failed to create repository' });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Delete a repo from a drive
|
|
200
|
+
app.delete('/api/drives/:mountpoint/repos/:repoName', (req, res) => {
|
|
201
|
+
try {
|
|
202
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
203
|
+
const repoName = decodeURIComponent(req.params.repoName);
|
|
204
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
205
|
+
|
|
206
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
207
|
+
const repoPath = path.join(gitDrivePath, bareRepoName);
|
|
208
|
+
|
|
209
|
+
if (!existsSync(repoPath)) {
|
|
210
|
+
// Also check without .git suffix
|
|
211
|
+
const altPath = path.join(gitDrivePath, repoName);
|
|
212
|
+
if (!existsSync(altPath)) {
|
|
213
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
execSync(`rm -rf "${altPath}"`);
|
|
217
|
+
} else {
|
|
218
|
+
execSync(`rm -rf "${repoPath}"`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
res.json({ message: `Repository '${repoName}' deleted` });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
res.status(500).json({ error: 'Failed to delete repository' });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Get info about a specific repo
|
|
228
|
+
app.get('/api/drives/:mountpoint/repos/:repoName', (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
231
|
+
const repoName = decodeURIComponent(req.params.repoName);
|
|
232
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
233
|
+
|
|
234
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
235
|
+
let repoPath = path.join(gitDrivePath, bareRepoName);
|
|
236
|
+
|
|
237
|
+
if (!existsSync(repoPath)) {
|
|
238
|
+
repoPath = path.join(gitDrivePath, repoName);
|
|
239
|
+
if (!existsSync(repoPath)) {
|
|
240
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Get branches
|
|
246
|
+
let branches: string[] = [];
|
|
247
|
+
try {
|
|
248
|
+
const branchOutput = git("branch --format='%(refname:short)'", repoPath);
|
|
249
|
+
branches = branchOutput
|
|
250
|
+
.split('\n')
|
|
251
|
+
.map((b) => b.trim().replace(/^'|'$/g, ''))
|
|
252
|
+
.filter(Boolean);
|
|
253
|
+
} catch {
|
|
254
|
+
// Empty repo has no branches
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Get tags
|
|
258
|
+
let tags: string[] = [];
|
|
259
|
+
try {
|
|
260
|
+
const tagOutput = git("tag", repoPath);
|
|
261
|
+
tags = tagOutput
|
|
262
|
+
.split('\n')
|
|
263
|
+
.map((t) => t.trim())
|
|
264
|
+
.filter(Boolean);
|
|
265
|
+
} catch {
|
|
266
|
+
// No tags
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get last commit info
|
|
270
|
+
let lastCommit: { hash: string; message: string; date: string } | null = null;
|
|
271
|
+
try {
|
|
272
|
+
const log = git('log -1 --format="%H|%s|%ci" --all', repoPath);
|
|
273
|
+
if (log) {
|
|
274
|
+
const [hash, message, date] = log.replace(/^"|"$/g, '').split('|');
|
|
275
|
+
lastCommit = { hash, message, date };
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// Empty repo
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const stat = statSync(repoPath);
|
|
282
|
+
|
|
283
|
+
res.json({
|
|
284
|
+
name: repoName.replace(/\.git$/, ''),
|
|
285
|
+
path: repoPath,
|
|
286
|
+
branches,
|
|
287
|
+
tags,
|
|
288
|
+
lastCommit,
|
|
289
|
+
lastModified: stat.mtime.toISOString(),
|
|
290
|
+
remoteUrl: repoPath,
|
|
291
|
+
});
|
|
292
|
+
} catch (err) {
|
|
293
|
+
res.status(500).json({ error: 'Failed to get repo info' });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Check if there are local working directory unpushed changes
|
|
298
|
+
app.get('/api/drives/:mountpoint/repos/:repoName/local-status', (req, res) => {
|
|
299
|
+
try {
|
|
300
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
301
|
+
let repoName = decodeURIComponent(req.params.repoName);
|
|
302
|
+
repoName = repoName.replace(/\.git$/, '');
|
|
303
|
+
|
|
304
|
+
const links = loadLinks();
|
|
305
|
+
let localPath: string | null = null;
|
|
306
|
+
|
|
307
|
+
// Find if this specific drive's repo is globally linked to any local folder on the user's machine
|
|
308
|
+
for (const [p, data] of Object.entries(links)) {
|
|
309
|
+
if (data.mountpoint === mountpoint && data.repoName.replace(/\.git$/, '') === repoName) {
|
|
310
|
+
if (existsSync(p)) {
|
|
311
|
+
localPath = p;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!localPath) {
|
|
318
|
+
res.json({ linked: false });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check git status locally
|
|
323
|
+
let hasChanges = false;
|
|
324
|
+
let unpushed = false;
|
|
325
|
+
try {
|
|
326
|
+
const statusOutput = git('status --porcelain', localPath);
|
|
327
|
+
hasChanges = statusOutput.trim().length > 0;
|
|
328
|
+
|
|
329
|
+
const unpushedOutput = git('log gd/main..HEAD --oneline', localPath); // Assuming main for now
|
|
330
|
+
unpushed = unpushedOutput.trim().length > 0;
|
|
331
|
+
} catch {
|
|
332
|
+
// Ignore git errors if repo is in weird state
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
res.json({
|
|
336
|
+
linked: true,
|
|
337
|
+
localPath,
|
|
338
|
+
hasChanges,
|
|
339
|
+
unpushed
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
res.status(500).json({ error: 'Failed to check local status' });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Remotely push local working directory to git-drive
|
|
347
|
+
app.post('/api/drives/:mountpoint/repos/:repoName/push', (req, res) => {
|
|
348
|
+
try {
|
|
349
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
350
|
+
let repoName = decodeURIComponent(req.params.repoName);
|
|
351
|
+
repoName = repoName.replace(/\.git$/, '');
|
|
352
|
+
|
|
353
|
+
const links = loadLinks();
|
|
354
|
+
let localPath: string | null = null;
|
|
355
|
+
for (const [p, data] of Object.entries(links)) {
|
|
356
|
+
if (data.mountpoint === mountpoint && data.repoName.replace(/\.git$/, '') === repoName) {
|
|
357
|
+
if (existsSync(p)) {
|
|
358
|
+
localPath = p;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!localPath) {
|
|
365
|
+
res.status(404).json({ error: 'Local linked repository not found.' });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
git('push gd --all', localPath);
|
|
370
|
+
git('push gd --tags', localPath);
|
|
371
|
+
|
|
372
|
+
// Save push telemetry
|
|
373
|
+
try {
|
|
374
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
375
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
376
|
+
let repoPath = path.join(gitDrivePath, bareRepoName);
|
|
377
|
+
if (!existsSync(repoPath)) repoPath = path.join(gitDrivePath, repoName);
|
|
378
|
+
|
|
379
|
+
const payload = {
|
|
380
|
+
date: new Date().toISOString(),
|
|
381
|
+
computer: homedir(), // Server relies on os module
|
|
382
|
+
user: process.env.USER || 'local-user',
|
|
383
|
+
localDir: localPath,
|
|
384
|
+
mode: 'web-ui',
|
|
385
|
+
};
|
|
386
|
+
const logFile = path.join(repoPath, "git-drive-pushlog.json");
|
|
387
|
+
const fs = require('fs');
|
|
388
|
+
fs.appendFileSync(logFile, JSON.stringify(payload) + "\n", "utf-8");
|
|
389
|
+
} catch { }
|
|
390
|
+
|
|
391
|
+
res.json({ success: true, message: 'Successfully backed up local code to Git Drive!' });
|
|
392
|
+
} catch (err: any) {
|
|
393
|
+
res.status(500).json({ error: err.message || 'Failed to push' });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Browse repository files tree
|
|
398
|
+
app.get('/api/drives/:mountpoint/repos/:repoName/tree', (req, res) => {
|
|
399
|
+
try {
|
|
400
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
401
|
+
const repoName = decodeURIComponent(req.params.repoName);
|
|
402
|
+
const branch = req.query.branch || 'main'; // Provide a default if they use main
|
|
403
|
+
const treePath = (req.query.path as string) || '';
|
|
404
|
+
|
|
405
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
406
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
407
|
+
let repoPath = path.join(gitDrivePath, bareRepoName);
|
|
408
|
+
|
|
409
|
+
if (!existsSync(repoPath)) {
|
|
410
|
+
repoPath = path.join(gitDrivePath, repoName);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Resolves default branch if passed "HEAD" or empty logic. Safely ask git for HEAD branch:
|
|
414
|
+
let targetBranch = branch as string;
|
|
415
|
+
if (!targetBranch) {
|
|
416
|
+
try {
|
|
417
|
+
const branchOutput = git('branch --show-current', repoPath);
|
|
418
|
+
targetBranch = branchOutput || 'HEAD';
|
|
419
|
+
} catch {
|
|
420
|
+
targetBranch = 'main';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// git ls-tree
|
|
425
|
+
const target = treePath ? `${targetBranch}:${treePath}` : targetBranch;
|
|
426
|
+
const output = git(`ls-tree ${target}`, repoPath);
|
|
427
|
+
|
|
428
|
+
const files = output.split('\n').filter(Boolean).map((line) => {
|
|
429
|
+
// 040000 tree <hash>\t<path>
|
|
430
|
+
// 100644 blob <hash>\t<path>
|
|
431
|
+
const parts = line.split('\t');
|
|
432
|
+
const meta = parts[0].split(' ');
|
|
433
|
+
return {
|
|
434
|
+
mode: meta[0],
|
|
435
|
+
type: meta[1],
|
|
436
|
+
hash: meta[2],
|
|
437
|
+
path: parts[1],
|
|
438
|
+
name: parts[1].split('/').pop(),
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
res.json({ files });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
res.json({ files: [] }); // Typically happens if repo has zero commits
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Get combined git commit history and git-drive push logs
|
|
449
|
+
app.get('/api/drives/:mountpoint/repos/:repoName/commits', (req, res) => {
|
|
450
|
+
try {
|
|
451
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
452
|
+
const repoName = decodeURIComponent(req.params.repoName);
|
|
453
|
+
const branch = req.query.branch || 'main'; // Using branch filters
|
|
454
|
+
|
|
455
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
456
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
457
|
+
let repoPath = path.join(gitDrivePath, bareRepoName);
|
|
458
|
+
|
|
459
|
+
if (!existsSync(repoPath)) {
|
|
460
|
+
repoPath = path.join(gitDrivePath, repoName);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Git commits log
|
|
464
|
+
let commits: any[] = [];
|
|
465
|
+
try {
|
|
466
|
+
// hash|authorName|authorEmail|message|date
|
|
467
|
+
const logOutput = git(`log ${branch} -n 100 --format="%H|%an|%ae|%s|%ci"`, repoPath);
|
|
468
|
+
commits = logOutput
|
|
469
|
+
.split('\n')
|
|
470
|
+
.filter(Boolean)
|
|
471
|
+
.map((line) => {
|
|
472
|
+
const [hash, author, email, message, date] = line.split('|');
|
|
473
|
+
return { hash, author, email, message, date };
|
|
474
|
+
});
|
|
475
|
+
} catch (e) {
|
|
476
|
+
// Empty repo or invalid branch
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Git drive push logs overlay
|
|
480
|
+
let pushLogs: any[] = [];
|
|
481
|
+
try {
|
|
482
|
+
const logFile = path.join(repoPath, "git-drive-pushlog.json");
|
|
483
|
+
if (existsSync(logFile)) {
|
|
484
|
+
const rawLogs = require('fs').readFileSync(logFile, "utf-8").trim().split('\n');
|
|
485
|
+
pushLogs = rawLogs.map((l: string) => {
|
|
486
|
+
try {
|
|
487
|
+
return JSON.parse(l);
|
|
488
|
+
} catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}).filter(Boolean);
|
|
492
|
+
pushLogs.reverse(); // Newest first
|
|
493
|
+
}
|
|
494
|
+
} catch { }
|
|
495
|
+
|
|
496
|
+
res.json({ commits, pushLogs });
|
|
497
|
+
} catch (err) {
|
|
498
|
+
res.status(500).json({ error: 'Failed to retrieve history' });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Get single commit details (patch/diff)
|
|
503
|
+
app.get('/api/drives/:mountpoint/repos/:repoName/commits/:hash', (req, res) => {
|
|
504
|
+
try {
|
|
505
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
506
|
+
const repoName = decodeURIComponent(req.params.repoName);
|
|
507
|
+
const hash = req.params.hash;
|
|
508
|
+
|
|
509
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
510
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
511
|
+
let repoPath = path.join(gitDrivePath, bareRepoName);
|
|
512
|
+
if (!existsSync(repoPath)) repoPath = path.join(gitDrivePath, repoName);
|
|
513
|
+
|
|
514
|
+
// hash|authorName|authorEmail|message|date
|
|
515
|
+
const logOutput = git(`log -1 --format="%H|%an|%ae|%s|%ci" ${hash}`, repoPath);
|
|
516
|
+
const [commitHash, author, email, message, date] = logOutput.split('|');
|
|
517
|
+
|
|
518
|
+
// Get the diff/patch
|
|
519
|
+
const patch = git(`show --format="" ${hash}`, repoPath);
|
|
520
|
+
|
|
521
|
+
res.json({
|
|
522
|
+
hash: commitHash,
|
|
523
|
+
author,
|
|
524
|
+
email,
|
|
525
|
+
message,
|
|
526
|
+
date,
|
|
527
|
+
patch
|
|
528
|
+
});
|
|
529
|
+
} catch (err) {
|
|
530
|
+
res.status(500).json({ error: 'Failed to retrieve commit details' });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Read raw file content
|
|
535
|
+
app.get('/api/drives/:mountpoint/repos/:repoName/blob', (req, res) => {
|
|
536
|
+
try {
|
|
537
|
+
const mountpoint = decodeURIComponent(req.params.mountpoint);
|
|
538
|
+
const repoName = decodeURIComponent(req.params.repoName);
|
|
539
|
+
const branch = req.query.branch || 'main';
|
|
540
|
+
const filePath = req.query.path as string;
|
|
541
|
+
|
|
542
|
+
const gitDrivePath = getGitDrivePath(mountpoint);
|
|
543
|
+
const bareRepoName = repoName.endsWith('.git') ? repoName : `${repoName}.git`;
|
|
544
|
+
let repoPath = path.join(gitDrivePath, bareRepoName);
|
|
545
|
+
|
|
546
|
+
if (!existsSync(repoPath)) {
|
|
547
|
+
repoPath = path.join(gitDrivePath, repoName);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const content = git(`show ${branch}:${filePath}`, repoPath);
|
|
551
|
+
res.send(content);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
res.status(500).json({ error: 'Failed to read file' });
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// SPA fallback - serve index.html for all non-API routes
|
|
558
|
+
app.get('*', (_req, res) => {
|
|
559
|
+
const indexPath = path.join(__dirname, '../../../packages/ui/dist', 'index.html');
|
|
560
|
+
if (existsSync(indexPath)) {
|
|
561
|
+
res.sendFile(indexPath);
|
|
562
|
+
} else {
|
|
563
|
+
res.status(404).send('UI not built. Run: pnpm build:ui');
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
app.listen(port, () => {
|
|
568
|
+
console.log(`\n 🚀 Git Drive is running at http://localhost:${port}\n`);
|
|
569
|
+
});
|