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.
- package/README.md +143 -0
- package/package.json +1 -1
- 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
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
396
|
-
const
|
|
397
|
-
|
|
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
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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] :
|
|
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`, '
|
|
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
|
-
|
|
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
|
-
|
|
744
|
+
const lines = oldContent.split(/\r?\n/)
|
|
745
|
+
for (const line of lines) process.stdout.write(color(`-${line}\n`, 'red'))
|
|
493
746
|
} else {
|
|
494
|
-
|
|
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
|
|
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] :
|
|
766
|
+
const files = filePath ? [filePath] : [...new Set([...Object.keys(baseFiles), ...Object.keys(local)])]
|
|
518
767
|
|
|
519
768
|
for (const p of files) {
|
|
520
|
-
const
|
|
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 (
|
|
772
|
+
if (baseContent !== localContent) {
|
|
526
773
|
if (opts.json === 'true') {
|
|
527
|
-
print({ path: p,
|
|
774
|
+
print({ path: p, base: baseContent, local: localContent }, true)
|
|
528
775
|
} else {
|
|
529
|
-
process.stdout.write(color(`diff --git a/${p} b/${p}\n`, '
|
|
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
|
-
|
|
533
|
-
|
|
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
|
-
|
|
783
|
+
const lines = localContent.split(/\r?\n/)
|
|
784
|
+
for (const line of lines) process.stdout.write(color(`+${line}\n`, 'green'))
|
|
536
785
|
} else {
|
|
537
|
-
|
|
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
|
}
|