resulgit 1.0.17 → 1.0.19
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/package.json +1 -1
- package/resulgit.js +216 -1250
- package/README.md +0 -194
package/resulgit.js
CHANGED
|
@@ -3,32 +3,9 @@ const fs = require('fs')
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const os = require('os')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
|
-
const ora = require('ora')
|
|
7
|
-
const validation = require('./lib/validation')
|
|
8
|
-
const errors = require('./lib/errors')
|
|
9
|
-
const { parseBlame, formatBlameOutput, formatBlameJson } = require('./lib/blame')
|
|
10
|
-
const { generateLogGraph, formatCompactLog, generateCommitStats } = require('./lib/log-viz')
|
|
11
|
-
const hooks = require('./lib/hooks')
|
|
12
6
|
const COLORS = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m' }
|
|
13
7
|
function color(str, c) { return (COLORS[c] || '') + String(str) + COLORS.reset }
|
|
14
8
|
|
|
15
|
-
function createSpinner(text, jsonMode) {
|
|
16
|
-
if (jsonMode === 'true' || !process.stdout.isTTY) return null
|
|
17
|
-
return ora(text).start()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function spinnerSuccess(spinner, text) {
|
|
21
|
-
if (spinner) spinner.succeed(text)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function spinnerFail(spinner, text) {
|
|
25
|
-
if (spinner) spinner.fail(text)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function spinnerUpdate(spinner, text) {
|
|
29
|
-
if (spinner) spinner.text = text
|
|
30
|
-
}
|
|
31
|
-
|
|
32
9
|
function parseArgs(argv) {
|
|
33
10
|
const tokens = argv.slice(2)
|
|
34
11
|
const cmd = []
|
|
@@ -37,22 +14,12 @@ function parseArgs(argv) {
|
|
|
37
14
|
const t = tokens[i]
|
|
38
15
|
if (t.startsWith('--')) {
|
|
39
16
|
const key = t.slice(2)
|
|
40
|
-
const val = tokens[i + 1] && !tokens[i + 1].startsWith('
|
|
17
|
+
const val = tokens[i + 1] && !tokens[i + 1].startsWith('--') ? tokens[++i] : 'true'
|
|
41
18
|
opts[key] = val
|
|
42
|
-
} else if (
|
|
43
|
-
// Short flag like -m, -a
|
|
44
|
-
const key = t.slice(1)
|
|
45
|
-
const val = tokens[i + 1] && !tokens[i + 1].startsWith('-') ? tokens[++i] : 'true'
|
|
46
|
-
opts[key] = val
|
|
47
|
-
} else if (cmd.length < 3) {
|
|
48
|
-
// Allow up to 3 positional args: e.g. 'branch create DevBranch'
|
|
19
|
+
} else if (cmd.length < 2) {
|
|
49
20
|
cmd.push(t)
|
|
50
21
|
}
|
|
51
22
|
}
|
|
52
|
-
// Map short flags to long flags
|
|
53
|
-
if (opts.m && !opts.message) { opts.message = opts.m; delete opts.m }
|
|
54
|
-
if (opts.a && !opts.all) { opts.all = opts.a; delete opts.a }
|
|
55
|
-
if (opts.b && !opts.branch) { opts.branch = opts.b; delete opts.b }
|
|
56
23
|
return { cmd, opts }
|
|
57
24
|
}
|
|
58
25
|
|
|
@@ -68,7 +35,7 @@ function loadConfig() {
|
|
|
68
35
|
const s = fs.readFileSync(file, 'utf8')
|
|
69
36
|
return JSON.parse(s)
|
|
70
37
|
} catch {
|
|
71
|
-
return { server: '
|
|
38
|
+
return { server: 'https://gitu.resulticks.com', token: '', workspaceRoot: path.join(os.homedir(), 'resulgit-workspace') }
|
|
72
39
|
}
|
|
73
40
|
}
|
|
74
41
|
|
|
@@ -82,7 +49,7 @@ function saveConfig(update) {
|
|
|
82
49
|
}
|
|
83
50
|
|
|
84
51
|
function getServer(opts, cfg) {
|
|
85
|
-
return opts.server || cfg.server || '
|
|
52
|
+
return opts.server || cfg.server || 'https://gitu.resulticks.com'
|
|
86
53
|
}
|
|
87
54
|
|
|
88
55
|
function getToken(opts, cfg) {
|
|
@@ -102,43 +69,6 @@ async function request(method, url, body, token) {
|
|
|
102
69
|
return res.text()
|
|
103
70
|
}
|
|
104
71
|
|
|
105
|
-
/**
|
|
106
|
-
* Upload files as blobs via multipart form data
|
|
107
|
-
* @param {string} server - Server URL
|
|
108
|
-
* @param {string} repoId - Repository ID
|
|
109
|
-
* @param {Record<string, string>} files - Map of file paths to content
|
|
110
|
-
* @param {string} token - Auth token
|
|
111
|
-
* @returns {Promise<Record<string, string>>} Map of file paths to blob IDs
|
|
112
|
-
*/
|
|
113
|
-
async function uploadBlobs(server, repoId, files, token) {
|
|
114
|
-
const FormData = (await import('node:buffer')).File ? globalThis.FormData : (await import('undici')).FormData
|
|
115
|
-
const formData = new FormData()
|
|
116
|
-
|
|
117
|
-
for (const [filePath, content] of Object.entries(files)) {
|
|
118
|
-
// Create a Blob/File from the content
|
|
119
|
-
const blob = new Blob([content], { type: 'text/plain' })
|
|
120
|
-
formData.append('files', blob, filePath)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const url = new URL(`/api/repositories/${repoId}/blobs`, server).toString()
|
|
124
|
-
const headers = {}
|
|
125
|
-
if (token) headers['Authorization'] = `Bearer ${token}`
|
|
126
|
-
|
|
127
|
-
const res = await fetch(url, {
|
|
128
|
-
method: 'POST',
|
|
129
|
-
headers,
|
|
130
|
-
body: formData
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
if (!res.ok) {
|
|
134
|
-
const text = await res.text()
|
|
135
|
-
throw new Error(`Blob upload failed: ${res.status} ${res.statusText} ${text}`)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const data = await res.json()
|
|
139
|
-
return data.blobs || {}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
72
|
function print(obj, json) {
|
|
143
73
|
if (json) {
|
|
144
74
|
process.stdout.write(JSON.stringify(obj, null, 2) + '\n')
|
|
@@ -176,17 +106,11 @@ async function cmdAuth(sub, opts) {
|
|
|
176
106
|
const password = opts.password
|
|
177
107
|
if (!email || !password) throw new Error('Missing --email and --password')
|
|
178
108
|
const url = new URL('/api/auth/login', server).toString()
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
spinnerSuccess(spinner, `Logged in as ${email}`)
|
|
185
|
-
print(res, opts.json === 'true')
|
|
186
|
-
} catch (err) {
|
|
187
|
-
spinnerFail(spinner, 'Login failed')
|
|
188
|
-
throw err
|
|
189
|
-
}
|
|
109
|
+
print({ server, email, password, url }, opts.json === 'true')
|
|
110
|
+
const res = await request('POST', url, { email, password }, '')
|
|
111
|
+
const token = res.token || ''
|
|
112
|
+
if (token) saveConfig({ token })
|
|
113
|
+
print(res, opts.json === 'true')
|
|
190
114
|
return
|
|
191
115
|
}
|
|
192
116
|
if (sub === 'register') {
|
|
@@ -197,17 +121,10 @@ async function cmdAuth(sub, opts) {
|
|
|
197
121
|
const displayName = opts.displayName || username
|
|
198
122
|
if (!username || !email || !password) throw new Error('Missing --username --email --password')
|
|
199
123
|
const url = new URL('/api/auth/register', server).toString()
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (token) saveConfig({ token })
|
|
205
|
-
spinnerSuccess(spinner, `Registered as ${username}`)
|
|
206
|
-
print(res, opts.json === 'true')
|
|
207
|
-
} catch (err) {
|
|
208
|
-
spinnerFail(spinner, 'Registration failed')
|
|
209
|
-
throw err
|
|
210
|
-
}
|
|
124
|
+
const res = await request('POST', url, { username, email, password, displayName }, '')
|
|
125
|
+
const token = res.token || ''
|
|
126
|
+
if (token) saveConfig({ token })
|
|
127
|
+
print(res, opts.json === 'true')
|
|
211
128
|
return
|
|
212
129
|
}
|
|
213
130
|
throw new Error('Unknown auth subcommand')
|
|
@@ -217,17 +134,10 @@ async function cmdRepo(sub, opts, cfg) {
|
|
|
217
134
|
const server = getServer(opts, cfg)
|
|
218
135
|
const token = getToken(opts, cfg)
|
|
219
136
|
if (sub === 'list') {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
spinnerSuccess(spinner, `Found ${(data || []).length} repositories`)
|
|
225
|
-
print(data, opts.json === 'true')
|
|
226
|
-
await tuiSelectRepo(data || [], cfg, opts)
|
|
227
|
-
} catch (err) {
|
|
228
|
-
spinnerFail(spinner, 'Failed to fetch repositories')
|
|
229
|
-
throw err
|
|
230
|
-
}
|
|
137
|
+
const url = new URL('/api/repositories', server).toString()
|
|
138
|
+
const data = await request('GET', url, null, token)
|
|
139
|
+
print(data, opts.json === 'true')
|
|
140
|
+
await tuiSelectRepo(data || [], cfg, opts)
|
|
231
141
|
return
|
|
232
142
|
}
|
|
233
143
|
if (sub === 'create') {
|
|
@@ -238,21 +148,14 @@ async function cmdRepo(sub, opts, cfg) {
|
|
|
238
148
|
initializeWithReadme: opts.init === 'true'
|
|
239
149
|
}
|
|
240
150
|
if (!body.name) throw new Error('Missing --name')
|
|
241
|
-
const
|
|
151
|
+
const url = new URL('/api/repositories', server).toString()
|
|
152
|
+
const data = await request('POST', url, body, token)
|
|
153
|
+
print(data, opts.json === 'true')
|
|
242
154
|
try {
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
const repoId = String(data.id || '')
|
|
249
|
-
const branch = String(data.defaultBranch || 'main')
|
|
250
|
-
if (repoId) { await cmdClone({ repo: repoId, branch, dest: opts.dest }, cfg) }
|
|
251
|
-
} catch { }
|
|
252
|
-
} catch (err) {
|
|
253
|
-
spinnerFail(spinner, `Failed to create repository '${body.name}'`)
|
|
254
|
-
throw err
|
|
255
|
-
}
|
|
155
|
+
const repoId = String(data.id || '')
|
|
156
|
+
const branch = String(data.defaultBranch || 'main')
|
|
157
|
+
if (repoId) { await cmdClone({ repo: repoId, branch, dest: opts.dest }, cfg) }
|
|
158
|
+
} catch { }
|
|
256
159
|
return
|
|
257
160
|
}
|
|
258
161
|
if (sub === 'log') {
|
|
@@ -318,108 +221,94 @@ async function cmdClone(opts, cfg) {
|
|
|
318
221
|
const server = getServer(opts, cfg)
|
|
319
222
|
const token = getToken(opts, cfg)
|
|
320
223
|
const repo = opts.repo
|
|
321
|
-
const branch = opts.branch
|
|
224
|
+
const branch = opts.branch
|
|
322
225
|
let dest = opts.dest
|
|
323
226
|
if (!repo || !branch) throw new Error('Missing --repo and --branch')
|
|
324
|
-
const spinner = createSpinner('Initializing clone...', opts.json)
|
|
325
227
|
const headers = token ? { Authorization: `Bearer ${token}` } : {}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
dest = name
|
|
336
|
-
} else {
|
|
337
|
-
dest = String(repo)
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
228
|
+
if (!dest) {
|
|
229
|
+
try {
|
|
230
|
+
const infoUrl = new URL(`/api/repositories/${repo}`, server)
|
|
231
|
+
const infoRes = await fetch(infoUrl.toString(), { headers })
|
|
232
|
+
if (infoRes.ok) {
|
|
233
|
+
const info = await infoRes.json()
|
|
234
|
+
const name = info.name || String(repo)
|
|
235
|
+
dest = name
|
|
236
|
+
} else {
|
|
340
237
|
dest = String(repo)
|
|
341
238
|
}
|
|
239
|
+
} catch {
|
|
240
|
+
dest = String(repo)
|
|
342
241
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
242
|
+
}
|
|
243
|
+
dest = path.resolve(dest)
|
|
244
|
+
const url = new URL(`/api/repositories/${repo}/snapshot`, server)
|
|
245
|
+
url.searchParams.set('branch', branch)
|
|
246
|
+
const res = await fetch(url.toString(), { headers })
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
const text = await res.text()
|
|
249
|
+
throw new Error(`Snapshot fetch failed: ${res.status} ${res.statusText} - ${text}`)
|
|
250
|
+
}
|
|
251
|
+
const data = await res.json()
|
|
252
|
+
const files = data.files || {}
|
|
253
|
+
const root = dest
|
|
254
|
+
for (const [p, content] of Object.entries(files)) {
|
|
255
|
+
const fullPath = path.join(root, p)
|
|
256
|
+
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true })
|
|
257
|
+
await fs.promises.writeFile(fullPath, content, 'utf8')
|
|
258
|
+
}
|
|
259
|
+
const metaDir = path.join(root, '.vcs-next')
|
|
260
|
+
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
261
|
+
const meta = { repoId: repo, branch, commitId: data.commitId || '', server, token: token || '' }
|
|
262
|
+
await fs.promises.writeFile(path.join(metaDir, 'remote.json'), JSON.stringify(meta, null, 2))
|
|
263
|
+
const gitDir = path.join(root, '.git')
|
|
264
|
+
const refsHeadsDir = path.join(gitDir, 'refs', 'heads')
|
|
265
|
+
await fs.promises.mkdir(refsHeadsDir, { recursive: true })
|
|
266
|
+
const headContent = `ref: refs/heads/${branch}\n`
|
|
267
|
+
await fs.promises.writeFile(path.join(gitDir, 'HEAD'), headContent, 'utf8')
|
|
268
|
+
const commitId = data.commitId || ''
|
|
269
|
+
await fs.promises.writeFile(path.join(refsHeadsDir, branch), commitId, 'utf8')
|
|
270
|
+
const gitConfig = [
|
|
271
|
+
'[core]',
|
|
272
|
+
'\trepositoryformatversion = 0',
|
|
273
|
+
'\tfilemode = true',
|
|
274
|
+
'\tbare = false',
|
|
275
|
+
'\tlogallrefupdates = true',
|
|
276
|
+
'',
|
|
277
|
+
'[vcs-next]',
|
|
278
|
+
`\tserver = ${server}`,
|
|
279
|
+
`\trepoId = ${repo}`,
|
|
280
|
+
`\tbranch = ${branch}`,
|
|
281
|
+
`\ttoken = ${token || ''}`
|
|
282
|
+
].join('\n')
|
|
283
|
+
await fs.promises.writeFile(path.join(gitDir, 'config'), gitConfig, 'utf8')
|
|
284
|
+
await fs.promises.writeFile(path.join(gitDir, 'vcs-next.json'), JSON.stringify({ ...meta }, null, 2))
|
|
285
|
+
try {
|
|
286
|
+
const branchesUrl = new URL(`/api/repositories/${repo}/branches`, server)
|
|
287
|
+
const branchesRes = await fetch(branchesUrl.toString(), { headers })
|
|
288
|
+
if (branchesRes.ok) {
|
|
289
|
+
const branchesData = await branchesRes.json()
|
|
290
|
+
const branches = Array.isArray(branchesData.branches) ? branchesData.branches : []
|
|
291
|
+
const allRefs = {}
|
|
292
|
+
for (const b of branches) {
|
|
293
|
+
const name = b.name || ''
|
|
294
|
+
const id = b.commitId || ''
|
|
295
|
+
if (!name) continue
|
|
296
|
+
allRefs[name] = id
|
|
297
|
+
await fs.promises.writeFile(path.join(refsHeadsDir, name), id, 'utf8')
|
|
298
|
+
}
|
|
299
|
+
await fs.promises.writeFile(path.join(gitDir, 'vcs-next-refs.json'), JSON.stringify(allRefs, null, 2))
|
|
351
300
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
await fs.promises.
|
|
360
|
-
await fs.promises.writeFile(fullPath, content, 'utf8')
|
|
301
|
+
} catch { }
|
|
302
|
+
try {
|
|
303
|
+
const commitsUrl = new URL(`/api/repositories/${repo}/commits`, server)
|
|
304
|
+
commitsUrl.searchParams.set('branch', branch)
|
|
305
|
+
const commitsRes = await fetch(commitsUrl.toString(), { headers })
|
|
306
|
+
if (commitsRes.ok) {
|
|
307
|
+
const commitsList = await commitsRes.json()
|
|
308
|
+
await fs.promises.writeFile(path.join(gitDir, 'vcs-next-commits.json'), JSON.stringify(commitsList || [], null, 2))
|
|
361
309
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
365
|
-
const meta = { repoId: repo, branch, commitId: data.commitId || '', server, token: token || '' }
|
|
366
|
-
await fs.promises.writeFile(path.join(metaDir, 'remote.json'), JSON.stringify(meta, null, 2))
|
|
367
|
-
const gitDir = path.join(root, '.git')
|
|
368
|
-
const refsHeadsDir = path.join(gitDir, 'refs', 'heads')
|
|
369
|
-
await fs.promises.mkdir(refsHeadsDir, { recursive: true })
|
|
370
|
-
const headContent = `ref: refs/heads/${branch}\\n`
|
|
371
|
-
await fs.promises.writeFile(path.join(gitDir, 'HEAD'), headContent, 'utf8')
|
|
372
|
-
const commitId = data.commitId || ''
|
|
373
|
-
await fs.promises.writeFile(path.join(refsHeadsDir, branch), commitId, 'utf8')
|
|
374
|
-
const gitConfig = [
|
|
375
|
-
'[core]',
|
|
376
|
-
'\\trepositoryformatversion = 0',
|
|
377
|
-
'\\tfilemode = true',
|
|
378
|
-
'\\tbare = false',
|
|
379
|
-
'\\tlogallrefupdates = true',
|
|
380
|
-
'',
|
|
381
|
-
'[vcs-next]',
|
|
382
|
-
`\\tserver = ${server}`,
|
|
383
|
-
`\\trepoId = ${repo}`,
|
|
384
|
-
`\\tbranch = ${branch}`,
|
|
385
|
-
`\\ttoken = ${token || ''}`
|
|
386
|
-
].join('\\n')
|
|
387
|
-
await fs.promises.writeFile(path.join(gitDir, 'config'), gitConfig, 'utf8')
|
|
388
|
-
await fs.promises.writeFile(path.join(gitDir, 'vcs-next.json'), JSON.stringify({ ...meta }, null, 2))
|
|
389
|
-
spinnerUpdate(spinner, 'Fetching branch information...')
|
|
390
|
-
try {
|
|
391
|
-
const branchesUrl = new URL(`/api/repositories/${repo}/branches`, server)
|
|
392
|
-
const branchesRes = await fetch(branchesUrl.toString(), { headers })
|
|
393
|
-
if (branchesRes.ok) {
|
|
394
|
-
const branchesData = await branchesRes.json()
|
|
395
|
-
const branches = Array.isArray(branchesData.branches) ? branchesData.branches : []
|
|
396
|
-
const allRefs = {}
|
|
397
|
-
for (const b of branches) {
|
|
398
|
-
const name = b.name || ''
|
|
399
|
-
const id = b.commitId || ''
|
|
400
|
-
if (!name) continue
|
|
401
|
-
allRefs[name] = id
|
|
402
|
-
await fs.promises.writeFile(path.join(refsHeadsDir, name), id, 'utf8')
|
|
403
|
-
}
|
|
404
|
-
await fs.promises.writeFile(path.join(gitDir, 'vcs-next-refs.json'), JSON.stringify(allRefs, null, 2))
|
|
405
|
-
}
|
|
406
|
-
} catch { }
|
|
407
|
-
spinnerUpdate(spinner, 'Fetching commit history...')
|
|
408
|
-
try {
|
|
409
|
-
const commitsUrl = new URL(`/api/repositories/${repo}/commits`, server)
|
|
410
|
-
commitsUrl.searchParams.set('branch', branch)
|
|
411
|
-
const commitsRes = await fetch(commitsUrl.toString(), { headers })
|
|
412
|
-
if (commitsRes.ok) {
|
|
413
|
-
const commitsList = await commitsRes.json()
|
|
414
|
-
await fs.promises.writeFile(path.join(gitDir, 'vcs-next-commits.json'), JSON.stringify(commitsList || [], null, 2))
|
|
415
|
-
}
|
|
416
|
-
} catch { }
|
|
417
|
-
spinnerSuccess(spinner, `Cloned to ${dest} (${fileCount} files)`)
|
|
418
|
-
print('Clone complete', opts.json === 'true')
|
|
419
|
-
} catch (err) {
|
|
420
|
-
spinnerFail(spinner, 'Clone failed')
|
|
421
|
-
throw err
|
|
422
|
-
}
|
|
310
|
+
} catch { }
|
|
311
|
+
print('Clone complete', opts.json === 'true')
|
|
423
312
|
}
|
|
424
313
|
|
|
425
314
|
function readRemoteMeta(dir) {
|
|
@@ -578,73 +467,6 @@ async function cmdDiff(opts) {
|
|
|
578
467
|
|
|
579
468
|
const filePath = opts.path
|
|
580
469
|
const commitId = opts.commit
|
|
581
|
-
const commit1 = opts.commit1
|
|
582
|
-
const commit2 = opts.commit2
|
|
583
|
-
const showStat = opts.stat === 'true'
|
|
584
|
-
|
|
585
|
-
// Handle diff between two commits: git diff <commit1> <commit2>
|
|
586
|
-
if (commit1 && commit2) {
|
|
587
|
-
const snap1 = await fetchSnapshotByCommit(server, meta.repoId, commit1, token)
|
|
588
|
-
const snap2 = await fetchSnapshotByCommit(server, meta.repoId, commit2, token)
|
|
589
|
-
const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(snap1.files), ...Object.keys(snap2.files)]))
|
|
590
|
-
|
|
591
|
-
let added = 0, deleted = 0, modified = 0
|
|
592
|
-
const stats = []
|
|
593
|
-
|
|
594
|
-
for (const p of files) {
|
|
595
|
-
const content1 = snap1.files[p] !== undefined ? String(snap1.files[p]) : null
|
|
596
|
-
const content2 = snap2.files[p] !== undefined ? String(snap2.files[p]) : null
|
|
597
|
-
if (content1 !== content2) {
|
|
598
|
-
if (content1 === null) {
|
|
599
|
-
added++
|
|
600
|
-
if (showStat) stats.push({ path: p, added: content2.split(/\r?\n/).length, deleted: 0 })
|
|
601
|
-
} else if (content2 === null) {
|
|
602
|
-
deleted++
|
|
603
|
-
if (showStat) stats.push({ path: p, added: 0, deleted: content1.split(/\r?\n/).length })
|
|
604
|
-
} else {
|
|
605
|
-
modified++
|
|
606
|
-
const lines1 = content1.split(/\r?\n/)
|
|
607
|
-
const lines2 = content2.split(/\r?\n/)
|
|
608
|
-
const diff = Math.abs(lines2.length - lines1.length)
|
|
609
|
-
if (showStat) stats.push({ path: p, added: lines2.length > lines1.length ? diff : 0, deleted: lines1.length > lines2.length ? diff : 0 })
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (!showStat) {
|
|
613
|
-
if (opts.json === 'true') {
|
|
614
|
-
print({ path: p, old: content1, new: content2 }, true)
|
|
615
|
-
} else {
|
|
616
|
-
process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'dim'))
|
|
617
|
-
// Show full diff (same as before)
|
|
618
|
-
const oldLines = content1 ? content1.split(/\r?\n/) : []
|
|
619
|
-
const newLines = content2 ? content2.split(/\r?\n/) : []
|
|
620
|
-
const maxLen = Math.max(oldLines.length, newLines.length)
|
|
621
|
-
for (let i = 0; i < maxLen; i++) {
|
|
622
|
-
const oldLine = oldLines[i]
|
|
623
|
-
const newLine = newLines[i]
|
|
624
|
-
if (oldLine !== newLine) {
|
|
625
|
-
if (oldLine !== undefined) process.stdout.write(color(`-${oldLine}\n`, 'red'))
|
|
626
|
-
if (newLine !== undefined) process.stdout.write(color(`+${newLine}\n`, 'green'))
|
|
627
|
-
} else if (oldLine !== undefined) {
|
|
628
|
-
process.stdout.write(` ${oldLine}\n`)
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (showStat) {
|
|
637
|
-
if (opts.json === 'true') {
|
|
638
|
-
print({ added, deleted, modified, files: stats }, true)
|
|
639
|
-
} else {
|
|
640
|
-
for (const stat of stats) {
|
|
641
|
-
process.stdout.write(` ${stat.path} | ${stat.added + stat.deleted} ${stat.added > 0 ? color('+' + stat.added, 'green') : ''}${stat.deleted > 0 ? color('-' + stat.deleted, 'red') : ''}\n`)
|
|
642
|
-
}
|
|
643
|
-
process.stdout.write(` ${stats.length} file${stats.length !== 1 ? 's' : ''} changed, ${added} insertion${added !== 1 ? 's' : ''}(+), ${deleted} deletion${deleted !== 1 ? 's' : ''}(-)\n`)
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return
|
|
647
|
-
}
|
|
648
470
|
|
|
649
471
|
if (commitId) {
|
|
650
472
|
// Show diff for specific commit
|
|
@@ -654,40 +476,6 @@ async function cmdDiff(opts) {
|
|
|
654
476
|
const parentSnap = parentId ? await fetchSnapshotByCommit(server, meta.repoId, parentId, token) : { files: {}, commitId: '' }
|
|
655
477
|
|
|
656
478
|
const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)]))
|
|
657
|
-
|
|
658
|
-
if (showStat) {
|
|
659
|
-
let added = 0, deleted = 0, modified = 0
|
|
660
|
-
const stats = []
|
|
661
|
-
for (const p of files) {
|
|
662
|
-
const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : null
|
|
663
|
-
const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : null
|
|
664
|
-
if (oldContent !== newContent) {
|
|
665
|
-
if (oldContent === null) {
|
|
666
|
-
added++
|
|
667
|
-
if (showStat) stats.push({ path: p, added: newContent.split(/\r?\n/).length, deleted: 0 })
|
|
668
|
-
} else if (newContent === null) {
|
|
669
|
-
deleted++
|
|
670
|
-
if (showStat) stats.push({ path: p, added: 0, deleted: oldContent.split(/\r?\n/).length })
|
|
671
|
-
} else {
|
|
672
|
-
modified++
|
|
673
|
-
const oldLines = oldContent.split(/\r?\n/)
|
|
674
|
-
const newLines = newContent.split(/\r?\n/)
|
|
675
|
-
const diff = Math.abs(newLines.length - oldLines.length)
|
|
676
|
-
if (showStat) stats.push({ path: p, added: newLines.length > oldLines.length ? diff : 0, deleted: oldLines.length > newLines.length ? diff : 0 })
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (opts.json === 'true') {
|
|
681
|
-
print({ added, deleted, modified, files: stats }, true)
|
|
682
|
-
} else {
|
|
683
|
-
for (const stat of stats) {
|
|
684
|
-
process.stdout.write(` ${stat.path} | ${stat.added + stat.deleted} ${stat.added > 0 ? color('+' + stat.added, 'green') : ''}${stat.deleted > 0 ? color('-' + stat.deleted, 'red') : ''}\n`)
|
|
685
|
-
}
|
|
686
|
-
process.stdout.write(` ${stats.length} file${stats.length !== 1 ? 's' : ''} changed, ${added} insertion${added !== 1 ? 's' : ''}(+), ${deleted} deletion${deleted !== 1 ? 's' : ''}(-)\n`)
|
|
687
|
-
}
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
|
|
691
479
|
for (const p of files) {
|
|
692
480
|
const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : null
|
|
693
481
|
const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : null
|
|
@@ -773,31 +561,6 @@ async function cmdRm(opts) {
|
|
|
773
561
|
const cfg = loadConfig()
|
|
774
562
|
const server = getServer(opts, cfg) || meta.server
|
|
775
563
|
const token = getToken(opts, cfg) || meta.token
|
|
776
|
-
|
|
777
|
-
// Handle --cached flag (remove from index but keep file)
|
|
778
|
-
if (opts.cached === 'true') {
|
|
779
|
-
// Mark file for deletion in next commit but don't delete from filesystem
|
|
780
|
-
const metaDir = path.join(dir, '.vcs-next')
|
|
781
|
-
const localPath = path.join(metaDir, 'local.json')
|
|
782
|
-
let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null, removedFiles: [] }
|
|
783
|
-
try {
|
|
784
|
-
const s = await fs.promises.readFile(localPath, 'utf8')
|
|
785
|
-
localMeta = JSON.parse(s)
|
|
786
|
-
} catch { }
|
|
787
|
-
if (!localMeta.removedFiles) localMeta.removedFiles = []
|
|
788
|
-
if (!localMeta.removedFiles.includes(pathArg)) {
|
|
789
|
-
localMeta.removedFiles.push(pathArg)
|
|
790
|
-
}
|
|
791
|
-
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
792
|
-
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
793
|
-
if (opts.json === 'true') {
|
|
794
|
-
print({ removed: pathArg, cached: true }, true)
|
|
795
|
-
} else {
|
|
796
|
-
process.stdout.write(color(`Removed '${pathArg}' from index (file kept in working directory)\n`, 'green'))
|
|
797
|
-
}
|
|
798
|
-
return
|
|
799
|
-
}
|
|
800
|
-
|
|
801
564
|
const u = new URL(`/api/repositories/${meta.repoId}/files`, server)
|
|
802
565
|
u.searchParams.set('branch', meta.branch)
|
|
803
566
|
u.searchParams.set('path', pathArg)
|
|
@@ -816,93 +579,40 @@ async function cmdCommit(opts) {
|
|
|
816
579
|
const message = opts.message || ''
|
|
817
580
|
if (!message) throw new Error('Missing --message')
|
|
818
581
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if (opts.
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
spinnerUpdate(spinner, 'Amending last commit...')
|
|
830
|
-
// Get the last commit and use its message if no new message provided
|
|
831
|
-
const meta = readRemoteMeta(dir)
|
|
832
|
-
const cfg = loadConfig()
|
|
833
|
-
const server = getServer(opts, cfg) || meta.server
|
|
834
|
-
const token = getToken(opts, cfg) || meta.token
|
|
835
|
-
const url = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
836
|
-
url.searchParams.set('branch', meta.branch)
|
|
837
|
-
url.searchParams.set('limit', '1')
|
|
838
|
-
const commits = await request('GET', url.toString(), null, token)
|
|
839
|
-
if (Array.isArray(commits) && commits.length > 0 && !message) {
|
|
840
|
-
opts.message = commits[0].message || 'Amended commit'
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Check for unresolved conflicts
|
|
845
|
-
spinnerUpdate(spinner, 'Checking for conflicts...')
|
|
846
|
-
const unresolvedConflicts = await checkForUnresolvedConflicts(dir)
|
|
847
|
-
if (unresolvedConflicts.length > 0) {
|
|
848
|
-
spinnerFail(spinner, 'Cannot commit with unresolved conflicts')
|
|
849
|
-
if (opts.json === 'true') {
|
|
850
|
-
print({ error: 'Cannot commit with unresolved conflicts', conflicts: unresolvedConflicts }, true)
|
|
851
|
-
} else {
|
|
852
|
-
process.stderr.write(color('Error: Cannot commit with unresolved conflicts\n', 'red'))
|
|
853
|
-
process.stderr.write(color('Conflicts in files:\n', 'yellow'))
|
|
854
|
-
for (const p of unresolvedConflicts) {
|
|
855
|
-
process.stderr.write(color(` ${p}\n`, 'red'))
|
|
856
|
-
}
|
|
857
|
-
process.stderr.write(color('\nResolve conflicts manually before committing.\n', 'yellow'))
|
|
858
|
-
}
|
|
859
|
-
return
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
spinnerUpdate(spinner, 'Collecting changes...')
|
|
863
|
-
const metaDir = path.join(dir, '.vcs-next')
|
|
864
|
-
const localPath = path.join(metaDir, 'local.json')
|
|
865
|
-
let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null }
|
|
866
|
-
try {
|
|
867
|
-
const s = await fs.promises.readFile(localPath, 'utf8')
|
|
868
|
-
localMeta = JSON.parse(s)
|
|
869
|
-
} catch { }
|
|
870
|
-
const local = await collectLocal(dir)
|
|
871
|
-
const files = {}
|
|
872
|
-
for (const [p, v] of Object.entries(local)) files[p] = v.content
|
|
873
|
-
|
|
874
|
-
// Execute pre-commit hook
|
|
875
|
-
spinnerUpdate(spinner, 'Running pre-commit hook...')
|
|
876
|
-
try {
|
|
877
|
-
const hookResult = await hooks.executeHook(dir, 'pre-commit', { message, files: Object.keys(files) })
|
|
878
|
-
if (hookResult.executed && hookResult.exitCode !== 0) {
|
|
879
|
-
spinnerFail(spinner, 'Pre-commit hook failed')
|
|
880
|
-
throw new Error('pre-commit hook failed')
|
|
582
|
+
// Check for unresolved conflicts
|
|
583
|
+
const unresolvedConflicts = await checkForUnresolvedConflicts(dir)
|
|
584
|
+
if (unresolvedConflicts.length > 0) {
|
|
585
|
+
if (opts.json === 'true') {
|
|
586
|
+
print({ error: 'Cannot commit with unresolved conflicts', conflicts: unresolvedConflicts }, true)
|
|
587
|
+
} else {
|
|
588
|
+
process.stderr.write(color('Error: Cannot commit with unresolved conflicts\n', 'red'))
|
|
589
|
+
process.stderr.write(color('Conflicts in files:\n', 'yellow'))
|
|
590
|
+
for (const p of unresolvedConflicts) {
|
|
591
|
+
process.stderr.write(color(` ${p}\n`, 'red'))
|
|
881
592
|
}
|
|
882
|
-
|
|
883
|
-
if (err.message === 'pre-commit hook failed') throw err
|
|
884
|
-
// Hook doesn't exist or other error, continue
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
localMeta.pendingCommit = { message, files, createdAt: Date.now() }
|
|
888
|
-
// Clear conflicts if they were resolved
|
|
889
|
-
if (localMeta.conflicts) {
|
|
890
|
-
delete localMeta.conflicts
|
|
593
|
+
process.stderr.write(color('\nResolve conflicts manually before committing.\n', 'yellow'))
|
|
891
594
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
// Execute post-commit hook
|
|
896
|
-
try {
|
|
897
|
-
await hooks.executeHook(dir, 'post-commit', { message, files: Object.keys(files) })
|
|
898
|
-
} catch { }
|
|
595
|
+
return
|
|
596
|
+
}
|
|
899
597
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
|
|
598
|
+
const metaDir = path.join(dir, '.vcs-next')
|
|
599
|
+
const localPath = path.join(metaDir, 'local.json')
|
|
600
|
+
let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null }
|
|
601
|
+
try {
|
|
602
|
+
const s = await fs.promises.readFile(localPath, 'utf8')
|
|
603
|
+
localMeta = JSON.parse(s)
|
|
604
|
+
} catch { }
|
|
605
|
+
const local = await collectLocal(dir)
|
|
606
|
+
const files = {}
|
|
607
|
+
for (const [p, v] of Object.entries(local)) files[p] = v.content
|
|
608
|
+
localMeta.pendingCommit = { message, files, createdAt: Date.now() }
|
|
609
|
+
// Clear conflicts if they were resolved
|
|
610
|
+
if (localMeta.conflicts) {
|
|
611
|
+
delete localMeta.conflicts
|
|
905
612
|
}
|
|
613
|
+
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
614
|
+
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
615
|
+
print({ pendingCommit: message }, opts.json === 'true')
|
|
906
616
|
}
|
|
907
617
|
|
|
908
618
|
async function pullToDir(repo, branch, dir, server, token) {
|
|
@@ -966,15 +676,8 @@ async function cmdPull(opts) {
|
|
|
966
676
|
const cfg = loadConfig()
|
|
967
677
|
const server = getServer(opts, cfg) || meta.server
|
|
968
678
|
const token = getToken(opts, cfg) || meta.token
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
await pullToDir(meta.repoId, meta.branch, dir, server, token)
|
|
972
|
-
spinnerSuccess(spinner, `Pulled latest changes from '${meta.branch}'`)
|
|
973
|
-
print('Pull complete', opts.json === 'true')
|
|
974
|
-
} catch (err) {
|
|
975
|
-
spinnerFail(spinner, 'Pull failed')
|
|
976
|
-
throw err
|
|
977
|
-
}
|
|
679
|
+
await pullToDir(meta.repoId, meta.branch, dir, server, token)
|
|
680
|
+
print('Pull complete', opts.json === 'true')
|
|
978
681
|
}
|
|
979
682
|
|
|
980
683
|
async function cmdFetch(opts) {
|
|
@@ -1030,14 +733,6 @@ async function fetchRemoteSnapshot(server, repo, branch, token) {
|
|
|
1030
733
|
if (branch) url.searchParams.set('branch', branch)
|
|
1031
734
|
const res = await fetch(url.toString(), { headers })
|
|
1032
735
|
if (!res.ok) {
|
|
1033
|
-
// For new/empty repos, return empty snapshot instead of throwing
|
|
1034
|
-
if (res.status === 404 || res.status === 500) {
|
|
1035
|
-
// Check if this is a "new repo" scenario
|
|
1036
|
-
const text = await res.text()
|
|
1037
|
-
if (text.includes('not found') || text.includes('empty') || text.includes('no commits') || res.status === 500) {
|
|
1038
|
-
return { files: {}, commitId: '' }
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
736
|
const text = await res.text()
|
|
1042
737
|
throw new Error(text || 'snapshot failed')
|
|
1043
738
|
}
|
|
@@ -1371,190 +1066,73 @@ async function cmdPush(opts) {
|
|
|
1371
1066
|
const metaDir = path.join(dir, '.vcs-next')
|
|
1372
1067
|
const localPath = path.join(metaDir, 'local.json')
|
|
1373
1068
|
let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null }
|
|
1374
|
-
const spinner = createSpinner('Preparing to push...', opts.json)
|
|
1375
1069
|
try {
|
|
1376
1070
|
const s = await fs.promises.readFile(localPath, 'utf8')
|
|
1377
1071
|
localMeta = JSON.parse(s)
|
|
1378
1072
|
} catch { }
|
|
1379
1073
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
process.stderr.write(color(
|
|
1390
|
-
process.stderr.write(color('Conflicts in files:\\n', 'yellow'))
|
|
1391
|
-
for (const p of unresolvedConflicts) {
|
|
1392
|
-
process.stderr.write(color(` ${p}\\n`, 'red'))
|
|
1393
|
-
}
|
|
1394
|
-
process.stderr.write(color('\\nResolve conflicts manually, then try pushing again.\\n', 'yellow'))
|
|
1074
|
+
// Check for unresolved conflicts in files
|
|
1075
|
+
const unresolvedConflicts = await checkForUnresolvedConflicts(dir)
|
|
1076
|
+
if (unresolvedConflicts.length > 0) {
|
|
1077
|
+
if (opts.json === 'true') {
|
|
1078
|
+
print({ error: 'Unresolved conflicts', conflicts: unresolvedConflicts }, true)
|
|
1079
|
+
} else {
|
|
1080
|
+
process.stderr.write(color('Error: Cannot push with unresolved conflicts\n', 'red'))
|
|
1081
|
+
process.stderr.write(color('Conflicts in files:\n', 'yellow'))
|
|
1082
|
+
for (const p of unresolvedConflicts) {
|
|
1083
|
+
process.stderr.write(color(` ${p}\n`, 'red'))
|
|
1395
1084
|
}
|
|
1396
|
-
|
|
1085
|
+
process.stderr.write(color('\nResolve conflicts manually, then try pushing again.\n', 'yellow'))
|
|
1397
1086
|
}
|
|
1087
|
+
return
|
|
1088
|
+
}
|
|
1398
1089
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const changedRemote = String(r) !== String(b)
|
|
1424
|
-
|
|
1425
|
-
// Check if local file has conflict markers (unresolved)
|
|
1426
|
-
if (l && hasConflictMarkers(l)) {
|
|
1427
|
-
const line = firstDiffLine(l || '', r || '')
|
|
1428
|
-
conflicts.push({ path: p, line, reason: 'Unresolved conflict markers in file' })
|
|
1429
|
-
} else if (changedLocal && changedRemote && String(l) !== String(r)) {
|
|
1430
|
-
// Both changed and different - but if local is the "resolved" version, use it
|
|
1431
|
-
// This happens when user resolved a conflict and is pushing the resolution
|
|
1432
|
-
// The local version wins if it doesn't have conflict markers
|
|
1433
|
-
if (l !== null) merged[p] = l
|
|
1434
|
-
} else if (changedLocal && !changedRemote) {
|
|
1435
|
-
// Local changed, use local version
|
|
1436
|
-
if (l !== null) merged[p] = l
|
|
1437
|
-
} else if (!changedLocal && changedRemote) {
|
|
1438
|
-
// Remote changed, use remote version
|
|
1439
|
-
if (r !== null) merged[p] = r
|
|
1440
|
-
} else {
|
|
1441
|
-
// No changes - include the file from whatever source has it
|
|
1442
|
-
if (l !== null) merged[p] = l
|
|
1443
|
-
else if (r !== null) merged[p] = r
|
|
1444
|
-
else if (b !== null) merged[p] = b
|
|
1445
|
-
}
|
|
1090
|
+
const cfg = loadConfig()
|
|
1091
|
+
const server = getServer(opts, cfg) || remoteMeta.server
|
|
1092
|
+
const token = getToken(opts, cfg) || remoteMeta.token
|
|
1093
|
+
const remote = await fetchRemoteSnapshot(server, remoteMeta.repoId, remoteMeta.branch, token)
|
|
1094
|
+
const base = localMeta.baseFiles || {}
|
|
1095
|
+
const local = await collectLocal(dir)
|
|
1096
|
+
const conflicts = []
|
|
1097
|
+
const merged = {}
|
|
1098
|
+
const paths = new Set([...Object.keys(base), ...Object.keys(remote.files), ...Object.keys(local).map(k => k)])
|
|
1099
|
+
for (const p of paths) {
|
|
1100
|
+
const b = p in base ? base[p] : null
|
|
1101
|
+
const r = p in remote.files ? remote.files[p] : null
|
|
1102
|
+
const l = p in local ? local[p].content : null
|
|
1103
|
+
const changedLocal = String(l) !== String(b)
|
|
1104
|
+
const changedRemote = String(r) !== String(b)
|
|
1105
|
+
if (changedLocal && changedRemote && String(l) !== String(r)) {
|
|
1106
|
+
const line = firstDiffLine(l || '', r || '')
|
|
1107
|
+
conflicts.push({ path: p, line })
|
|
1108
|
+
} else if (changedLocal && !changedRemote) {
|
|
1109
|
+
if (l !== null) merged[p] = l
|
|
1110
|
+
} else if (!changedLocal && changedRemote) {
|
|
1111
|
+
if (r !== null) merged[p] = r
|
|
1112
|
+
} else {
|
|
1113
|
+
if (b !== null) merged[p] = b
|
|
1446
1114
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1115
|
+
}
|
|
1116
|
+
if (conflicts.length > 0) {
|
|
1117
|
+
if (opts.json === 'true') {
|
|
1118
|
+
print({ conflicts }, true)
|
|
1119
|
+
} else {
|
|
1120
|
+
process.stderr.write(color('Error: Cannot push with conflicts\n', 'red'))
|
|
1121
|
+
process.stderr.write(color('Conflicts detected:\n', 'yellow'))
|
|
1451
1122
|
for (const c of conflicts) {
|
|
1452
|
-
|
|
1453
|
-
const remoteContent = remote.files[c.path] || ''
|
|
1454
|
-
|
|
1455
|
-
// Create conflict-marked content
|
|
1456
|
-
const conflictContent = [
|
|
1457
|
-
'<<<<<<< LOCAL (your changes)',
|
|
1458
|
-
localContent,
|
|
1459
|
-
'=======',
|
|
1460
|
-
remoteContent,
|
|
1461
|
-
'>>>>>>> REMOTE (server changes)'
|
|
1462
|
-
].join('\n')
|
|
1463
|
-
|
|
1464
|
-
// Write to local file
|
|
1465
|
-
const conflictPath = path.join(dir, c.path)
|
|
1466
|
-
await fs.promises.mkdir(path.dirname(conflictPath), { recursive: true })
|
|
1467
|
-
await fs.promises.writeFile(conflictPath, conflictContent, 'utf8')
|
|
1123
|
+
process.stderr.write(color(` ${c.path}:${c.line}\n`, 'red'))
|
|
1468
1124
|
}
|
|
1469
|
-
|
|
1470
|
-
if (opts.json === 'true') {
|
|
1471
|
-
print({ conflicts, message: 'Conflict markers written to files. Resolve them and try again.' }, true)
|
|
1472
|
-
} else {
|
|
1473
|
-
process.stderr.write(color('\nConflicts detected! The following files have been updated with conflict markers:\n', 'red'))
|
|
1474
|
-
for (const c of conflicts) {
|
|
1475
|
-
process.stderr.write(color(` ${c.path}\n`, 'yellow'))
|
|
1476
|
-
}
|
|
1477
|
-
process.stderr.write(color('\nTo resolve:\n', 'cyan'))
|
|
1478
|
-
process.stderr.write(color(' 1. Open the files above and look for conflict markers:\n', 'dim'))
|
|
1479
|
-
process.stderr.write(color(' <<<<<<< LOCAL (your changes)\n', 'dim'))
|
|
1480
|
-
process.stderr.write(color(' ... your version ...\n', 'dim'))
|
|
1481
|
-
process.stderr.write(color(' =======\n', 'dim'))
|
|
1482
|
-
process.stderr.write(color(' ... server version ...\n', 'dim'))
|
|
1483
|
-
process.stderr.write(color(' >>>>>>> REMOTE (server changes)\n', 'dim'))
|
|
1484
|
-
process.stderr.write(color(' 2. Edit the files to keep the changes you want\n', 'dim'))
|
|
1485
|
-
process.stderr.write(color(' 3. Remove the conflict markers\n', 'dim'))
|
|
1486
|
-
process.stderr.write(color(' 4. Run: resulgit add . && resulgit commit -m "Resolved conflicts" && resulgit push\n\n', 'dim'))
|
|
1487
|
-
}
|
|
1488
|
-
return
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// Check if there are any files to push
|
|
1492
|
-
if (Object.keys(merged).length === 0) {
|
|
1493
|
-
spinnerFail(spinner, 'Nothing to push')
|
|
1494
|
-
if (opts.json === 'true') {
|
|
1495
|
-
print({ error: 'No files to push' }, true)
|
|
1496
|
-
} else {
|
|
1497
|
-
process.stdout.write('No files to push. Add some files first.\n')
|
|
1498
|
-
}
|
|
1499
|
-
return
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// Execute pre-push hook
|
|
1503
|
-
spinnerUpdate(spinner, 'Running pre-push hook...')
|
|
1504
|
-
try {
|
|
1505
|
-
const hookResult = await hooks.executeHook(dir, 'pre-push', { branch: remoteMeta.branch, files: Object.keys(merged) })
|
|
1506
|
-
if (hookResult.executed && hookResult.exitCode !== 0) {
|
|
1507
|
-
spinnerFail(spinner, 'Pre-push hook failed')
|
|
1508
|
-
throw new Error('pre-push hook failed')
|
|
1509
|
-
}
|
|
1510
|
-
} catch (err) {
|
|
1511
|
-
if (err.message === 'pre-push hook failed') throw err
|
|
1512
|
-
// Hook doesn't exist or other error, continue
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// Step 1: Upload files as blobs
|
|
1516
|
-
spinnerUpdate(spinner, `Uploading ${Object.keys(merged).length} file(s)...`)
|
|
1517
|
-
let blobMap = {}
|
|
1518
|
-
try {
|
|
1519
|
-
blobMap = await uploadBlobs(server, remoteMeta.repoId, merged, token)
|
|
1520
|
-
} catch (err) {
|
|
1521
|
-
// If blob upload fails, fall back to sending content directly
|
|
1522
|
-
// (This handles servers that accept content in commits endpoint)
|
|
1523
|
-
spinnerUpdate(spinner, 'Blob upload not available, sending files directly...')
|
|
1524
|
-
blobMap = merged // Use content directly
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// Step 2: Create commit with blob IDs (or content if blob upload failed)
|
|
1528
|
-
const commitFiles = {}
|
|
1529
|
-
for (const [filePath, content] of Object.entries(merged)) {
|
|
1530
|
-
// Use blob ID if available, otherwise use content
|
|
1531
|
-
commitFiles[filePath] = blobMap[filePath] || content
|
|
1532
1125
|
}
|
|
1533
|
-
|
|
1534
|
-
const body = {
|
|
1535
|
-
message: localMeta.pendingCommit?.message || (opts.message || 'Push'),
|
|
1536
|
-
files: commitFiles,
|
|
1537
|
-
branchName: remoteMeta.branch
|
|
1538
|
-
}
|
|
1539
|
-
spinnerUpdate(spinner, `Creating commit on '${remoteMeta.branch}'...`)
|
|
1540
|
-
const url = new URL(`/api/repositories/${remoteMeta.repoId}/commits`, server).toString()
|
|
1541
|
-
const data = await request('POST', url, body, token)
|
|
1542
|
-
localMeta.baseCommitId = data.id || remote.commitId || ''
|
|
1543
|
-
localMeta.baseFiles = merged
|
|
1544
|
-
localMeta.pendingCommit = null
|
|
1545
|
-
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
1546
|
-
|
|
1547
|
-
// Execute post-push hook
|
|
1548
|
-
try {
|
|
1549
|
-
await hooks.executeHook(dir, 'post-push', { branch: remoteMeta.branch, commitId: localMeta.baseCommitId })
|
|
1550
|
-
} catch { }
|
|
1551
|
-
|
|
1552
|
-
spinnerSuccess(spinner, `Pushed to '${remoteMeta.branch}' (commit: ${(data.id || '').slice(0, 7)})`)
|
|
1553
|
-
print({ pushed: localMeta.baseCommitId }, opts.json === 'true')
|
|
1554
|
-
} catch (err) {
|
|
1555
|
-
spinnerFail(spinner, 'Push failed')
|
|
1556
|
-
throw err
|
|
1126
|
+
return
|
|
1557
1127
|
}
|
|
1128
|
+
const body = { message: localMeta.pendingCommit?.message || (opts.message || 'Push'), files: merged, branchName: remoteMeta.branch }
|
|
1129
|
+
const url = new URL(`/api/repositories/${remoteMeta.repoId}/commits`, server).toString()
|
|
1130
|
+
const data = await request('POST', url, body, token)
|
|
1131
|
+
localMeta.baseCommitId = data.id || remote.commitId || ''
|
|
1132
|
+
localMeta.baseFiles = merged
|
|
1133
|
+
localMeta.pendingCommit = null
|
|
1134
|
+
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
1135
|
+
print({ pushed: localMeta.baseCommitId }, opts.json === 'true')
|
|
1558
1136
|
}
|
|
1559
1137
|
|
|
1560
1138
|
async function cmdMerge(opts) {
|
|
@@ -1896,20 +1474,7 @@ async function cmdBranch(sub, opts) {
|
|
|
1896
1474
|
if (m) current = m[1]
|
|
1897
1475
|
} catch { }
|
|
1898
1476
|
process.stdout.write(color('Branches:\n', 'bold'))
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
// Handle --sort option
|
|
1902
|
-
if (opts.sort) {
|
|
1903
|
-
const sortBy = opts.sort.replace(/^-/, '') // Remove leading dash
|
|
1904
|
-
if (sortBy === 'committerdate' || sortBy === '-committerdate') {
|
|
1905
|
-
list.sort((a, b) => {
|
|
1906
|
-
const dateA = new Date(a.date || 0).getTime()
|
|
1907
|
-
const dateB = new Date(b.date || 0).getTime()
|
|
1908
|
-
return sortBy.startsWith('-') ? dateB - dateA : dateA - dateB
|
|
1909
|
-
})
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1477
|
+
const list = (data.branches || []).map(b => ({ name: b.name, commitId: b.commitId || '' }))
|
|
1913
1478
|
for (const b of list) {
|
|
1914
1479
|
const isCur = b.name === current
|
|
1915
1480
|
const mark = isCur ? color('*', 'green') : ' '
|
|
@@ -1933,17 +1498,12 @@ async function cmdBranch(sub, opts) {
|
|
|
1933
1498
|
if (sub === 'delete') {
|
|
1934
1499
|
const name = opts.name
|
|
1935
1500
|
if (!name) throw new Error('Missing --name')
|
|
1936
|
-
const force = opts.force === 'true' || opts.D === 'true'
|
|
1937
1501
|
const u = new URL(`/api/repositories/${meta.repoId}/branches`, server)
|
|
1938
1502
|
u.searchParams.set('name', name)
|
|
1939
|
-
u.searchParams.set('force', force ? 'true' : 'false')
|
|
1940
1503
|
const headers = token ? { Authorization: `Bearer ${token}` } : {}
|
|
1941
1504
|
const res = await fetch(u.toString(), { method: 'DELETE', headers })
|
|
1942
1505
|
if (!res.ok) {
|
|
1943
1506
|
const body = await res.text().catch(() => '')
|
|
1944
|
-
if (force) {
|
|
1945
|
-
throw new Error(`Branch force delete failed: ${res.status} ${res.statusText} ${body}`)
|
|
1946
|
-
}
|
|
1947
1507
|
throw new Error(`Branch delete failed: ${res.status} ${res.statusText} ${body}`)
|
|
1948
1508
|
}
|
|
1949
1509
|
const data = await res.json()
|
|
@@ -1972,24 +1532,10 @@ async function cmdSwitch(opts) {
|
|
|
1972
1532
|
const dir = path.resolve(opts.dir || '.')
|
|
1973
1533
|
const meta = readRemoteMeta(dir)
|
|
1974
1534
|
const branch = opts.branch
|
|
1975
|
-
const create = opts.create === 'true' || opts.c === 'true'
|
|
1976
1535
|
if (!branch) throw new Error('Missing --branch')
|
|
1977
1536
|
const cfg = loadConfig()
|
|
1978
1537
|
const server = getServer(opts, cfg) || meta.server
|
|
1979
1538
|
const token = getToken(opts, cfg) || meta.token
|
|
1980
|
-
|
|
1981
|
-
// Handle -c flag (create and switch)
|
|
1982
|
-
if (create) {
|
|
1983
|
-
// Check if branch exists
|
|
1984
|
-
const branchesUrl = new URL(`/api/repositories/${meta.repoId}/branches`, server)
|
|
1985
|
-
const branchesData = await request('GET', branchesUrl.toString(), null, token)
|
|
1986
|
-
const exists = (branchesData.branches || []).some(b => b.name === branch)
|
|
1987
|
-
if (!exists) {
|
|
1988
|
-
// Create the branch
|
|
1989
|
-
await cmdBranch('create', { dir, name: branch, base: meta.branch, repo: meta.repoId, server, token })
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
1539
|
await pullToDir(meta.repoId, branch, dir, server, token)
|
|
1994
1540
|
print({ repoId: meta.repoId, branch, dir }, opts.json === 'true')
|
|
1995
1541
|
}
|
|
@@ -2344,28 +1890,10 @@ async function cmdReset(opts) {
|
|
|
2344
1890
|
const server = getServer(opts, cfg) || meta.server
|
|
2345
1891
|
const token = getToken(opts, cfg) || meta.token
|
|
2346
1892
|
|
|
2347
|
-
|
|
1893
|
+
const commitId = opts.commit || 'HEAD'
|
|
2348
1894
|
const mode = opts.mode || 'mixed' // soft, mixed, hard
|
|
2349
1895
|
const filePath = opts.path
|
|
2350
1896
|
|
|
2351
|
-
// Handle HEAD^ syntax (parent commit)
|
|
2352
|
-
if (commitId === 'HEAD^' || commitId.endsWith('^')) {
|
|
2353
|
-
const baseCommit = commitId.replace(/\^+$/, '')
|
|
2354
|
-
const targetCommit = baseCommit === 'HEAD' ? 'HEAD' : baseCommit
|
|
2355
|
-
let targetSnap
|
|
2356
|
-
if (targetCommit === 'HEAD') {
|
|
2357
|
-
targetSnap = await fetchRemoteSnapshot(server, meta.repoId, meta.branch, token)
|
|
2358
|
-
} else {
|
|
2359
|
-
targetSnap = await fetchSnapshotByCommit(server, meta.repoId, targetCommit, token)
|
|
2360
|
-
}
|
|
2361
|
-
const commit = await fetchCommitMeta(server, meta.repoId, targetSnap.commitId, token)
|
|
2362
|
-
const parentId = (Array.isArray(commit.parents) && commit.parents[0]) || ''
|
|
2363
|
-
if (!parentId) {
|
|
2364
|
-
throw new Error('No parent commit found')
|
|
2365
|
-
}
|
|
2366
|
-
commitId = parentId
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
1897
|
if (filePath) {
|
|
2370
1898
|
// Reset specific file (unstage)
|
|
2371
1899
|
const metaDir = path.join(dir, '.vcs-next')
|
|
@@ -2436,78 +1964,14 @@ async function cmdReset(opts) {
|
|
|
2436
1964
|
}
|
|
2437
1965
|
|
|
2438
1966
|
async function cmdInit(opts) {
|
|
1967
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2439
1968
|
const cfg = loadConfig()
|
|
2440
1969
|
const server = getServer(opts, cfg)
|
|
2441
1970
|
const token = getToken(opts, cfg)
|
|
2442
|
-
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
// Determine target directory:
|
|
2448
|
-
// - If --dir is specified, use that
|
|
2449
|
-
// - Otherwise, if repo name is provided, create folder with that name
|
|
2450
|
-
// - Fallback to current directory
|
|
2451
|
-
let targetDir
|
|
2452
|
-
if (opts.dir) {
|
|
2453
|
-
targetDir = path.resolve(opts.dir)
|
|
2454
|
-
} else if (repo) {
|
|
2455
|
-
targetDir = path.resolve(repo)
|
|
2456
|
-
} else {
|
|
2457
|
-
targetDir = path.resolve('.')
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
// Create the target directory if it doesn't exist
|
|
2461
|
-
await fs.promises.mkdir(targetDir, { recursive: true })
|
|
2462
|
-
|
|
2463
|
-
const spinner = createSpinner(`Initializing repository${repo ? ` '${repo}'` : ''}...`, opts.json)
|
|
2464
|
-
|
|
2465
|
-
let repoId = repo
|
|
2466
|
-
let remoteCreated = false
|
|
2467
|
-
|
|
2468
|
-
// If a repo name is provided and we have server, try to create remote repo
|
|
2469
|
-
if (repo && server) {
|
|
2470
|
-
if (!token) {
|
|
2471
|
-
spinnerUpdate(spinner, 'No auth token set. Run "resulgit auth login" first for remote repo creation.')
|
|
2472
|
-
}
|
|
2473
|
-
try {
|
|
2474
|
-
spinnerUpdate(spinner, 'Creating remote repository...')
|
|
2475
|
-
const createUrl = new URL('/api/repositories', server).toString()
|
|
2476
|
-
const createRes = await request('POST', createUrl, {
|
|
2477
|
-
name: repo,
|
|
2478
|
-
description: opts.description || '',
|
|
2479
|
-
visibility: opts.visibility || 'private',
|
|
2480
|
-
initializeWithReadme: true
|
|
2481
|
-
}, token)
|
|
2482
|
-
// Use the ID returned by the server (could be numeric or string)
|
|
2483
|
-
repoId = String(createRes.id || repo)
|
|
2484
|
-
remoteCreated = true
|
|
2485
|
-
spinnerUpdate(spinner, `Remote repository '${repo}' created (ID: ${repoId})`)
|
|
2486
|
-
} catch (err) {
|
|
2487
|
-
// If repo already exists (409), try to fetch its ID
|
|
2488
|
-
if (err.message && (err.message.includes('409') || err.message.includes('already exists'))) {
|
|
2489
|
-
spinnerUpdate(spinner, `Remote repository '${repo}' already exists, linking...`)
|
|
2490
|
-
try {
|
|
2491
|
-
const listUrl = new URL('/api/repositories', server).toString()
|
|
2492
|
-
const repos = await request('GET', listUrl, null, token)
|
|
2493
|
-
const found = (repos || []).find(r => r.name === repo)
|
|
2494
|
-
if (found) {
|
|
2495
|
-
repoId = String(found.id)
|
|
2496
|
-
remoteCreated = true
|
|
2497
|
-
}
|
|
2498
|
-
} catch { /* ignore */ }
|
|
2499
|
-
} else if (err.message && err.message.includes('401')) {
|
|
2500
|
-
spinnerUpdate(spinner, 'Authentication required. Run "resulgit auth login" to set up credentials.')
|
|
2501
|
-
} else {
|
|
2502
|
-
// Other error - continue with local init only
|
|
2503
|
-
spinnerUpdate(spinner, `Could not create remote repo: ${err.message}`)
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
spinnerUpdate(spinner, 'Setting up local repository...')
|
|
2509
|
-
const metaDir = path.join(targetDir, '.vcs-next')
|
|
2510
|
-
const gitDir = path.join(targetDir, '.git')
|
|
1971
|
+
const repo = opts.repo || ''
|
|
1972
|
+
const branch = opts.branch || 'main'
|
|
1973
|
+
const metaDir = path.join(dir, '.vcs-next')
|
|
1974
|
+
const gitDir = path.join(dir, '.git')
|
|
2511
1975
|
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
2512
1976
|
await fs.promises.mkdir(path.join(gitDir, 'refs', 'heads'), { recursive: true })
|
|
2513
1977
|
await fs.promises.writeFile(path.join(gitDir, 'HEAD'), `ref: refs/heads/${branch}\n`, 'utf8')
|
|
@@ -2521,47 +1985,16 @@ async function cmdInit(opts) {
|
|
|
2521
1985
|
'',
|
|
2522
1986
|
'[vcs-next]',
|
|
2523
1987
|
`\tserver = ${opts.server || server || ''}`,
|
|
2524
|
-
`\trepoId = ${
|
|
1988
|
+
`\trepoId = ${repo}`,
|
|
2525
1989
|
`\tbranch = ${branch}`,
|
|
2526
1990
|
`\ttoken = ${opts.token || token || ''}`
|
|
2527
1991
|
].join('\n')
|
|
2528
1992
|
await fs.promises.writeFile(path.join(gitDir, 'config'), gitConfig, 'utf8')
|
|
2529
|
-
|
|
2530
|
-
let remoteCommitId = ''
|
|
2531
|
-
let baseFiles = {}
|
|
2532
|
-
|
|
2533
|
-
// If remote was created, fetch the initial snapshot to sync baseFiles
|
|
2534
|
-
if (remoteCreated && repoId && server) {
|
|
2535
|
-
try {
|
|
2536
|
-
spinnerUpdate(spinner, 'Syncing with remote...')
|
|
2537
|
-
const snapshot = await fetchRemoteSnapshot(server, repoId, branch, token)
|
|
2538
|
-
remoteCommitId = snapshot.commitId || ''
|
|
2539
|
-
baseFiles = snapshot.files || {}
|
|
2540
|
-
|
|
2541
|
-
// Write the remote files to the local directory (like a clone)
|
|
2542
|
-
for (const [filePath, content] of Object.entries(baseFiles)) {
|
|
2543
|
-
const fullPath = path.join(targetDir, filePath)
|
|
2544
|
-
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true })
|
|
2545
|
-
await fs.promises.writeFile(fullPath, String(content), 'utf8')
|
|
2546
|
-
}
|
|
2547
|
-
} catch (err) {
|
|
2548
|
-
// Ignore sync errors - the repo might be truly empty
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
const remoteMeta = { repoId: repoId, branch, commitId: remoteCommitId, server: opts.server || server || '', token: opts.token || token || '' }
|
|
1993
|
+
const remoteMeta = { repoId: repo, branch, commitId: '', server: opts.server || server || '', token: opts.token || token || '' }
|
|
2553
1994
|
await fs.promises.writeFile(path.join(metaDir, 'remote.json'), JSON.stringify(remoteMeta, null, 2))
|
|
2554
|
-
const localMeta = { baseCommitId:
|
|
1995
|
+
const localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null }
|
|
2555
1996
|
await fs.promises.writeFile(path.join(metaDir, 'local.json'), JSON.stringify(localMeta, null, 2))
|
|
2556
|
-
|
|
2557
|
-
if (remoteCreated) {
|
|
2558
|
-
spinnerSuccess(spinner, `Initialized repository '${repo}' in ${targetDir} (remote linked)`)
|
|
2559
|
-
} else if (repo) {
|
|
2560
|
-
spinnerSuccess(spinner, `Initialized local repository in ${targetDir} (remote not created - check auth)`)
|
|
2561
|
-
} else {
|
|
2562
|
-
spinnerSuccess(spinner, `Initialized repository in ${targetDir}`)
|
|
2563
|
-
}
|
|
2564
|
-
print({ initialized: targetDir, branch, repoId: repoId, remoteCreated }, opts.json === 'true')
|
|
1997
|
+
print({ initialized: dir, branch }, opts.json === 'true')
|
|
2565
1998
|
}
|
|
2566
1999
|
|
|
2567
2000
|
async function cmdMv(opts) {
|
|
@@ -2831,400 +2264,6 @@ async function cmdRebase(opts) {
|
|
|
2831
2264
|
print({ rebased: sourceBranch, onto }, opts.json === 'true')
|
|
2832
2265
|
}
|
|
2833
2266
|
}
|
|
2834
|
-
|
|
2835
|
-
async function cmdBlame(opts) {
|
|
2836
|
-
const dir = path.resolve(opts.dir || '.')
|
|
2837
|
-
const filePath = opts.path
|
|
2838
|
-
if (!filePath) throw new errors.ValidationError('Missing --path', 'path')
|
|
2839
|
-
|
|
2840
|
-
const validPath = validation.validateFilePath(filePath)
|
|
2841
|
-
const meta = readRemoteMeta(dir)
|
|
2842
|
-
const cfg = loadConfig()
|
|
2843
|
-
const server = getServer(opts, cfg) || meta.server
|
|
2844
|
-
const token = getToken(opts, cfg) || meta.token
|
|
2845
|
-
|
|
2846
|
-
const spinner = createSpinner(`Getting blame for ${validPath}...`, opts.json)
|
|
2847
|
-
|
|
2848
|
-
try {
|
|
2849
|
-
// Get file content
|
|
2850
|
-
const local = await collectLocal(dir)
|
|
2851
|
-
if (!local[validPath]) {
|
|
2852
|
-
throw new errors.FileSystemError(`File not found: ${validPath}`, validPath, 'read')
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
// Get commits
|
|
2856
|
-
const commitsUrl = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
2857
|
-
commitsUrl.searchParams.set('branch', meta.branch)
|
|
2858
|
-
const commitsRes = await fetch(commitsUrl.toString(), {
|
|
2859
|
-
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
2860
|
-
})
|
|
2861
|
-
|
|
2862
|
-
if (!commitsRes.ok) {
|
|
2863
|
-
throw new errors.NetworkError('Failed to fetch commits', commitsRes.status, commitsUrl.toString())
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
const commits = await commitsRes.json()
|
|
2867
|
-
const blameData = parseBlame(local[validPath].content, commits, validPath)
|
|
2868
|
-
|
|
2869
|
-
spinnerSuccess(spinner, `Blame for ${validPath}`)
|
|
2870
|
-
|
|
2871
|
-
if (opts.json === 'true') {
|
|
2872
|
-
print(formatBlameJson(blameData), false)
|
|
2873
|
-
} else {
|
|
2874
|
-
process.stdout.write(formatBlameOutput(blameData) + '\n')
|
|
2875
|
-
}
|
|
2876
|
-
} catch (err) {
|
|
2877
|
-
spinnerFail(spinner, 'Blame failed')
|
|
2878
|
-
throw err
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
async function cmdLog(opts) {
|
|
2883
|
-
const dir = path.resolve(opts.dir || '.')
|
|
2884
|
-
const meta = readRemoteMeta(dir)
|
|
2885
|
-
const cfg = loadConfig()
|
|
2886
|
-
const server = getServer(opts, cfg) || meta.server
|
|
2887
|
-
const token = getToken(opts, cfg) || meta.token
|
|
2888
|
-
|
|
2889
|
-
const spinner = createSpinner('Fetching commit history...', opts.json)
|
|
2890
|
-
|
|
2891
|
-
try {
|
|
2892
|
-
const url = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
2893
|
-
if (opts.branch) url.searchParams.set('branch', opts.branch)
|
|
2894
|
-
else url.searchParams.set('branch', meta.branch)
|
|
2895
|
-
|
|
2896
|
-
const data = await request('GET', url.toString(), null, token)
|
|
2897
|
-
let commits = Array.isArray(data) ? data : []
|
|
2898
|
-
|
|
2899
|
-
// Filter by file path if provided
|
|
2900
|
-
const filePath = opts.path
|
|
2901
|
-
if (filePath) {
|
|
2902
|
-
const filteredCommits = []
|
|
2903
|
-
for (const commit of commits) {
|
|
2904
|
-
const commitSnap = await fetchSnapshotByCommit(server, meta.repoId, commit.id || commit._id, token)
|
|
2905
|
-
if (commitSnap.files[filePath] !== undefined) {
|
|
2906
|
-
filteredCommits.push(commit)
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
commits = filteredCommits
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
// Filter by content pattern (-G flag)
|
|
2913
|
-
const pattern = opts.G || opts.pattern
|
|
2914
|
-
if (pattern) {
|
|
2915
|
-
const regex = new RegExp(pattern, opts.ignoreCase === 'true' ? 'i' : '')
|
|
2916
|
-
const filteredCommits = []
|
|
2917
|
-
for (const commit of commits) {
|
|
2918
|
-
const commitSnap = await fetchSnapshotByCommit(server, meta.repoId, commit.id || commit._id, token)
|
|
2919
|
-
const parentId = (Array.isArray(commit.parents) && commit.parents[0]) || ''
|
|
2920
|
-
const parentSnap = parentId ? await fetchSnapshotByCommit(server, meta.repoId, parentId, token) : { files: {} }
|
|
2921
|
-
|
|
2922
|
-
// Check if pattern matches in any file changed in this commit
|
|
2923
|
-
const allPaths = new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)])
|
|
2924
|
-
let matches = false
|
|
2925
|
-
for (const p of allPaths) {
|
|
2926
|
-
const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : ''
|
|
2927
|
-
const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : ''
|
|
2928
|
-
if (regex.test(oldContent) || regex.test(newContent)) {
|
|
2929
|
-
matches = true
|
|
2930
|
-
break
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
if (matches) {
|
|
2934
|
-
filteredCommits.push(commit)
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
commits = filteredCommits
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
spinnerSuccess(spinner, `Found ${commits.length} commits`)
|
|
2941
|
-
|
|
2942
|
-
if (opts.json === 'true') {
|
|
2943
|
-
print(commits, true)
|
|
2944
|
-
} else if (opts.oneline === 'true') {
|
|
2945
|
-
process.stdout.write(formatCompactLog(commits) + '\n')
|
|
2946
|
-
} else if (opts.stats === 'true') {
|
|
2947
|
-
const stats = generateCommitStats(commits)
|
|
2948
|
-
process.stdout.write(color('Commit Statistics\n', 'bold'))
|
|
2949
|
-
process.stdout.write(color('━'.repeat(50) + '\n', 'dim'))
|
|
2950
|
-
process.stdout.write(`Total commits: ${stats.totalCommits}\n`)
|
|
2951
|
-
process.stdout.write(`\nTop authors:\n`)
|
|
2952
|
-
for (const author of stats.topAuthors) {
|
|
2953
|
-
process.stdout.write(` ${author.name.padEnd(30)} ${color(author.commits + ' commits', 'cyan')}\n`)
|
|
2954
|
-
}
|
|
2955
|
-
if (stats.datesRange.earliest && stats.datesRange.latest) {
|
|
2956
|
-
process.stdout.write(`\nDate range: ${stats.datesRange.earliest.toISOString().split('T')[0]} to ${stats.datesRange.latest.toISOString().split('T')[0]}\n`)
|
|
2957
|
-
}
|
|
2958
|
-
} else {
|
|
2959
|
-
const maxCommits = parseInt(opts.max || '50', 10)
|
|
2960
|
-
process.stdout.write(generateLogGraph(commits, { maxCommits }) + '\n')
|
|
2961
|
-
}
|
|
2962
|
-
} catch (err) {
|
|
2963
|
-
spinnerFail(spinner, 'Log fetch failed')
|
|
2964
|
-
throw err
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
async function cmdHook(sub, opts) {
|
|
2969
|
-
const dir = path.resolve(opts.dir || '.')
|
|
2970
|
-
|
|
2971
|
-
if (sub === 'list') {
|
|
2972
|
-
const hooksList = await hooks.listHooks(dir)
|
|
2973
|
-
if (opts.json === 'true') {
|
|
2974
|
-
print(hooksList, true)
|
|
2975
|
-
} else {
|
|
2976
|
-
if (hooksList.length === 0) {
|
|
2977
|
-
process.stdout.write('No hooks installed.\n')
|
|
2978
|
-
} else {
|
|
2979
|
-
process.stdout.write(color('Installed Hooks:\n', 'bold'))
|
|
2980
|
-
for (const hook of hooksList) {
|
|
2981
|
-
const exe = hook.executable ? color('✓', 'green') : color('✗', 'red')
|
|
2982
|
-
process.stdout.write(` ${exe} ${color(hook.name, 'cyan')}\n`)
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
return
|
|
2987
|
-
}
|
|
2988
|
-
|
|
2989
|
-
if (sub === 'install') {
|
|
2990
|
-
const hookName = opts.name
|
|
2991
|
-
if (!hookName) throw new Error('Missing --name')
|
|
2992
|
-
|
|
2993
|
-
let script = opts.script
|
|
2994
|
-
if (!script && opts.sample === 'true') {
|
|
2995
|
-
script = hooks.SAMPLE_HOOKS[hookName]
|
|
2996
|
-
if (!script) throw new Error(`No sample available for ${hookName}`)
|
|
2997
|
-
}
|
|
2998
|
-
if (!script) throw new Error('Missing --script or use --sample')
|
|
2999
|
-
|
|
3000
|
-
const result = await hooks.installHook(dir, hookName, script)
|
|
3001
|
-
print(result, opts.json === 'true')
|
|
3002
|
-
if (opts.json !== 'true') {
|
|
3003
|
-
process.stdout.write(color(`✓ Hook '${hookName}' installed\n`, 'green'))
|
|
3004
|
-
}
|
|
3005
|
-
return
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
if (sub === 'remove') {
|
|
3009
|
-
const hookName = opts.name
|
|
3010
|
-
if (!hookName) throw new Error('Missing --name')
|
|
3011
|
-
|
|
3012
|
-
const result = await hooks.removeHook(dir, hookName)
|
|
3013
|
-
print(result, opts.json === 'true')
|
|
3014
|
-
return
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
if (sub === 'show') {
|
|
3018
|
-
const hookName = opts.name
|
|
3019
|
-
if (!hookName) throw new Error('Missing --name')
|
|
3020
|
-
|
|
3021
|
-
const result = await hooks.readHook(dir, hookName)
|
|
3022
|
-
if (result.content) {
|
|
3023
|
-
process.stdout.write(result.content + '\n')
|
|
3024
|
-
} else {
|
|
3025
|
-
process.stdout.write(`Hook '${hookName}' not found.\n`)
|
|
3026
|
-
}
|
|
3027
|
-
return
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
throw new Error('Unknown hook subcommand. Use: list, install, remove, show')
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
async function cmdGrep(opts) {
|
|
3034
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3035
|
-
const pattern = opts.pattern || opts.p || ''
|
|
3036
|
-
if (!pattern) throw new Error('Missing --pattern or -p')
|
|
3037
|
-
|
|
3038
|
-
const meta = readRemoteMeta(dir)
|
|
3039
|
-
const local = await collectLocal(dir)
|
|
3040
|
-
const results = []
|
|
3041
|
-
|
|
3042
|
-
const regex = new RegExp(pattern, opts.ignoreCase === 'true' ? 'i' : '')
|
|
3043
|
-
|
|
3044
|
-
for (const [filePath, fileData] of Object.entries(local)) {
|
|
3045
|
-
const lines = fileData.content.split(/\r?\n/)
|
|
3046
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3047
|
-
if (regex.test(lines[i])) {
|
|
3048
|
-
results.push({
|
|
3049
|
-
path: filePath,
|
|
3050
|
-
line: i + 1,
|
|
3051
|
-
content: lines[i].trim()
|
|
3052
|
-
})
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
if (opts.json === 'true') {
|
|
3058
|
-
print(results, true)
|
|
3059
|
-
} else {
|
|
3060
|
-
for (const result of results) {
|
|
3061
|
-
process.stdout.write(color(`${result.path}:${result.line}`, 'cyan'))
|
|
3062
|
-
process.stdout.write(`: ${result.content}\n`)
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
async function cmdLsFiles(opts) {
|
|
3068
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3069
|
-
const meta = readRemoteMeta(dir)
|
|
3070
|
-
const cfg = loadConfig()
|
|
3071
|
-
const server = getServer(opts, cfg) || meta.server
|
|
3072
|
-
const token = getToken(opts, cfg) || meta.token
|
|
3073
|
-
|
|
3074
|
-
const remote = await fetchRemoteFilesMap(server, meta.repoId, meta.branch, token)
|
|
3075
|
-
const files = Object.keys(remote.map)
|
|
3076
|
-
|
|
3077
|
-
if (opts.json === 'true') {
|
|
3078
|
-
print(files.map(f => ({ path: f })), true)
|
|
3079
|
-
} else {
|
|
3080
|
-
for (const file of files) {
|
|
3081
|
-
process.stdout.write(`${file}\n`)
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
async function cmdReflog(opts) {
|
|
3087
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3088
|
-
const metaDir = path.join(dir, '.vcs-next')
|
|
3089
|
-
const reflogPath = path.join(metaDir, 'reflog.json')
|
|
3090
|
-
|
|
3091
|
-
let reflog = []
|
|
3092
|
-
try {
|
|
3093
|
-
const content = await fs.promises.readFile(reflogPath, 'utf8')
|
|
3094
|
-
reflog = JSON.parse(content)
|
|
3095
|
-
} catch { }
|
|
3096
|
-
|
|
3097
|
-
if (opts.json === 'true') {
|
|
3098
|
-
print(reflog, true)
|
|
3099
|
-
} else {
|
|
3100
|
-
if (reflog.length === 0) {
|
|
3101
|
-
process.stdout.write('No reflog entries.\n')
|
|
3102
|
-
} else {
|
|
3103
|
-
for (const entry of reflog) {
|
|
3104
|
-
const date = new Date(entry.timestamp).toLocaleString()
|
|
3105
|
-
process.stdout.write(`${color(entry.commitId.slice(0, 7), 'yellow')} ${entry.action} ${date} ${entry.message || ''}\n`)
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
async function cmdCatFile(opts) {
|
|
3112
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3113
|
-
const type = opts.type || ''
|
|
3114
|
-
const object = opts.object || ''
|
|
3115
|
-
|
|
3116
|
-
if (!type || !object) throw new Error('Missing --type and --object')
|
|
3117
|
-
|
|
3118
|
-
const meta = readRemoteMeta(dir)
|
|
3119
|
-
const cfg = loadConfig()
|
|
3120
|
-
const server = getServer(opts, cfg) || meta.server
|
|
3121
|
-
const token = getToken(opts, cfg) || meta.token
|
|
3122
|
-
|
|
3123
|
-
if (type === 'blob') {
|
|
3124
|
-
const snap = await fetchSnapshotByCommit(server, meta.repoId, object, token)
|
|
3125
|
-
const filePath = opts.path || ''
|
|
3126
|
-
if (filePath && snap.files[filePath]) {
|
|
3127
|
-
process.stdout.write(String(snap.files[filePath]))
|
|
3128
|
-
} else {
|
|
3129
|
-
throw new Error('File not found in commit')
|
|
3130
|
-
}
|
|
3131
|
-
} else if (type === 'commit') {
|
|
3132
|
-
const commit = await fetchCommitMeta(server, meta.repoId, object, token)
|
|
3133
|
-
if (opts.json === 'true') {
|
|
3134
|
-
print(commit, true)
|
|
3135
|
-
} else {
|
|
3136
|
-
process.stdout.write(`commit ${commit.id || commit._id}\n`)
|
|
3137
|
-
process.stdout.write(`Author: ${commit.author?.name || ''} <${commit.author?.email || ''}>\n`)
|
|
3138
|
-
process.stdout.write(`Date: ${new Date(commit.createdAt || commit.committer?.date || '').toLocaleString()}\n\n`)
|
|
3139
|
-
process.stdout.write(`${commit.message || ''}\n`)
|
|
3140
|
-
}
|
|
3141
|
-
} else {
|
|
3142
|
-
throw new Error(`Unsupported type: ${type}`)
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
async function cmdRevParse(opts) {
|
|
3147
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3148
|
-
const rev = opts.rev || 'HEAD'
|
|
3149
|
-
|
|
3150
|
-
const meta = readRemoteMeta(dir)
|
|
3151
|
-
const cfg = loadConfig()
|
|
3152
|
-
const server = getServer(opts, cfg) || meta.server
|
|
3153
|
-
const token = getToken(opts, cfg) || meta.token
|
|
3154
|
-
|
|
3155
|
-
if (rev === 'HEAD') {
|
|
3156
|
-
const info = await request('GET', new URL(`/api/repositories/${meta.repoId}/branches`, server).toString(), null, token)
|
|
3157
|
-
const found = (info.branches || []).find(b => b.name === meta.branch)
|
|
3158
|
-
const commitId = found ? (found.commitId || '') : ''
|
|
3159
|
-
print(opts.json === 'true' ? { rev, commitId } : commitId, opts.json === 'true')
|
|
3160
|
-
} else if (rev.startsWith('refs/heads/')) {
|
|
3161
|
-
const branchName = rev.replace('refs/heads/', '')
|
|
3162
|
-
const info = await request('GET', new URL(`/api/repositories/${meta.repoId}/branches`, server).toString(), null, token)
|
|
3163
|
-
const found = (info.branches || []).find(b => b.name === branchName)
|
|
3164
|
-
const commitId = found ? (found.commitId || '') : ''
|
|
3165
|
-
print(opts.json === 'true' ? { rev, commitId } : commitId, opts.json === 'true')
|
|
3166
|
-
} else {
|
|
3167
|
-
// Assume it's a commit ID
|
|
3168
|
-
print(opts.json === 'true' ? { rev, commitId: rev } : rev, opts.json === 'true')
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
async function cmdDescribe(opts) {
|
|
3173
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3174
|
-
const commitId = opts.commit || 'HEAD'
|
|
3175
|
-
|
|
3176
|
-
const meta = readRemoteMeta(dir)
|
|
3177
|
-
const cfg = loadConfig()
|
|
3178
|
-
const server = getServer(opts, cfg) || meta.server
|
|
3179
|
-
const token = getToken(opts, cfg) || meta.token
|
|
3180
|
-
|
|
3181
|
-
// Get tags
|
|
3182
|
-
const tagsUrl = new URL(`/api/repositories/${meta.repoId}/tags`, server)
|
|
3183
|
-
const tags = await request('GET', tagsUrl.toString(), null, token)
|
|
3184
|
-
const tagsList = Array.isArray(tags) ? tags : []
|
|
3185
|
-
|
|
3186
|
-
// Find nearest tag (simplified - just find any tag)
|
|
3187
|
-
const nearestTag = tagsList[0]
|
|
3188
|
-
|
|
3189
|
-
if (nearestTag) {
|
|
3190
|
-
const desc = `${nearestTag.name}-0-g${commitId.slice(0, 7)}`
|
|
3191
|
-
print(opts.json === 'true' ? { tag: nearestTag.name, commitId, describe: desc } : desc, opts.json === 'true')
|
|
3192
|
-
} else {
|
|
3193
|
-
print(opts.json === 'true' ? { commitId, describe: commitId.slice(0, 7) } : commitId.slice(0, 7), opts.json === 'true')
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
async function cmdShortlog(opts) {
|
|
3198
|
-
const dir = path.resolve(opts.dir || '.')
|
|
3199
|
-
const meta = readRemoteMeta(dir)
|
|
3200
|
-
const cfg = loadConfig()
|
|
3201
|
-
const server = getServer(opts, cfg) || meta.server
|
|
3202
|
-
const token = getToken(opts, cfg) || meta.token
|
|
3203
|
-
|
|
3204
|
-
const url = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
3205
|
-
if (opts.branch) url.searchParams.set('branch', opts.branch)
|
|
3206
|
-
else url.searchParams.set('branch', meta.branch)
|
|
3207
|
-
|
|
3208
|
-
const commits = await request('GET', url.toString(), null, token)
|
|
3209
|
-
const commitsList = Array.isArray(commits) ? commits : []
|
|
3210
|
-
|
|
3211
|
-
const stats = generateCommitStats(commitsList)
|
|
3212
|
-
|
|
3213
|
-
if (opts.json === 'true') {
|
|
3214
|
-
print(stats, true)
|
|
3215
|
-
} else {
|
|
3216
|
-
for (const author of stats.topAuthors) {
|
|
3217
|
-
process.stdout.write(`\n${color(author.name, 'bold')} (${author.commits} commit${author.commits !== 1 ? 's' : ''})\n`)
|
|
3218
|
-
// Show commit messages for this author
|
|
3219
|
-
const authorCommits = commitsList.filter(c => (c.author?.name || 'Unknown') === author.name)
|
|
3220
|
-
for (const commit of authorCommits.slice(0, opts.max ? parseInt(opts.max, 10) : 10)) {
|
|
3221
|
-
const msg = (commit.message || 'No message').split('\n')[0].slice(0, 60)
|
|
3222
|
-
process.stdout.write(` ${color((commit.id || commit._id || '').slice(0, 7), 'yellow')} ${msg}\n`)
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
2267
|
function help() {
|
|
3229
2268
|
const h = [
|
|
3230
2269
|
'Usage: resulgit <group> <command> [options]',
|
|
@@ -3239,37 +2278,26 @@ function help() {
|
|
|
3239
2278
|
' repo log --repo <id> [--branch <name>] [--json]',
|
|
3240
2279
|
' repo head --repo <id> [--branch <name>] [--json]',
|
|
3241
2280
|
' repo select [--workspace] (interactive select and clone/open)',
|
|
3242
|
-
' branch list|create|delete|rename [--dir <path>] [--name <branch>] [--base <branch>] [--old <name>] [--new <name>]
|
|
3243
|
-
' switch --branch <name> [--
|
|
3244
|
-
' checkout --branch <name> [--create|-b] [--commit <id>] [--dir <path>]',
|
|
2281
|
+
' branch list|create|delete|rename [--dir <path>] [--name <branch>] [--base <branch>] [--old <name>] [--new <name>]',
|
|
2282
|
+
' switch --branch <name> [--dir <path>]',
|
|
3245
2283
|
' current [--dir <path>] (show active repo/branch)',
|
|
3246
2284
|
' add <file> [--content <text>] [--from <src>] [--all] [--dir <path>] [--commit-message <text>]',
|
|
3247
2285
|
' tag list|create|delete [--dir <path>] [--name <tag>] [--branch <name>]',
|
|
3248
2286
|
' status [--dir <path>] [--json]',
|
|
3249
|
-
' diff [--dir <path>] [--path <file>] [--commit <id>] [--
|
|
3250
|
-
' commit --message <text> [--
|
|
2287
|
+
' diff [--dir <path>] [--path <file>] [--commit <id>] [--json]',
|
|
2288
|
+
' commit --message <text> [--dir <path>] [--json]',
|
|
3251
2289
|
' push [--dir <path>] [--json]',
|
|
3252
2290
|
' head [--dir <path>] [--json]',
|
|
3253
|
-
' rm --path <file> [--
|
|
2291
|
+
' rm --path <file> [--dir <path>] [--json]',
|
|
3254
2292
|
' pull [--dir <path>]',
|
|
3255
2293
|
' fetch [--dir <path>]',
|
|
3256
2294
|
' merge --branch <name> [--squash] [--no-push] [--dir <path>]',
|
|
3257
2295
|
' stash [save|list|pop|apply|drop|clear] [--message <msg>] [--index <n>] [--dir <path>]',
|
|
3258
2296
|
' restore --path <file> [--source <commit>] [--dir <path>]',
|
|
3259
2297
|
' revert --commit <id> [--no-push] [--dir <path>]',
|
|
3260
|
-
' reset [--commit <id
|
|
2298
|
+
' reset [--commit <id>] [--mode <soft|mixed|hard>] [--path <file>] [--dir <path>]',
|
|
3261
2299
|
' show --commit <id> [--dir <path>] [--json]',
|
|
3262
2300
|
' mv --from <old> --to <new> [--dir <path>]',
|
|
3263
|
-
' blame --path <file> [--dir <path>] [--json] - Show line-by-line authorship',
|
|
3264
|
-
' log [--branch <name>] [--max <N>] [--oneline] [--stats] [--path <file>] [-G <pattern>] [--json] - Show commit history',
|
|
3265
|
-
' hook list|install|remove|show [--name <hook>] [--script <code>] [--sample] - Manage Git hooks',
|
|
3266
|
-
' grep --pattern <pattern> [--ignore-case] [--dir <path>] [--json] - Search in repository',
|
|
3267
|
-
' ls-files [--dir <path>] [--json] - List tracked files',
|
|
3268
|
-
' reflog [--dir <path>] [--json] - Show reference log',
|
|
3269
|
-
' cat-file --type <type> --object <id> [--path <file>] [--dir <path>] [--json] - Display file contents',
|
|
3270
|
-
' rev-parse --rev <ref> [--dir <path>] [--json] - Parse revision names',
|
|
3271
|
-
' describe [--commit <id>] [--dir <path>] [--json] - Describe a commit',
|
|
3272
|
-
' shortlog [--branch <name>] [--max <N>] [--dir <path>] [--json] - Summarize commit log',
|
|
3273
2301
|
'',
|
|
3274
2302
|
'Conflict Resolution:',
|
|
3275
2303
|
' When conflicts occur (merge/cherry-pick/revert), conflict markers are written to files:',
|
|
@@ -3278,8 +2306,8 @@ function help() {
|
|
|
3278
2306
|
' >>>>>>> incoming (incoming changes)',
|
|
3279
2307
|
' Resolve conflicts manually, then commit and push. Push is blocked until conflicts are resolved.',
|
|
3280
2308
|
' pr list|create|merge [--dir <path>] [--title <t>] [--id <id>] [--source <branch>] [--target <branch>]',
|
|
3281
|
-
' clone <url> [--dest <dir>] | --repo <
|
|
3282
|
-
' init [--dir <path>] [--repo <
|
|
2309
|
+
' clone <url> [--dest <dir>] | --repo <id> --branch <name> [--dest <dir>] [--server <url>] [--token <token>]',
|
|
2310
|
+
' init [--dir <path>] [--repo <id>] [--server <url>] [--branch <name>] [--token <tok>]',
|
|
3283
2311
|
' remote show|set-url|set-token [--dir <path>] [--server <url>] [--token <tok>]',
|
|
3284
2312
|
' config list|get|set [--key <k>] [--value <v>]',
|
|
3285
2313
|
' clean [--dir <path>] [--force]',
|
|
@@ -3312,20 +2340,13 @@ async function main() {
|
|
|
3312
2340
|
return
|
|
3313
2341
|
}
|
|
3314
2342
|
if (cmd[0] === 'clone') {
|
|
3315
|
-
|
|
3316
|
-
if (arg && !arg.startsWith('--')) {
|
|
3317
|
-
if (arg.includes('://')) {
|
|
3318
|
-
opts.url = arg;
|
|
3319
|
-
} else {
|
|
3320
|
-
opts.repo = arg;
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
2343
|
+
if (!opts.url && cmd[1] && !cmd[1].startsWith('--')) opts.url = cmd[1]
|
|
3323
2344
|
if (opts.url) {
|
|
3324
|
-
await cmdCloneFromUrl(opts, cfg)
|
|
2345
|
+
await cmdCloneFromUrl(opts, cfg)
|
|
3325
2346
|
} else {
|
|
3326
|
-
await cmdClone(opts, cfg)
|
|
2347
|
+
await cmdClone(opts, cfg)
|
|
3327
2348
|
}
|
|
3328
|
-
return
|
|
2349
|
+
return
|
|
3329
2350
|
}
|
|
3330
2351
|
if (cmd[0] === 'status') {
|
|
3331
2352
|
await cmdStatus(opts)
|
|
@@ -3356,18 +2377,10 @@ async function main() {
|
|
|
3356
2377
|
return
|
|
3357
2378
|
}
|
|
3358
2379
|
if (cmd[0] === 'branch') {
|
|
3359
|
-
// Support positional: 'branch create DevBranch' or 'branch delete DevBranch'
|
|
3360
|
-
if ((cmd[1] === 'create' || cmd[1] === 'delete') && cmd[2] && !opts.name) {
|
|
3361
|
-
opts.name = cmd[2]
|
|
3362
|
-
}
|
|
3363
2380
|
await cmdBranch(cmd[1], opts)
|
|
3364
2381
|
return
|
|
3365
2382
|
}
|
|
3366
2383
|
if (cmd[0] === 'switch') {
|
|
3367
|
-
// Support positional: 'switch main' instead of 'switch --branch main'
|
|
3368
|
-
if (cmd[1] && !cmd[1].startsWith('-') && !opts.branch) {
|
|
3369
|
-
opts.branch = cmd[1]
|
|
3370
|
-
}
|
|
3371
2384
|
await cmdSwitch(opts)
|
|
3372
2385
|
return
|
|
3373
2386
|
}
|
|
@@ -3380,10 +2393,6 @@ async function main() {
|
|
|
3380
2393
|
return
|
|
3381
2394
|
}
|
|
3382
2395
|
if (cmd[0] === 'merge') {
|
|
3383
|
-
// Support positional: 'merge DevBranch' instead of 'merge --branch DevBranch'
|
|
3384
|
-
if (cmd[1] && !cmd[1].startsWith('-') && !opts.branch) {
|
|
3385
|
-
opts.branch = cmd[1]
|
|
3386
|
-
}
|
|
3387
2396
|
await cmdMerge(opts)
|
|
3388
2397
|
return
|
|
3389
2398
|
}
|
|
@@ -3412,12 +2421,8 @@ async function main() {
|
|
|
3412
2421
|
return
|
|
3413
2422
|
}
|
|
3414
2423
|
if (cmd[0] === 'init') {
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
opts.repo = cmd[1];
|
|
3418
|
-
}
|
|
3419
|
-
await cmdInit(opts);
|
|
3420
|
-
return;
|
|
2424
|
+
await cmdInit(opts)
|
|
2425
|
+
return
|
|
3421
2426
|
}
|
|
3422
2427
|
if (cmd[0] === 'remote') {
|
|
3423
2428
|
await cmdRemote(cmd[1], opts)
|
|
@@ -3467,46 +2472,6 @@ async function main() {
|
|
|
3467
2472
|
await cmdAdd(opts)
|
|
3468
2473
|
return
|
|
3469
2474
|
}
|
|
3470
|
-
if (cmd[0] === 'blame') {
|
|
3471
|
-
await cmdBlame(opts)
|
|
3472
|
-
return
|
|
3473
|
-
}
|
|
3474
|
-
if (cmd[0] === 'log') {
|
|
3475
|
-
await cmdLog(opts)
|
|
3476
|
-
return
|
|
3477
|
-
}
|
|
3478
|
-
if (cmd[0] === 'hook') {
|
|
3479
|
-
await cmdHook(cmd[1], opts)
|
|
3480
|
-
return
|
|
3481
|
-
}
|
|
3482
|
-
if (cmd[0] === 'grep') {
|
|
3483
|
-
await cmdGrep(opts)
|
|
3484
|
-
return
|
|
3485
|
-
}
|
|
3486
|
-
if (cmd[0] === 'ls-files') {
|
|
3487
|
-
await cmdLsFiles(opts)
|
|
3488
|
-
return
|
|
3489
|
-
}
|
|
3490
|
-
if (cmd[0] === 'reflog') {
|
|
3491
|
-
await cmdReflog(opts)
|
|
3492
|
-
return
|
|
3493
|
-
}
|
|
3494
|
-
if (cmd[0] === 'cat-file') {
|
|
3495
|
-
await cmdCatFile(opts)
|
|
3496
|
-
return
|
|
3497
|
-
}
|
|
3498
|
-
if (cmd[0] === 'rev-parse') {
|
|
3499
|
-
await cmdRevParse(opts)
|
|
3500
|
-
return
|
|
3501
|
-
}
|
|
3502
|
-
if (cmd[0] === 'describe') {
|
|
3503
|
-
await cmdDescribe(opts)
|
|
3504
|
-
return
|
|
3505
|
-
}
|
|
3506
|
-
if (cmd[0] === 'shortlog') {
|
|
3507
|
-
await cmdShortlog(opts)
|
|
3508
|
-
return
|
|
3509
|
-
}
|
|
3510
2475
|
throw new Error('Unknown command')
|
|
3511
2476
|
}
|
|
3512
2477
|
|
|
@@ -3621,6 +2586,7 @@ async function copyDir(src, dest) {
|
|
|
3621
2586
|
}
|
|
3622
2587
|
}
|
|
3623
2588
|
|
|
2589
|
+
|
|
3624
2590
|
async function tuiSelectBranch(branches, current) {
|
|
3625
2591
|
const items = (branches || []).map((b) => ({ name: b.name || '', commitId: b.commitId || '' })).filter((b) => b.name)
|
|
3626
2592
|
if (items.length === 0) throw new Error('No branches available')
|