resulgit 1.0.19 → 1.0.21

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 (3) hide show
  1. package/README.md +143 -0
  2. package/package.json +1 -1
  3. package/resulgit.js +303 -66
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # resulgit
2
+
3
+ A powerful command-line interface (CLI) tool for version control system operations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g resulgit
9
+ ```
10
+
11
+ Or install locally:
12
+
13
+ ```bash
14
+ npm install resulgit
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ resulgit <command> [options]
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ ### Authentication
26
+
27
+ - `resulgit auth set-token --token <token>` - Set authentication token
28
+ - `resulgit auth set-server --server <url>` - Set server URL
29
+ - `resulgit auth login --email <email> --password <password>` - Login to server
30
+ - `resulgit auth register --username <name> --email <email> --password <password>` - Register new account
31
+
32
+ ### Repository Management
33
+
34
+ - `resulgit repo list` - List all repositories
35
+ - `resulgit repo create --name <name> [--description <text>] [--visibility <private|public>]` - Create new repository
36
+ - `resulgit repo log --repo <id> [--branch <name>]` - View commit log
37
+ - `resulgit repo head --repo <id> [--branch <name>]` - Get HEAD commit
38
+ - `resulgit repo select` - Interactive repository selection
39
+
40
+ ### Clone & Workspace
41
+
42
+ - `resulgit clone --repo <id> --branch <name> [--dest <dir>]` - Clone a repository
43
+ - `resulgit workspace set-root --path <dir>` - Set workspace root directory
44
+
45
+ ### Branch Operations
46
+
47
+ - `resulgit branch list` - List all branches
48
+ - `resulgit branch create --name <branch> [--base <branch>]` - Create new branch
49
+ - `resulgit branch delete --name <branch>` - Delete a branch
50
+ - `resulgit switch --branch <name>` - Switch to a branch
51
+ - `resulgit checkout --branch <name>` - Checkout a branch
52
+
53
+ ### File Operations
54
+
55
+ - `resulgit status` - Show working directory status
56
+ - `resulgit diff [--path <file>] [--commit <id>]` - Show differences
57
+ - `resulgit add <file> [--content <text>] [--all]` - Add files
58
+ - `resulgit rm --path <file>` - Remove files
59
+ - `resulgit mv --from <old> --to <new>` - Move/rename files
60
+ - `resulgit restore --path <file> [--source <commit>]` - Restore file from commit
61
+
62
+ ### Version Control
63
+
64
+ - `resulgit commit --message <text>` - Create a commit
65
+ - `resulgit push` - Push changes to remote
66
+ - `resulgit pull` - Pull changes from remote
67
+ - `resulgit merge --branch <name> [--squash] [--no-push]` - Merge branches
68
+ - `resulgit cherry-pick --commit <id> [--branch <name>] [--no-push]` - Cherry-pick a commit
69
+ - `resulgit revert --commit <id> [--no-push]` - Revert a commit
70
+ - `resulgit reset [--commit <id>] [--mode <soft|mixed|hard>]` - Reset to commit
71
+
72
+ ### Stash Operations
73
+
74
+ - `resulgit stash` or `resulgit stash save [--message <msg>]` - Save changes to stash
75
+ - `resulgit stash list` - List all stashes
76
+ - `resulgit stash pop [--index <n>]` - Apply and remove stash
77
+ - `resulgit stash apply [--index <n>]` - Apply stash without removing
78
+ - `resulgit stash drop [--index <n>]` - Delete a stash
79
+ - `resulgit stash clear` - Clear all stashes
80
+
81
+ ### Tags
82
+
83
+ - `resulgit tag list` - List all tags
84
+ - `resulgit tag create --name <tag> [--branch <name>]` - Create a tag
85
+ - `resulgit tag delete --name <tag>` - Delete a tag
86
+
87
+ ### Pull Requests
88
+
89
+ - `resulgit pr list` - List pull requests
90
+ - `resulgit pr create --title <title> [--source <branch>] [--target <branch>]` - Create pull request
91
+ - `resulgit pr merge --id <id>` - Merge a pull request
92
+
93
+ ### Information
94
+
95
+ - `resulgit current` - Show current repository and branch
96
+ - `resulgit head` - Show HEAD commit ID
97
+ - `resulgit show --commit <id>` - Show commit details
98
+
99
+ ## Global Options
100
+
101
+ - `--server <url>` - Override default server
102
+ - `--token <token>` - Override stored token
103
+ - `--json` - Output in JSON format
104
+ - `--dir <path>` - Specify working directory
105
+
106
+ ## Examples
107
+
108
+ ```bash
109
+ # Login to server
110
+ resulgit auth login --email user@example.com --password mypassword
111
+
112
+ # List repositories
113
+ resulgit repo list
114
+
115
+ # Clone a repository
116
+ resulgit clone --repo 123 --branch main
117
+
118
+ # Check status
119
+ resulgit status
120
+
121
+ # Create and commit changes
122
+ resulgit add file.txt --content "Hello World"
123
+ resulgit commit --message "Add file.txt"
124
+ resulgit push
125
+
126
+ # Create a branch
127
+ resulgit branch create --name feature-branch
128
+
129
+ # Merge branches
130
+ resulgit merge --branch feature-branch
131
+ ```
132
+
133
+ ## Configuration
134
+
135
+ Configuration is stored in `~/.resulgit/config.json`. You can set:
136
+ - `server`: Default server URL
137
+ - `token`: Authentication token
138
+ - `workspaceRoot`: Default workspace directory
139
+
140
+ ## License
141
+
142
+ MIT
143
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resulgit",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "A powerful CLI tool for version control system operations - clone, commit, push, pull, merge, branch management, and more",
5
5
  "main": "resulgit.js",
6
6
  "bin": {
package/resulgit.js CHANGED
@@ -3,6 +3,7 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
  const os = require('os')
5
5
  const crypto = require('crypto')
6
+ const readline = require('readline')
6
7
  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' }
7
8
  function color(str, c) { return (COLORS[c] || '') + String(str) + COLORS.reset }
8
9
 
@@ -85,6 +86,44 @@ function print(obj, json) {
85
86
  process.stdout.write(Object.entries(obj).map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`).join('\n') + '\n')
86
87
  }
87
88
 
89
+ async function prompt(msg, mask = false) {
90
+ if (mask) {
91
+ process.stdout.write(msg)
92
+ process.stdin.setRawMode(true)
93
+ process.stdin.resume()
94
+ return new Promise(resolve => {
95
+ let pw = ''
96
+ const onData = buf => {
97
+ const s = buf.toString('utf8')
98
+ if (s === '\r' || s === '\n') {
99
+ process.stdin.setRawMode(false)
100
+ process.stdin.pause()
101
+ process.stdin.removeListener('data', onData)
102
+ process.stdout.write('\n')
103
+ resolve(pw)
104
+ } else if (s === '\u0003') { // Ctrl-C
105
+ process.stdin.setRawMode(false)
106
+ process.stdin.pause()
107
+ process.stdout.write('\n')
108
+ process.exit(0)
109
+ } else if (s === '\u007f' || s === '\b' || s === '\x08') { // Backspace
110
+ if (pw.length > 0) {
111
+ pw = pw.slice(0, -1)
112
+ process.stdout.write('\b \b')
113
+ }
114
+ } else if (s.length === 1 && s.charCodeAt(0) >= 32) {
115
+ pw += s
116
+ process.stdout.write('*')
117
+ }
118
+ }
119
+ process.stdin.on('data', onData)
120
+ })
121
+ } else {
122
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
123
+ return new Promise(resolve => rl.question(msg, line => { rl.close(); resolve(line) }))
124
+ }
125
+ }
126
+
88
127
  async function cmdAuth(sub, opts) {
89
128
  if (sub === 'set-token') {
90
129
  const token = opts.token || ''
@@ -102,11 +141,22 @@ async function cmdAuth(sub, opts) {
102
141
  }
103
142
  if (sub === 'login') {
104
143
  const server = opts.server || loadConfig().server
105
- const email = opts.email
106
- const password = opts.password
107
- if (!email || !password) throw new Error('Missing --email and --password')
144
+ let email = opts.email
145
+ let password = opts.password
146
+
147
+ if (!email) {
148
+ email = await prompt('Email: ')
149
+ }
150
+ if (!password) {
151
+ password = await prompt('Password: ', true)
152
+ }
153
+
154
+ if (!email || !password) throw new Error('Email and password required')
155
+
108
156
  const url = new URL('/api/auth/login', server).toString()
109
- print({ server, email, password, url }, opts.json === 'true')
157
+ const maskedPassword = password.replace(/./g, '*')
158
+ print({ server, email, password: maskedPassword, url }, opts.json === 'true')
159
+
110
160
  const res = await request('POST', url, { email, password }, '')
111
161
  const token = res.token || ''
112
162
  if (token) saveConfig({ token })
@@ -115,12 +165,21 @@ async function cmdAuth(sub, opts) {
115
165
  }
116
166
  if (sub === 'register') {
117
167
  const server = opts.server || loadConfig().server
118
- const username = opts.username
119
- const email = opts.email
120
- const password = opts.password
168
+ let username = opts.username
169
+ let email = opts.email
170
+ let password = opts.password
121
171
  const displayName = opts.displayName || username
172
+
173
+ if (!username) username = await prompt('Username: ')
174
+ if (!email) email = await prompt('Email: ')
175
+ if (!password) password = await prompt('Password: ', true)
176
+
122
177
  if (!username || !email || !password) throw new Error('Missing --username --email --password')
178
+
123
179
  const url = new URL('/api/auth/register', server).toString()
180
+ const maskedPassword = password.replace(/./g, '*')
181
+ print({ server, username, email, password: maskedPassword, url }, opts.json === 'true')
182
+
124
183
  const res = await request('POST', url, { username, email, password, displayName }, '')
125
184
  const token = res.token || ''
126
185
  if (token) saveConfig({ token })
@@ -339,15 +398,134 @@ function hashContent(buf) {
339
398
  return crypto.createHash('sha1').update(buf).digest('hex')
340
399
  }
341
400
 
401
+ // LCS-based diff: computes edit script (list of equal/insert/delete operations)
402
+ function computeDiff(oldLines, newLines) {
403
+ const m = oldLines.length
404
+ const n = newLines.length
405
+ // Build LCS table
406
+ const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1))
407
+ for (let i = 1; i <= m; i++) {
408
+ for (let j = 1; j <= n; j++) {
409
+ if (oldLines[i - 1] === newLines[j - 1]) {
410
+ dp[i][j] = dp[i - 1][j - 1] + 1
411
+ } else {
412
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
413
+ }
414
+ }
415
+ }
416
+ // Backtrack to produce edit operations
417
+ const ops = [] // { type: 'equal'|'delete'|'insert', oldIdx, newIdx, line }
418
+ let i = m, j = n
419
+ while (i > 0 || j > 0) {
420
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
421
+ ops.push({ type: 'equal', oldIdx: i - 1, newIdx: j - 1, line: oldLines[i - 1] })
422
+ i--; j--
423
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
424
+ ops.push({ type: 'insert', newIdx: j - 1, line: newLines[j - 1] })
425
+ j--
426
+ } else {
427
+ ops.push({ type: 'delete', oldIdx: i - 1, line: oldLines[i - 1] })
428
+ i--
429
+ }
430
+ }
431
+ ops.reverse()
432
+ return ops
433
+ }
434
+
435
+ // Print unified diff with context lines and @@ hunk headers
436
+ function printUnifiedDiff(oldLines, newLines, contextLines) {
437
+ const ctx = contextLines !== undefined ? contextLines : 3
438
+ const ops = computeDiff(oldLines, newLines)
439
+ // Group ops into hunks (runs of changes with context)
440
+ const hunks = []
441
+ let hunk = null
442
+ let lastChangeEnd = -1
443
+ for (let k = 0; k < ops.length; k++) {
444
+ const op = ops[k]
445
+ if (op.type !== 'equal') {
446
+ // Start or extend a hunk
447
+ const contextStart = Math.max(0, k - ctx)
448
+ if (hunk && contextStart <= lastChangeEnd + ctx) {
449
+ // Extend current hunk
450
+ } else {
451
+ // Save previous hunk and start new
452
+ if (hunk) hunks.push(hunk)
453
+ hunk = { startIdx: Math.max(0, k - ctx), endIdx: k }
454
+ }
455
+ hunk.endIdx = k
456
+ lastChangeEnd = k
457
+ }
458
+ }
459
+ if (hunk) {
460
+ hunk.endIdx = Math.min(ops.length - 1, hunk.endIdx + ctx)
461
+ hunks.push(hunk)
462
+ }
463
+ // Print each hunk
464
+ for (const h of hunks) {
465
+ const start = h.startIdx
466
+ const end = Math.min(ops.length - 1, h.endIdx)
467
+ // Compute line numbers for header
468
+ let oldStart = 1, newStart = 1
469
+ for (let k = 0; k < start; k++) {
470
+ if (ops[k].type === 'equal' || ops[k].type === 'delete') oldStart++
471
+ if (ops[k].type === 'equal' || ops[k].type === 'insert') newStart++
472
+ }
473
+ let oldCount = 0, newCount = 0
474
+ for (let k = start; k <= end; k++) {
475
+ if (ops[k].type === 'equal' || ops[k].type === 'delete') oldCount++
476
+ if (ops[k].type === 'equal' || ops[k].type === 'insert') newCount++
477
+ }
478
+ process.stdout.write(color(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@\n`, 'cyan'))
479
+ for (let k = start; k <= end; k++) {
480
+ const op = ops[k]
481
+ if (op.type === 'equal') {
482
+ process.stdout.write(` ${op.line}\n`)
483
+ } else if (op.type === 'delete') {
484
+ process.stdout.write(color(`-${op.line}\n`, 'red'))
485
+ } else if (op.type === 'insert') {
486
+ process.stdout.write(color(`+${op.line}\n`, 'green'))
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ function loadIgnorePatterns(dir) {
493
+ const patterns = ['.git', '.vcs-next', 'node_modules', '.DS_Store', 'dist', 'build']
494
+ const tryFiles = ['.vcs-ignore', '.gitignore']
495
+ for (const f of tryFiles) {
496
+ try {
497
+ const content = fs.readFileSync(path.join(dir, f), 'utf8')
498
+ const lines = content.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'))
499
+ patterns.push(...lines)
500
+ } catch {}
501
+ }
502
+ return [...new Set(patterns)]
503
+ }
504
+
505
+ function shouldIgnore(p, patterns) {
506
+ const segments = p.split('/')
507
+ for (const pat of patterns) {
508
+ if (segments.includes(pat)) return true
509
+ if (p === pat || p.startsWith(pat + '/')) return true
510
+ // Basic glob-like support for simple cases
511
+ if (pat.startsWith('**/')) {
512
+ const sub = pat.slice(3)
513
+ if (p.endsWith('/' + sub) || p === sub) return true
514
+ }
515
+ }
516
+ return false
517
+ }
518
+
342
519
  async function collectLocal(dir) {
343
520
  const out = {}
344
521
  const base = path.resolve(dir)
522
+ const patterns = loadIgnorePatterns(base)
345
523
  async function walk(cur, rel) {
346
524
  const entries = await fs.promises.readdir(cur, { withFileTypes: true })
347
525
  for (const e of entries) {
348
- if (e.name === '.git' || e.name === '.vcs-next') continue
349
- const abs = path.join(cur, e.name)
350
526
  const rp = rel ? rel + '/' + e.name : e.name
527
+ if (shouldIgnore(rp, patterns)) continue
528
+ const abs = path.join(cur, e.name)
351
529
  if (e.isDirectory()) {
352
530
  await walk(abs, rp)
353
531
  } else if (e.isFile()) {
@@ -392,25 +570,98 @@ async function fetchRemoteFilesMap(server, repo, branch, token) {
392
570
  async function cmdStatus(opts) {
393
571
  const dir = path.resolve(opts.dir || '.')
394
572
  const meta = readRemoteMeta(dir)
395
- const cfg = loadConfig()
396
- const server = getServer(opts, cfg) || meta.server
397
- const token = getToken(opts, cfg) || meta.token
573
+ const metaDir = path.join(dir, '.vcs-next')
574
+ const localPath = path.join(metaDir, 'local.json')
575
+ let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null }
576
+ try {
577
+ const s = fs.readFileSync(localPath, 'utf8')
578
+ localMeta = JSON.parse(s)
579
+ } catch {}
580
+
398
581
  const local = await collectLocal(dir)
399
- const remote = await fetchRemoteFilesMap(server, meta.repoId, meta.branch, token)
400
- const added = []
401
- const modified = []
402
- const deleted = []
403
- const remotePaths = new Set(Object.keys(remote.map))
404
- const localPaths = new Set(Object.keys(local))
405
- for (const p of localPaths) {
406
- if (!remotePaths.has(p)) added.push(p)
407
- else if (remote.map[p] !== local[p].id) modified.push(p)
582
+ const baseFiles = localMeta.baseFiles || {}
583
+ const pendingFiles = localMeta.pendingCommit ? localMeta.pendingCommit.files : null
584
+
585
+ const untracked = []
586
+ const modifiedUnstaged = []
587
+ const deletedUnstaged = []
588
+ const modifiedStaged = []
589
+ const deletedStaged = []
590
+ const newStaged = []
591
+
592
+ // If there's a pending commit, that's our "staged" area
593
+ if (pendingFiles) {
594
+ // staged changes: pendingFiles vs baseFiles
595
+ const allStagedPaths = new Set([...Object.keys(baseFiles), ...Object.keys(pendingFiles)])
596
+ for (const p of allStagedPaths) {
597
+ const b = baseFiles[p]
598
+ const s = pendingFiles[p]
599
+ if (b === undefined && s !== undefined) newStaged.push(p)
600
+ else if (b !== undefined && s === undefined) deletedStaged.push(p)
601
+ else if (b !== s) modifiedStaged.push(p)
602
+ }
603
+
604
+ // unstaged changes: local vs pendingFiles
605
+ const localPaths = new Set(Object.keys(local))
606
+ const stagedPaths = new Set(Object.keys(pendingFiles))
607
+ for (const p of localPaths) {
608
+ if (!stagedPaths.has(p)) untracked.push(p)
609
+ else if (pendingFiles[p] !== local[p].content) modifiedUnstaged.push(p)
610
+ }
611
+ for (const p of stagedPaths) {
612
+ if (!localPaths.has(p)) deletedUnstaged.push(p)
613
+ }
614
+ } else {
615
+ // No pending commit: just local vs baseFiles
616
+ const localPaths = new Set(Object.keys(local))
617
+ const basePaths = new Set(Object.keys(baseFiles))
618
+ for (const p of localPaths) {
619
+ if (!basePaths.has(p)) untracked.push(p)
620
+ else if (baseFiles[p] !== local[p].content) modifiedUnstaged.push(p)
621
+ }
622
+ for (const p of basePaths) {
623
+ if (!localPaths.has(p)) deletedUnstaged.push(p)
624
+ }
625
+ }
626
+
627
+ if (opts.json === 'true') {
628
+ print({
629
+ branch: meta.branch,
630
+ ahead: localMeta.pendingCommit ? 1 : 0,
631
+ staged: { modified: modifiedStaged, deleted: deletedStaged, new: newStaged },
632
+ unstaged: { modified: modifiedUnstaged, deleted: deletedUnstaged },
633
+ untracked
634
+ }, true)
635
+ return
408
636
  }
409
- for (const p of remotePaths) {
410
- if (!localPaths.has(p)) deleted.push(p)
637
+
638
+ process.stdout.write(`On branch ${color(meta.branch, 'cyan')}\n`)
639
+ if (localMeta.pendingCommit) {
640
+ process.stdout.write(`Your branch is ahead of 'origin/${meta.branch}' by 1 commit.\n`)
641
+ }
642
+
643
+ if (modifiedStaged.length > 0 || deletedStaged.length > 0 || newStaged.length > 0) {
644
+ process.stdout.write('\nChanges to be committed:\n')
645
+ for (const p of newStaged) process.stdout.write(color(` new file: ${p}\n`, 'green'))
646
+ for (const p of modifiedStaged) process.stdout.write(color(` modified: ${p}\n`, 'green'))
647
+ for (const p of deletedStaged) process.stdout.write(color(` deleted: ${p}\n`, 'green'))
648
+ }
649
+
650
+ if (modifiedUnstaged.length > 0 || deletedUnstaged.length > 0) {
651
+ process.stdout.write('\nChanges not staged for commit:\n')
652
+ for (const p of modifiedUnstaged) process.stdout.write(color(` modified: ${p}\n`, 'red'))
653
+ for (const p of deletedUnstaged) process.stdout.write(color(` deleted: ${p}\n`, 'red'))
654
+ }
655
+
656
+ if (untracked.length > 0) {
657
+ process.stdout.write('\nUntracked files:\n')
658
+ for (const p of untracked) process.stdout.write(color(` ${p}\n`, 'red'))
659
+ }
660
+
661
+ if (modifiedStaged.length === 0 && deletedStaged.length === 0 && newStaged.length === 0 &&
662
+ modifiedUnstaged.length === 0 && deletedUnstaged.length === 0 && untracked.length === 0) {
663
+ process.stdout.write('nothing to commit, working tree clean\n')
411
664
  }
412
- const out = { branch: meta.branch, head: remote.headCommitId, added, modified, deleted }
413
- print(out, opts.json === 'true')
414
665
  }
415
666
 
416
667
  async function cmdRestore(opts) {
@@ -475,7 +726,7 @@ async function cmdDiff(opts) {
475
726
  const commitSnap = await fetchSnapshotByCommit(server, meta.repoId, commitId, token)
476
727
  const parentSnap = parentId ? await fetchSnapshotByCommit(server, meta.repoId, parentId, token) : { files: {}, commitId: '' }
477
728
 
478
- const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)]))
729
+ const files = filePath ? [filePath] : [...new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)])]
479
730
  for (const p of files) {
480
731
  const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : null
481
732
  const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : null
@@ -483,27 +734,17 @@ async function cmdDiff(opts) {
483
734
  if (opts.json === 'true') {
484
735
  print({ path: p, old: oldContent, new: newContent }, true)
485
736
  } else {
486
- process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'dim'))
737
+ process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'bold'))
487
738
  if (oldContent === null) {
488
739
  process.stdout.write(color(`+++ b/${p}\n`, 'green'))
489
- process.stdout.write(color(`+${newContent}\n`, 'green'))
740
+ const lines = newContent.split(/\r?\n/)
741
+ for (const line of lines) process.stdout.write(color(`+${line}\n`, 'green'))
490
742
  } else if (newContent === null) {
491
743
  process.stdout.write(color(`--- a/${p}\n`, 'red'))
492
- process.stdout.write(color(`-${oldContent}\n`, 'red'))
744
+ const lines = oldContent.split(/\r?\n/)
745
+ for (const line of lines) process.stdout.write(color(`-${line}\n`, 'red'))
493
746
  } else {
494
- const oldLines = oldContent.split(/\r?\n/)
495
- const newLines = newContent.split(/\r?\n/)
496
- const maxLen = Math.max(oldLines.length, newLines.length)
497
- for (let i = 0; i < maxLen; i++) {
498
- const oldLine = oldLines[i]
499
- const newLine = newLines[i]
500
- if (oldLine !== newLine) {
501
- if (oldLine !== undefined) process.stdout.write(color(`-${oldLine}\n`, 'red'))
502
- if (newLine !== undefined) process.stdout.write(color(`+${newLine}\n`, 'green'))
503
- } else if (oldLine !== undefined) {
504
- process.stdout.write(` ${oldLine}\n`)
505
- }
506
- }
747
+ printUnifiedDiff(oldContent.split(/\r?\n/), newContent.split(/\r?\n/))
507
748
  }
508
749
  }
509
750
  }
@@ -512,41 +753,37 @@ async function cmdDiff(opts) {
512
753
  }
513
754
 
514
755
  // Show diff for working directory
515
- const remoteSnap = await fetchRemoteSnapshot(server, meta.repoId, meta.branch, token)
756
+ const metaDir = path.join(dir, '.vcs-next')
757
+ const localPath = path.join(metaDir, 'local.json')
758
+ let localMeta = { baseFiles: {} }
759
+ try {
760
+ const s = fs.readFileSync(localPath, 'utf8')
761
+ localMeta = JSON.parse(s)
762
+ } catch {}
763
+
764
+ const baseFiles = localMeta.pendingCommit ? localMeta.pendingCommit.files : localMeta.baseFiles
516
765
  const local = await collectLocal(dir)
517
- const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(remoteSnap.files), ...Object.keys(local)]))
766
+ const files = filePath ? [filePath] : [...new Set([...Object.keys(baseFiles), ...Object.keys(local)])]
518
767
 
519
768
  for (const p of files) {
520
- const remoteContent = remoteSnap.files[p] !== undefined ? String(remoteSnap.files[p]) : null
769
+ const baseContent = baseFiles[p] !== undefined ? String(baseFiles[p]) : null
521
770
  const localContent = local[p]?.content || null
522
- const remoteId = remoteSnap.files[p] !== undefined ? hashContent(Buffer.from(String(remoteSnap.files[p]))) : null
523
- const localId = local[p]?.id
524
771
 
525
- if (remoteId !== localId) {
772
+ if (baseContent !== localContent) {
526
773
  if (opts.json === 'true') {
527
- print({ path: p, remote: remoteContent, local: localContent }, true)
774
+ print({ path: p, base: baseContent, local: localContent }, true)
528
775
  } else {
529
- process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'dim'))
776
+ process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'bold'))
530
777
  if (localContent === null) {
531
778
  process.stdout.write(color(`--- a/${p}\n`, 'red'))
532
- process.stdout.write(color(`-${remoteContent || ''}\n`, 'red'))
533
- } else if (remoteContent === null) {
779
+ const lines = (baseContent || '').split(/\r?\n/)
780
+ for (const line of lines) process.stdout.write(color(`-${line}\n`, 'red'))
781
+ } else if (baseContent === null) {
534
782
  process.stdout.write(color(`+++ b/${p}\n`, 'green'))
535
- process.stdout.write(color(`+${localContent}\n`, 'green'))
783
+ const lines = localContent.split(/\r?\n/)
784
+ for (const line of lines) process.stdout.write(color(`+${line}\n`, 'green'))
536
785
  } else {
537
- const remoteLines = String(remoteContent).split(/\r?\n/)
538
- const localLines = String(localContent).split(/\r?\n/)
539
- const maxLen = Math.max(remoteLines.length, localLines.length)
540
- for (let i = 0; i < maxLen; i++) {
541
- const remoteLine = remoteLines[i]
542
- const localLine = localLines[i]
543
- if (remoteLine !== localLine) {
544
- if (remoteLine !== undefined) process.stdout.write(color(`-${remoteLine}\n`, 'red'))
545
- if (localLine !== undefined) process.stdout.write(color(`+${localLine}\n`, 'green'))
546
- } else if (remoteLine !== undefined) {
547
- process.stdout.write(` ${remoteLine}\n`)
548
- }
549
- }
786
+ printUnifiedDiff(baseContent.split(/\r?\n/), localContent.split(/\r?\n/))
550
787
  }
551
788
  }
552
789
  }