guito 0.2.1 → 0.4.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.
Files changed (167) hide show
  1. package/README.md +39 -2
  2. package/bin/index.js +727 -50
  3. package/bin/ui/3rdpartylicenses.txt +329 -0
  4. package/bin/ui/browser/favicon.svg +9 -0
  5. package/bin/ui/browser/index.html +13 -0
  6. package/bin/ui/browser/main-2DRQNM37.js +5 -0
  7. package/bin/ui/browser/polyfills-5CFQRCPP.js +2 -0
  8. package/bin/ui/browser/styles-I7K2YZTW.css +1 -0
  9. package/bin/ui/browser/vs/abap-D-t0cyap.js +1 -0
  10. package/bin/ui/browser/vs/apex-CcIm7xu6.js +1 -0
  11. package/bin/ui/browser/vs/assets/css.worker-URu8fCFR.js +97 -0
  12. package/bin/ui/browser/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  13. package/bin/ui/browser/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  14. package/bin/ui/browser/vs/assets/html.worker-D1SL3iM8.js +474 -0
  15. package/bin/ui/browser/vs/assets/json.worker-CoJx_OPf.js +62 -0
  16. package/bin/ui/browser/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  17. package/bin/ui/browser/vs/azcli-BA0tQDCg.js +1 -0
  18. package/bin/ui/browser/vs/basic-languages/monaco.contribution.js +1 -0
  19. package/bin/ui/browser/vs/bat-C397hTD6.js +1 -0
  20. package/bin/ui/browser/vs/bicep-DF5aW17k.js +2 -0
  21. package/bin/ui/browser/vs/cameligo-plsz8qhj.js +1 -0
  22. package/bin/ui/browser/vs/clojure-Y2auQMzK.js +1 -0
  23. package/bin/ui/browser/vs/coffee-Bu45yuWE.js +1 -0
  24. package/bin/ui/browser/vs/cpp-CkKPQIni.js +1 -0
  25. package/bin/ui/browser/vs/csharp-CX28MZyh.js +1 -0
  26. package/bin/ui/browser/vs/csp-D8uWnyxW.js +1 -0
  27. package/bin/ui/browser/vs/css-CaeNmE3S.js +3 -0
  28. package/bin/ui/browser/vs/css.worker-CyhWkhHo.js +1 -0
  29. package/bin/ui/browser/vs/cssMode-CV6Ay48H.js +1 -0
  30. package/bin/ui/browser/vs/cypher-DVThT8BS.js +1 -0
  31. package/bin/ui/browser/vs/dart-CmGfCvrO.js +1 -0
  32. package/bin/ui/browser/vs/dockerfile-CZqqYdch.js +1 -0
  33. package/bin/ui/browser/vs/ecl-30fUercY.js +1 -0
  34. package/bin/ui/browser/vs/editor/editor.main.css +1 -0
  35. package/bin/ui/browser/vs/editor/editor.main.js +1 -0
  36. package/bin/ui/browser/vs/editor/editor.worker.js +1 -0
  37. package/bin/ui/browser/vs/editor-KLE6jdfb.js +831 -0
  38. package/bin/ui/browser/vs/editor.js +1 -0
  39. package/bin/ui/browser/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  40. package/bin/ui/browser/vs/elixir-xjPaIfzF.js +1 -0
  41. package/bin/ui/browser/vs/flow9-DqtmStfK.js +1 -0
  42. package/bin/ui/browser/vs/freemarker2-FWreY7v7.js +3 -0
  43. package/bin/ui/browser/vs/fsharp-BOMdg4U1.js +1 -0
  44. package/bin/ui/browser/vs/go-D_hbi-Jt.js +1 -0
  45. package/bin/ui/browser/vs/graphql-CKUU4kLG.js +1 -0
  46. package/bin/ui/browser/vs/handlebars-CZi4pHCH.js +1 -0
  47. package/bin/ui/browser/vs/hcl-DTaboeZW.js +1 -0
  48. package/bin/ui/browser/vs/html-j_7ZTNRU.js +1 -0
  49. package/bin/ui/browser/vs/html.worker-CA3iAimZ.js +1 -0
  50. package/bin/ui/browser/vs/htmlMode-27by_KM4.js +1 -0
  51. package/bin/ui/browser/vs/index-049xrXrp.js +1 -0
  52. package/bin/ui/browser/vs/index-CBVt3dzv.js +5 -0
  53. package/bin/ui/browser/vs/index.js +1 -0
  54. package/bin/ui/browser/vs/ini-CsNwO04R.js +1 -0
  55. package/bin/ui/browser/vs/initialize-DL0l1TGY.js +1 -0
  56. package/bin/ui/browser/vs/java-CI4ZMsH9.js +1 -0
  57. package/bin/ui/browser/vs/javascript-DTJl4Jn1.js +1 -0
  58. package/bin/ui/browser/vs/json.worker-BizpAl9O.js +1 -0
  59. package/bin/ui/browser/vs/jsonMode-DLUiMUrI.js +1 -0
  60. package/bin/ui/browser/vs/julia-BwzEvaQw.js +1 -0
  61. package/bin/ui/browser/vs/kotlin-IUYPiTV8.js +1 -0
  62. package/bin/ui/browser/vs/language/css/css.worker.js +66 -0
  63. package/bin/ui/browser/vs/language/css/monaco.contribution.js +1 -0
  64. package/bin/ui/browser/vs/language/html/html.worker.js +443 -0
  65. package/bin/ui/browser/vs/language/html/monaco.contribution.js +1 -0
  66. package/bin/ui/browser/vs/language/json/json.worker.js +17 -0
  67. package/bin/ui/browser/vs/language/json/monaco.contribution.js +1 -0
  68. package/bin/ui/browser/vs/language/typescript/monaco.contribution.js +1 -0
  69. package/bin/ui/browser/vs/language/typescript/ts.worker.js +67693 -0
  70. package/bin/ui/browser/vs/less-C0eDYdqa.js +2 -0
  71. package/bin/ui/browser/vs/lexon-iON-Kj97.js +1 -0
  72. package/bin/ui/browser/vs/liquid-CLJYelW6.js +1 -0
  73. package/bin/ui/browser/vs/loader.js +1368 -0
  74. package/bin/ui/browser/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  75. package/bin/ui/browser/vs/lua-DtygF91M.js +1 -0
  76. package/bin/ui/browser/vs/m3-CsR4AuFi.js +1 -0
  77. package/bin/ui/browser/vs/main-BEx-Fmlo.js +15 -0
  78. package/bin/ui/browser/vs/main-DsK8pnKg.js +3 -0
  79. package/bin/ui/browser/vs/markdown-C_rD0bIw.js +1 -0
  80. package/bin/ui/browser/vs/mdx-CpzZIPHF.js +1 -0
  81. package/bin/ui/browser/vs/mips-CiYP61RB.js +1 -0
  82. package/bin/ui/browser/vs/monaco.contribution-9cKT3C7t.js +1 -0
  83. package/bin/ui/browser/vs/monaco.contribution-BE88ZNGY.js +1 -0
  84. package/bin/ui/browser/vs/monaco.contribution-BPhsneLd.js +1 -0
  85. package/bin/ui/browser/vs/monaco.contribution-BgRy6xDf.js +1 -0
  86. package/bin/ui/browser/vs/msdax-C38-sJlp.js +1 -0
  87. package/bin/ui/browser/vs/mysql-CdtbpvbG.js +1 -0
  88. package/bin/ui/browser/vs/nls/lang/cs.d.ts +1 -0
  89. package/bin/ui/browser/vs/nls/lang/cs.js +9 -0
  90. package/bin/ui/browser/vs/nls/lang/de.d.ts +1 -0
  91. package/bin/ui/browser/vs/nls/lang/de.js +9 -0
  92. package/bin/ui/browser/vs/nls/lang/es.d.ts +1 -0
  93. package/bin/ui/browser/vs/nls/lang/es.js +9 -0
  94. package/bin/ui/browser/vs/nls/lang/fr.d.ts +1 -0
  95. package/bin/ui/browser/vs/nls/lang/fr.js +9 -0
  96. package/bin/ui/browser/vs/nls/lang/it.d.ts +1 -0
  97. package/bin/ui/browser/vs/nls/lang/it.js +9 -0
  98. package/bin/ui/browser/vs/nls/lang/ja.d.ts +1 -0
  99. package/bin/ui/browser/vs/nls/lang/ja.js +9 -0
  100. package/bin/ui/browser/vs/nls/lang/ko.d.ts +1 -0
  101. package/bin/ui/browser/vs/nls/lang/ko.js +9 -0
  102. package/bin/ui/browser/vs/nls/lang/pl.d.ts +1 -0
  103. package/bin/ui/browser/vs/nls/lang/pl.js +9 -0
  104. package/bin/ui/browser/vs/nls/lang/pt-br.d.ts +1 -0
  105. package/bin/ui/browser/vs/nls/lang/pt-br.js +9 -0
  106. package/bin/ui/browser/vs/nls/lang/ru.d.ts +1 -0
  107. package/bin/ui/browser/vs/nls/lang/ru.js +9 -0
  108. package/bin/ui/browser/vs/nls/lang/tr.d.ts +1 -0
  109. package/bin/ui/browser/vs/nls/lang/tr.js +9 -0
  110. package/bin/ui/browser/vs/nls/lang/zh-cn.d.ts +1 -0
  111. package/bin/ui/browser/vs/nls/lang/zh-cn.js +9 -0
  112. package/bin/ui/browser/vs/nls/lang/zh-tw.d.ts +1 -0
  113. package/bin/ui/browser/vs/nls/lang/zh-tw.js +9 -0
  114. package/bin/ui/browser/vs/nls.messages-loader.js +1 -0
  115. package/bin/ui/browser/vs/objective-c-CntZFaHX.js +1 -0
  116. package/bin/ui/browser/vs/pascal-r6kuqfl_.js +1 -0
  117. package/bin/ui/browser/vs/pascaligo-BiXoTmXh.js +1 -0
  118. package/bin/ui/browser/vs/perl-DABw_TcH.js +1 -0
  119. package/bin/ui/browser/vs/pgsql-me_jFXeX.js +1 -0
  120. package/bin/ui/browser/vs/php-D_kh-9LK.js +1 -0
  121. package/bin/ui/browser/vs/pla-VfZjczW0.js +1 -0
  122. package/bin/ui/browser/vs/postiats-BBSzz8Pk.js +1 -0
  123. package/bin/ui/browser/vs/powerquery-Dt-g_2cc.js +1 -0
  124. package/bin/ui/browser/vs/powershell-B-7ap1zc.js +1 -0
  125. package/bin/ui/browser/vs/protobuf-BmtuEB1A.js +2 -0
  126. package/bin/ui/browser/vs/pug-BRpRNeEb.js +1 -0
  127. package/bin/ui/browser/vs/python-CqWUUgfu.js +1 -0
  128. package/bin/ui/browser/vs/qsharp-BzsFaUU9.js +1 -0
  129. package/bin/ui/browser/vs/r-f8dDdrp4.js +1 -0
  130. package/bin/ui/browser/vs/razor-hok5y9Km.js +1 -0
  131. package/bin/ui/browser/vs/redis-fvZQY4PI.js +1 -0
  132. package/bin/ui/browser/vs/redshift-45Et0LQi.js +1 -0
  133. package/bin/ui/browser/vs/restructuredtext-C7UUFKFD.js +1 -0
  134. package/bin/ui/browser/vs/ruby-CZO8zYTz.js +1 -0
  135. package/bin/ui/browser/vs/rust-Bfetafyc.js +1 -0
  136. package/bin/ui/browser/vs/sb-3GYllVck.js +1 -0
  137. package/bin/ui/browser/vs/scala-foMgrKo1.js +1 -0
  138. package/bin/ui/browser/vs/scheme-CHdMtr7p.js +1 -0
  139. package/bin/ui/browser/vs/scss-C1cmLt9V.js +3 -0
  140. package/bin/ui/browser/vs/shell-ClXCKCEW.js +1 -0
  141. package/bin/ui/browser/vs/solidity-MZ6ExpPy.js +1 -0
  142. package/bin/ui/browser/vs/sophia-DWkuSsPQ.js +1 -0
  143. package/bin/ui/browser/vs/sparql-AUGFYSyk.js +1 -0
  144. package/bin/ui/browser/vs/sql-32GpJSV2.js +1 -0
  145. package/bin/ui/browser/vs/st-CuDFIVZ_.js +1 -0
  146. package/bin/ui/browser/vs/swift-t-PfMj7W.js +1 -0
  147. package/bin/ui/browser/vs/systemverilog-Ch4vA8Yt.js +1 -0
  148. package/bin/ui/browser/vs/tcl-D74tq1nH.js +1 -0
  149. package/bin/ui/browser/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  150. package/bin/ui/browser/vs/ts.worker-2QLmBukE.js +1 -0
  151. package/bin/ui/browser/vs/tsMode-B0S_kp2q.js +11 -0
  152. package/bin/ui/browser/vs/twig-C6taOxMV.js +1 -0
  153. package/bin/ui/browser/vs/typescript-_2t_511t.js +1 -0
  154. package/bin/ui/browser/vs/typespec-8OfoLt6R.js +1 -0
  155. package/bin/ui/browser/vs/vb-Dyb2648j.js +1 -0
  156. package/bin/ui/browser/vs/wgsl-BhLXMOR0.js +298 -0
  157. package/bin/ui/browser/vs/workers-BBttULjf.js +1 -0
  158. package/bin/ui/browser/vs/xml-cZjNDqDT.js +1 -0
  159. package/bin/ui/browser/vs/yaml-A1fOIdH6.js +1 -0
  160. package/bin/ui/prerendered-routes.json +3 -0
  161. package/package.json +19 -20
  162. package/bin/ui/assets/index-6fEMLduw.js +0 -7876
  163. package/bin/ui/assets/index-baVP32pl.css +0 -1
  164. package/bin/ui/assets/svelte-CKrU6DOB.css +0 -18684
  165. package/bin/ui/assets/svelte-dark-7cNU_wYW.css +0 -18684
  166. package/bin/ui/index.html +0 -38
  167. package/bin/ui/vite.svg +0 -1
package/bin/index.js CHANGED
@@ -1,50 +1,727 @@
1
- #! /usr/bin/env node
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import fastify from 'fastify';
5
- import fastifyStatic from '@fastify/static';
6
- import fastifyCors from '@fastify/cors';
7
- import open from 'open';
8
- import simpleGit from 'simple-git';
9
- void (async function main() {
10
- // Calculate dirname
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- // Get port number
13
- const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--port');
14
- const port = portArgIndex !== -1 ? +process.argv[portArgIndex + 1] : 8080;
15
- // Get if should not open browser
16
- const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() === '--no-open') === -1;
17
- // Initialize server
18
- const app = fastify({
19
- logger: false,
20
- });
21
- // Register cors
22
- await app.register(fastifyCors);
23
- // Register static file provider
24
- app.register(fastifyStatic, {
25
- root: join(__dirname, 'ui'),
26
- });
27
- // Serve UI
28
- app.get('/', (_req, resp) => resp.sendFile('index.html'));
29
- // Initialize git lib
30
- const git = simpleGit(process.cwd());
31
- // Git Api
32
- app.get('/api/commits', (_req, resp) => {
33
- git.log({}, (err, data) => {
34
- resp.type('application/json').send(data.all);
35
- });
36
- });
37
- app.get('/api/fetch', (_req, resp) => {
38
- git.fetch({}, (err, data) => {
39
- resp.type('application/json').send();
40
- });
41
- });
42
- // Run the server
43
- app.listen({ port }, (err, address) => {
44
- if (err)
45
- throw err;
46
- console.log(`Visit "${address}" to see git explorer.`);
47
- if (shouldOpenBrowser)
48
- open(address);
49
- });
50
- })();
1
+ #! /usr/bin/env node
2
+ import { existsSync } from 'node:fs';
3
+ import { execFile } from 'node:child_process';
4
+ import { readFile, rm } from 'node:fs/promises';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import fastify from 'fastify';
8
+ import fastifyStatic from '@fastify/static';
9
+ import fastifyCors from '@fastify/cors';
10
+ import open from 'open';
11
+ import simpleGit from 'simple-git';
12
+ import { promisify } from 'node:util';
13
+ const execFileAsync = promisify(execFile);
14
+ void (async function main() {
15
+ // Calculate dirname
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ // Get port number
18
+ const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--port');
19
+ const port = portArgIndex !== -1 ? +process.argv[portArgIndex + 1] : 8080;
20
+ // Get if should not open browser
21
+ const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() === '--no-open') === -1;
22
+ // Initialize server
23
+ const app = fastify({
24
+ logger: false,
25
+ });
26
+ // Register cors
27
+ await app.register(fastifyCors);
28
+ // Register static file provider (supports both `ui/` and `ui/browser/` layouts)
29
+ const uiRoot = join(__dirname, 'ui');
30
+ const staticRoot = existsSync(join(uiRoot, 'browser', 'index.html'))
31
+ ? join(uiRoot, 'browser')
32
+ : uiRoot;
33
+ if (existsSync(staticRoot)) {
34
+ await app.register(fastifyStatic, { root: staticRoot });
35
+ // Serve UI
36
+ app.get('/', (_req, resp) => resp.sendFile('index.html'));
37
+ }
38
+ else {
39
+ app.get('/', (_req, resp) => resp.send('Guito API is running. Build the UI with `npm run build:ui`.'));
40
+ }
41
+ // Initialize git lib
42
+ const git = simpleGit(process.cwd());
43
+ // ==================== Diff parsing ====================
44
+ function parseUnifiedDiff(rawDiff) {
45
+ const files = [];
46
+ const lines = rawDiff.split('\n');
47
+ let current = null;
48
+ let inHunk = false;
49
+ let oldLine = 0;
50
+ let newLine = 0;
51
+ const startFile = (line) => {
52
+ current = {
53
+ path: '',
54
+ oldPath: '',
55
+ status: 'modified',
56
+ lines: [],
57
+ additions: 0,
58
+ deletions: 0,
59
+ };
60
+ const match = line.match(/^diff --git a\/(.*) b\/(.*)$/);
61
+ if (match) {
62
+ current.oldPath = match[1];
63
+ current.path = match[2];
64
+ }
65
+ inHunk = false;
66
+ };
67
+ for (const line of lines) {
68
+ if (line.startsWith('diff --git ')) {
69
+ if (current)
70
+ files.push(current);
71
+ startFile(line);
72
+ continue;
73
+ }
74
+ if (!current)
75
+ continue;
76
+ if (line.startsWith('@@')) {
77
+ inHunk = true;
78
+ const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
79
+ if (match) {
80
+ oldLine = parseInt(match[1], 10);
81
+ newLine = parseInt(match[2], 10);
82
+ }
83
+ current.lines.push({ type: 'hunk', text: line });
84
+ continue;
85
+ }
86
+ if (!inHunk) {
87
+ // File meta headers (mode, index, rename, binary...)
88
+ if (line.startsWith('new file mode'))
89
+ current.status = 'added';
90
+ else if (line.startsWith('deleted file mode'))
91
+ current.status = 'deleted';
92
+ else if (line.startsWith('rename from '))
93
+ current.status = 'renamed';
94
+ else if (line.startsWith('rename to '))
95
+ current.path = line.slice('rename to '.length);
96
+ else if (line.startsWith('Binary files') || line.startsWith('GIT binary patch'))
97
+ current.status = 'binary';
98
+ continue;
99
+ }
100
+ if (line.startsWith('\\')) {
101
+ // ""
102
+ continue;
103
+ }
104
+ if (line.startsWith('+')) {
105
+ current.lines.push({ type: 'add', newLine: newLine++, text: line.slice(1) });
106
+ current.additions++;
107
+ }
108
+ else if (line.startsWith('-')) {
109
+ current.lines.push({ type: 'del', oldLine: oldLine++, text: line.slice(1) });
110
+ current.deletions++;
111
+ }
112
+ else {
113
+ current.lines.push({
114
+ type: 'context',
115
+ oldLine: oldLine++,
116
+ newLine: newLine++,
117
+ text: line.slice(1),
118
+ });
119
+ }
120
+ }
121
+ if (current)
122
+ files.push(current);
123
+ return files;
124
+ }
125
+ const repoRoot = async () => (await git.revparse(['--show-toplevel'])).trim();
126
+ // ==================== Repository ====================
127
+ app.get('/api/repo', async (_req, resp) => {
128
+ try {
129
+ const root = (await git.revparse(['--show-toplevel'])).trim();
130
+ resp.type('application/json').send({ root, name: basename(root) });
131
+ }
132
+ catch (err) {
133
+ resp.status(400).type('application/json').send({ error: err.message });
134
+ }
135
+ });
136
+ // ==================== Commits ====================
137
+ app.get('/api/commits', async (_req, resp) => {
138
+ try {
139
+ const log = await git.log({
140
+ format: {
141
+ hash: '%H',
142
+ date: '%aI',
143
+ message: '%s',
144
+ refs: '%D',
145
+ body: '%b',
146
+ author_name: '%aN',
147
+ author_email: '%aE',
148
+ parents: '%P',
149
+ },
150
+ });
151
+ const commits = log.all.map((commit) => ({
152
+ ...commit,
153
+ parents: String(commit.parents ?? '')
154
+ .split(' ')
155
+ .filter(Boolean),
156
+ }));
157
+ resp.type('application/json').send(commits);
158
+ }
159
+ catch (err) {
160
+ resp.status(400).type('application/json').send({ error: err.message });
161
+ }
162
+ });
163
+ app.post('/api/commit', async (req, resp) => {
164
+ try {
165
+ const { message, description } = req.body;
166
+ await git.commit([message, ...(description ? [description] : [])], { '--allow-empty': null });
167
+ resp.type('application/json').send({ success: true });
168
+ }
169
+ catch (err) {
170
+ resp.status(400).type('application/json').send({ error: err.message });
171
+ }
172
+ });
173
+ app.post('/api/amend', async (req, resp) => {
174
+ try {
175
+ const { message, description } = req.body;
176
+ await git.commit([message, ...(description ? [description] : [])], {
177
+ '--amend': null,
178
+ '--no-edit': null,
179
+ });
180
+ resp.type('application/json').send({ success: true });
181
+ }
182
+ catch (err) {
183
+ resp.status(400).type('application/json').send({ error: err.message });
184
+ }
185
+ });
186
+ app.post('/api/revert', async (req, resp) => {
187
+ try {
188
+ const { commit } = req.body;
189
+ await git.revert(commit);
190
+ resp.type('application/json').send({ success: true });
191
+ }
192
+ catch (err) {
193
+ resp.status(400).type('application/json').send({ error: err.message });
194
+ }
195
+ });
196
+ app.post('/api/cherry-pick', async (req, resp) => {
197
+ try {
198
+ await git.raw(['cherry-pick', req.body.commit]);
199
+ resp.type('application/json').send({ success: true });
200
+ }
201
+ catch (err) {
202
+ resp.status(400).type('application/json').send({ error: err.message });
203
+ }
204
+ });
205
+ app.post('/api/commit/drop', async (req, resp) => {
206
+ try {
207
+ await git.raw(['reset', '--hard', `${req.body.commit}^`]);
208
+ resp.type('application/json').send({ success: true });
209
+ }
210
+ catch (err) {
211
+ resp.status(400).type('application/json').send({ error: err.message });
212
+ }
213
+ });
214
+ app.post('/api/reset-commit', async (req, resp) => {
215
+ try {
216
+ await git.raw(['reset', '--hard', req.body.commit]);
217
+ resp.type('application/json').send({ success: true });
218
+ }
219
+ catch (err) {
220
+ resp.status(400).type('application/json').send({ error: err.message });
221
+ }
222
+ });
223
+ // ==================== Staging ====================
224
+ app.get('/api/status', async (_req, resp) => {
225
+ try {
226
+ const status = await git.status();
227
+ resp.type('application/json').send(status);
228
+ }
229
+ catch (err) {
230
+ resp.status(400).type('application/json').send({ error: err.message });
231
+ }
232
+ });
233
+ app.post('/api/stage', async (req, resp) => {
234
+ try {
235
+ const { files } = req.body;
236
+ if (Array.isArray(files) && files.length > 0) {
237
+ await git.add(files);
238
+ }
239
+ resp.type('application/json').send({ success: true });
240
+ }
241
+ catch (err) {
242
+ resp.status(400).type('application/json').send({ error: err.message });
243
+ }
244
+ });
245
+ app.post('/api/unstage', async (req, resp) => {
246
+ try {
247
+ const { files } = req.body;
248
+ if (Array.isArray(files) && files.length > 0) {
249
+ await git.reset(['HEAD', ...files]);
250
+ }
251
+ resp.type('application/json').send({ success: true });
252
+ }
253
+ catch (err) {
254
+ resp.status(400).type('application/json').send({ error: err.message });
255
+ }
256
+ });
257
+ app.post('/api/stage-lines', async (req, resp) => {
258
+ try {
259
+ const { file, patch } = req.body;
260
+ // Stage specific lines using git add -p equivalent (patch mode)
261
+ await git.raw('add --patch', { input: patch });
262
+ resp.type('application/json').send({ success: true });
263
+ }
264
+ catch (err) {
265
+ resp.status(400).type('application/json').send({ error: err.message });
266
+ }
267
+ });
268
+ // ==================== Branches ====================
269
+ app.get('/api/branches/all', async (_req, resp) => {
270
+ try {
271
+ const raw = await git.raw([
272
+ 'for-each-ref',
273
+ '--format=%(refname)%09%(objectname)%09%(HEAD)',
274
+ 'refs/heads',
275
+ 'refs/remotes',
276
+ ]);
277
+ const branches = raw
278
+ .split('\n')
279
+ .map((line) => line.trim())
280
+ .filter(Boolean)
281
+ .map((line) => {
282
+ const [refname, commit, headMarker] = line.split('\t');
283
+ const remote = refname.startsWith('refs/remotes/');
284
+ const name = refname.replace(/^refs\/(heads|remotes)\//, '');
285
+ return { name, commit, current: headMarker === '*', remote };
286
+ });
287
+ resp.type('application/json').send(branches);
288
+ }
289
+ catch (err) {
290
+ resp.status(400).type('application/json').send({ error: err.message });
291
+ }
292
+ });
293
+ app.get('/api/branches', async (_req, resp) => {
294
+ try {
295
+ const branchSummary = await git.branchLocal();
296
+ resp.type('application/json').send(branchSummary);
297
+ }
298
+ catch (err) {
299
+ resp.status(400).type('application/json').send({ error: err.message });
300
+ }
301
+ });
302
+ app.post('/api/branch/create', async (req, resp) => {
303
+ try {
304
+ const { name, startPoint } = req.body;
305
+ await git.branch([...(startPoint ? [name, startPoint] : [name])]);
306
+ resp.type('application/json').send({ success: true });
307
+ }
308
+ catch (err) {
309
+ resp.status(400).type('application/json').send({ error: err.message });
310
+ }
311
+ });
312
+ app.post('/api/branch/delete', async (req, resp) => {
313
+ try {
314
+ const { name, force } = req.body;
315
+ await git.deleteLocalBranch(name, force);
316
+ resp.type('application/json').send({ success: true });
317
+ }
318
+ catch (err) {
319
+ resp.status(400).type('application/json').send({ error: err.message });
320
+ }
321
+ });
322
+ app.post('/api/branch/delete-remote', async (req, resp) => {
323
+ try {
324
+ const { remote, branch } = req.body;
325
+ await git.push([remote, '--delete', branch]);
326
+ resp.type('application/json').send({ success: true });
327
+ }
328
+ catch (err) {
329
+ resp.status(400).type('application/json').send({ error: err.message });
330
+ }
331
+ });
332
+ app.post('/api/branch/rename', async (req, resp) => {
333
+ try {
334
+ const { oldName, newName } = req.body;
335
+ await git.branch(['-m', oldName, newName]);
336
+ resp.type('application/json').send({ success: true });
337
+ }
338
+ catch (err) {
339
+ resp.status(400).type('application/json').send({ error: err.message });
340
+ }
341
+ });
342
+ app.post('/api/checkout', async (req, resp) => {
343
+ try {
344
+ const { ref } = req.body;
345
+ await git.checkout(ref);
346
+ resp.type('application/json').send({ success: true });
347
+ }
348
+ catch (err) {
349
+ resp.status(400).type('application/json').send({ error: err.message });
350
+ }
351
+ });
352
+ // ==================== Merge & Rebase ====================
353
+ app.post('/api/merge', async (req, resp) => {
354
+ try {
355
+ const { branch } = req.body;
356
+ await git.merge([branch]);
357
+ resp.type('application/json').send({ success: true });
358
+ }
359
+ catch (err) {
360
+ resp.status(400).type('application/json').send({ error: err.message });
361
+ }
362
+ });
363
+ app.get('/api/archive', async (req, resp) => {
364
+ try {
365
+ const ref = String(req.query?.ref ?? 'HEAD');
366
+ if (!/^[0-9a-fA-F]{7,40}$/.test(ref) && !/^[A-Za-z0-9._/-]+$/.test(ref)) {
367
+ return resp.status(400).type('application/json').send({ error: 'invalid ref' });
368
+ }
369
+ const result = await execFileAsync('git', ['archive', '--format=zip', ref], {
370
+ cwd: process.cwd(),
371
+ encoding: 'buffer',
372
+ maxBuffer: 100 * 1024 * 1024,
373
+ });
374
+ return resp
375
+ .header('Content-Disposition', `attachment; filename="guito-${ref.slice(0, 8)}.zip"`)
376
+ .type('application/zip')
377
+ .send(result.stdout);
378
+ }
379
+ catch (err) {
380
+ return resp.status(400).type('application/json').send({ error: err.message });
381
+ }
382
+ });
383
+ app.post('/api/rebase', async (req, resp) => {
384
+ try {
385
+ const { branch } = req.body;
386
+ await git.rebase([branch]);
387
+ resp.type('application/json').send({ success: true });
388
+ }
389
+ catch (err) {
390
+ resp.status(400).type('application/json').send({ error: err.message });
391
+ }
392
+ });
393
+ app.post('/api/squash', async (req, resp) => {
394
+ try {
395
+ const { commits } = req.body;
396
+ // Squash: rebase -i with squash operation
397
+ await git.rebase(['-i', 'HEAD~' + commits]);
398
+ resp.type('application/json').send({ success: true });
399
+ }
400
+ catch (err) {
401
+ resp.status(400).type('application/json').send({ error: err.message });
402
+ }
403
+ });
404
+ // ==================== Stash ====================
405
+ app.get('/api/stash/list', async (_req, resp) => {
406
+ try {
407
+ const stashList = await git.stashList();
408
+ resp.type('application/json').send(stashList);
409
+ }
410
+ catch (err) {
411
+ resp.status(400).type('application/json').send({ error: err.message });
412
+ }
413
+ });
414
+ app.post('/api/stash/save', async (req, resp) => {
415
+ try {
416
+ const { message } = req.body;
417
+ await git.stash(['save', message || '']);
418
+ resp.type('application/json').send({ success: true });
419
+ }
420
+ catch (err) {
421
+ resp.status(400).type('application/json').send({ error: err.message });
422
+ }
423
+ });
424
+ app.post('/api/stash/apply', async (req, resp) => {
425
+ try {
426
+ const { index } = req.body;
427
+ await git.stash(['apply', `stash@{${index}}`]);
428
+ resp.type('application/json').send({ success: true });
429
+ }
430
+ catch (err) {
431
+ resp.status(400).type('application/json').send({ error: err.message });
432
+ }
433
+ });
434
+ app.post('/api/stash/pop', async (req, resp) => {
435
+ try {
436
+ const { index } = req.body;
437
+ await git.stash(['pop', `stash@{${index}}`]);
438
+ resp.type('application/json').send({ success: true });
439
+ }
440
+ catch (err) {
441
+ resp.status(400).type('application/json').send({ error: err.message });
442
+ }
443
+ });
444
+ app.post('/api/stash/drop', async (req, resp) => {
445
+ try {
446
+ const { index } = req.body;
447
+ await git.stash(['drop', `stash@{${index}}`]);
448
+ resp.type('application/json').send({ success: true });
449
+ }
450
+ catch (err) {
451
+ resp.status(400).type('application/json').send({ error: err.message });
452
+ }
453
+ });
454
+ app.post('/api/stash/show', async (req, resp) => {
455
+ try {
456
+ const { index } = req.body;
457
+ const show = await git.stash(['show', '-p', `stash@{${index}}`]);
458
+ resp.type('application/json').send({ preview: show });
459
+ }
460
+ catch (err) {
461
+ resp.status(400).type('application/json').send({ error: err.message });
462
+ }
463
+ });
464
+ // ==================== Tags ====================
465
+ app.get('/api/tags', async (_req, resp) => {
466
+ try {
467
+ const tags = await git.tags();
468
+ resp.type('application/json').send(tags);
469
+ }
470
+ catch (err) {
471
+ resp.status(400).type('application/json').send({ error: err.message });
472
+ }
473
+ });
474
+ app.post('/api/tag/create', async (req, resp) => {
475
+ try {
476
+ const { name, message, commit } = req.body;
477
+ if (message) {
478
+ await git.tag(['-a', name, '-m', message, ...(commit ? [commit] : [])]);
479
+ }
480
+ else {
481
+ await git.tag([name, ...(commit ? [commit] : [])]);
482
+ }
483
+ resp.type('application/json').send({ success: true });
484
+ }
485
+ catch (err) {
486
+ resp.status(400).type('application/json').send({ error: err.message });
487
+ }
488
+ });
489
+ app.post('/api/tag/delete', async (req, resp) => {
490
+ try {
491
+ const { name } = req.body;
492
+ await git.tag(['-d', name]);
493
+ resp.type('application/json').send({ success: true });
494
+ }
495
+ catch (err) {
496
+ resp.status(400).type('application/json').send({ error: err.message });
497
+ }
498
+ });
499
+ // ==================== Remote Operations ====================
500
+ app.get('/api/fetch', async (_req, resp) => {
501
+ try {
502
+ await git.fetch();
503
+ resp.type('application/json').send({ success: true });
504
+ }
505
+ catch (err) {
506
+ resp.status(400).type('application/json').send({ error: err.message });
507
+ }
508
+ });
509
+ app.post('/api/pull', async (req, resp) => {
510
+ try {
511
+ const { remote, branch, rebase } = req.body;
512
+ await git.pull(remote || 'origin', branch || undefined, rebase ? { '--rebase': null } : {});
513
+ resp.type('application/json').send({ success: true });
514
+ }
515
+ catch (err) {
516
+ resp.status(400).type('application/json').send({ error: err.message });
517
+ }
518
+ });
519
+ app.post('/api/push', async (req, resp) => {
520
+ try {
521
+ const { remote, branch, force } = req.body;
522
+ await git.push(remote || 'origin', branch || undefined, force ? { '-f': null } : {});
523
+ resp.type('application/json').send({ success: true });
524
+ }
525
+ catch (err) {
526
+ resp.status(400).type('application/json').send({ error: err.message });
527
+ }
528
+ });
529
+ app.post('/api/sync', async (_req, resp) => {
530
+ try {
531
+ await git.pull('origin', undefined, {});
532
+ await git.push('origin', undefined, {});
533
+ resp.type('application/json').send({ success: true });
534
+ }
535
+ catch (err) {
536
+ resp.status(400).type('application/json').send({ error: err.message });
537
+ }
538
+ });
539
+ app.post('/api/prune', async (req, resp) => {
540
+ try {
541
+ const { remote } = req.body;
542
+ await git.remote(['prune', remote || 'origin']);
543
+ resp.type('application/json').send({ success: true });
544
+ }
545
+ catch (err) {
546
+ resp.status(400).type('application/json').send({ error: err.message });
547
+ }
548
+ });
549
+ // ==================== Diff Viewer ====================
550
+ app.post('/api/commit/diff', async (req, resp) => {
551
+ try {
552
+ const { hash } = req.body;
553
+ if (!hash) {
554
+ return resp.status(400).type('application/json').send({ error: 'hash required' });
555
+ }
556
+ // Get the raw unified diff (diff against the first parent for merges)
557
+ const rawDiff = await git.raw([
558
+ 'show',
559
+ '--no-color',
560
+ '--pretty=format:',
561
+ '--find-renames',
562
+ '--first-parent',
563
+ '-m',
564
+ hash,
565
+ ]);
566
+ resp.type('application/json').send({ hash, files: parseUnifiedDiff(rawDiff) });
567
+ }
568
+ catch (err) {
569
+ resp.status(400).type('application/json').send({ error: err.message });
570
+ }
571
+ });
572
+ // ==================== Working Changes ====================
573
+ app.get('/api/working-changes', async (_req, resp) => {
574
+ try {
575
+ const root = await repoRoot();
576
+ // Combined diff of the working tree + index against HEAD.
577
+ const rawDiff = await git.raw(['diff', 'HEAD', '--no-color', '--find-renames']);
578
+ const files = parseUnifiedDiff(rawDiff);
579
+ // Untracked files are not part of `git diff` — add them as new files.
580
+ const status = await git.status();
581
+ for (const file of status.not_added ?? []) {
582
+ let isBinary = false;
583
+ let content = '';
584
+ try {
585
+ const buffer = await readFile(join(root, file));
586
+ if (buffer.includes(0)) {
587
+ isBinary = true;
588
+ }
589
+ else {
590
+ content = buffer.toString('utf8');
591
+ }
592
+ }
593
+ catch {
594
+ content = '';
595
+ }
596
+ if (isBinary) {
597
+ files.push({
598
+ path: file,
599
+ oldPath: '',
600
+ status: 'binary',
601
+ lines: [],
602
+ additions: 0,
603
+ deletions: 0,
604
+ });
605
+ }
606
+ else {
607
+ const lines = content.length > 0 ? content.split('\n') : [];
608
+ files.push({
609
+ path: file,
610
+ oldPath: '',
611
+ status: 'added',
612
+ lines: lines.map((text, index) => ({ type: 'add', newLine: index + 1, text })),
613
+ additions: lines.length,
614
+ deletions: 0,
615
+ });
616
+ }
617
+ }
618
+ const stagedRaw = await git.raw(['diff', '--name-only', '--cached']);
619
+ const unstagedRaw = await git.raw(['diff', '--name-only']);
620
+ resp.type('application/json').send({
621
+ files,
622
+ staged: stagedRaw.split('\n').filter(Boolean),
623
+ unstaged: unstagedRaw.split('\n').filter(Boolean),
624
+ untracked: status.not_added ?? [],
625
+ });
626
+ }
627
+ catch (err) {
628
+ resp.status(400).type('application/json').send({ error: err.message });
629
+ }
630
+ });
631
+ // ==================== File Content ====================
632
+ app.post('/api/file-content', async (req, resp) => {
633
+ try {
634
+ const { path, ref } = req.body;
635
+ if (!path || !ref) {
636
+ return resp.status(400).type('application/json').send({ error: 'path and ref required' });
637
+ }
638
+ if (ref === 'WORKING') {
639
+ const root = await repoRoot();
640
+ const abs = resolve(root, path);
641
+ const rel = relative(root, abs);
642
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
643
+ return resp.status(400).type('application/json').send({ error: 'invalid path' });
644
+ }
645
+ try {
646
+ const buffer = await readFile(abs);
647
+ if (buffer.includes(0)) {
648
+ return resp.type('application/json').send({ binary: true, content: '' });
649
+ }
650
+ return resp.type('application/json').send({ content: buffer.toString('utf8') });
651
+ }
652
+ catch {
653
+ // File no longer exists on disk (deleted).
654
+ return resp.type('application/json').send({ content: '' });
655
+ }
656
+ }
657
+ if (ref === 'EMPTY') {
658
+ return resp.type('application/json').send({ content: '' });
659
+ }
660
+ try {
661
+ const content = await git.raw(['show', `${ref}:${path}`]);
662
+ if (content.includes('\u0000')) {
663
+ return resp.type('application/json').send({ binary: true, content: '' });
664
+ }
665
+ resp.type('application/json').send({ content });
666
+ }
667
+ catch {
668
+ // Path did not exist at that revision (added file).
669
+ resp.type('application/json').send({ content: '' });
670
+ }
671
+ }
672
+ catch (err) {
673
+ resp.status(400).type('application/json').send({ error: err.message });
674
+ }
675
+ });
676
+ // ==================== Discard ====================
677
+ app.post('/api/discard', async (req, resp) => {
678
+ try {
679
+ const { files } = req.body;
680
+ if (!Array.isArray(files) || files.length === 0) {
681
+ return resp.status(400).type('application/json').send({ error: 'files required' });
682
+ }
683
+ const root = await repoRoot();
684
+ const status = await git.status();
685
+ const untracked = new Set(status.not_added ?? []);
686
+ const tracked = files.filter((file) => !untracked.has(file));
687
+ const removed = files.filter((file) => untracked.has(file));
688
+ if (tracked.length > 0) {
689
+ await git.raw(['checkout', 'HEAD', '--', ...tracked]);
690
+ }
691
+ for (const file of removed) {
692
+ await rm(join(root, file), { force: true, recursive: true });
693
+ }
694
+ resp.type('application/json').send({ success: true });
695
+ }
696
+ catch (err) {
697
+ resp.status(400).type('application/json').send({ error: err.message });
698
+ }
699
+ });
700
+ // ==================== Working Tree Actions ====================
701
+ app.post('/api/reset', async (_req, resp) => {
702
+ try {
703
+ await git.raw(['reset', '--hard', 'HEAD']);
704
+ resp.type('application/json').send({ ok: true });
705
+ }
706
+ catch (err) {
707
+ resp.status(400).type('application/json').send({ error: err.message });
708
+ }
709
+ });
710
+ app.post('/api/clean', async (_req, resp) => {
711
+ try {
712
+ await git.raw(['clean', '-fd']);
713
+ resp.type('application/json').send({ ok: true });
714
+ }
715
+ catch (err) {
716
+ resp.status(400).type('application/json').send({ error: err.message });
717
+ }
718
+ });
719
+ // Run the server
720
+ app.listen({ port }, (err, address) => {
721
+ if (err)
722
+ throw err;
723
+ console.log(`Visit "${address}" to see git explorer.`);
724
+ if (shouldOpenBrowser)
725
+ open(address);
726
+ });
727
+ })();