resulgit 1.0.2 → 1.0.3
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 +40 -1
- package/package.json +1 -1
- package/resulgit.js +696 -8
package/README.md
CHANGED
|
@@ -90,11 +90,29 @@ resulgit <command> [options]
|
|
|
90
90
|
- `resulgit pr create --title <title> [--source <branch>] [--target <branch>]` - Create pull request
|
|
91
91
|
- `resulgit pr merge --id <id>` - Merge a pull request
|
|
92
92
|
|
|
93
|
-
### Information
|
|
93
|
+
### Information & Inspection
|
|
94
94
|
|
|
95
95
|
- `resulgit current` - Show current repository and branch
|
|
96
96
|
- `resulgit head` - Show HEAD commit ID
|
|
97
97
|
- `resulgit show --commit <id>` - Show commit details
|
|
98
|
+
- `resulgit log [--branch <name>] [--max <N>] [--oneline] [--stats]` - Show commit history with visualization
|
|
99
|
+
- `resulgit blame --path <file>` - Show line-by-line authorship information
|
|
100
|
+
- `resulgit grep --pattern <pattern> [--ignore-case]` - Search for patterns in repository
|
|
101
|
+
- `resulgit ls-files` - List all tracked files
|
|
102
|
+
- `resulgit reflog` - Show reference log history
|
|
103
|
+
- `resulgit cat-file --type <type> --object <id> [--path <file>]` - Display file/commit contents
|
|
104
|
+
- `resulgit rev-parse --rev <ref>` - Parse revision names to commit IDs
|
|
105
|
+
- `resulgit describe [--commit <id>]` - Describe a commit with nearest tag
|
|
106
|
+
- `resulgit shortlog [--branch <name>] [--max <N>]` - Summarize commit log by author
|
|
107
|
+
|
|
108
|
+
### Git Hooks
|
|
109
|
+
|
|
110
|
+
- `resulgit hook list` - List installed hooks
|
|
111
|
+
- `resulgit hook install --name <hook> [--script <code>] [--sample]` - Install a hook
|
|
112
|
+
- `resulgit hook remove --name <hook>` - Remove a hook
|
|
113
|
+
- `resulgit hook show --name <hook>` - Show hook content
|
|
114
|
+
|
|
115
|
+
**Available hooks:** `pre-commit`, `post-commit`, `pre-push`, `post-push`, `pre-merge`, `post-merge`, `pre-checkout`, `post-checkout`
|
|
98
116
|
|
|
99
117
|
## Global Options
|
|
100
118
|
|
|
@@ -128,6 +146,27 @@ resulgit branch create --name feature-branch
|
|
|
128
146
|
|
|
129
147
|
# Merge branches
|
|
130
148
|
resulgit merge --branch feature-branch
|
|
149
|
+
|
|
150
|
+
# View commit history with graph
|
|
151
|
+
resulgit log --max 20
|
|
152
|
+
|
|
153
|
+
# View commit history in one line
|
|
154
|
+
resulgit log --oneline
|
|
155
|
+
|
|
156
|
+
# View commit statistics
|
|
157
|
+
resulgit log --stats
|
|
158
|
+
|
|
159
|
+
# Show line-by-line authorship
|
|
160
|
+
resulgit blame --path src/index.js
|
|
161
|
+
|
|
162
|
+
# Search in repository
|
|
163
|
+
resulgit grep --pattern "TODO"
|
|
164
|
+
|
|
165
|
+
# List tracked files
|
|
166
|
+
resulgit ls-files
|
|
167
|
+
|
|
168
|
+
# Install a pre-commit hook
|
|
169
|
+
resulgit hook install --name pre-commit --sample
|
|
131
170
|
```
|
|
132
171
|
|
|
133
172
|
## Configuration
|
package/package.json
CHANGED
package/resulgit.js
CHANGED
|
@@ -4,6 +4,11 @@ const path = require('path')
|
|
|
4
4
|
const os = require('os')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
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')
|
|
7
12
|
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' }
|
|
8
13
|
function color(str, c) { return (COLORS[c] || '') + String(str) + COLORS.reset }
|
|
9
14
|
|
|
@@ -526,6 +531,73 @@ async function cmdDiff(opts) {
|
|
|
526
531
|
|
|
527
532
|
const filePath = opts.path
|
|
528
533
|
const commitId = opts.commit
|
|
534
|
+
const commit1 = opts.commit1
|
|
535
|
+
const commit2 = opts.commit2
|
|
536
|
+
const showStat = opts.stat === 'true'
|
|
537
|
+
|
|
538
|
+
// Handle diff between two commits: git diff <commit1> <commit2>
|
|
539
|
+
if (commit1 && commit2) {
|
|
540
|
+
const snap1 = await fetchSnapshotByCommit(server, meta.repoId, commit1, token)
|
|
541
|
+
const snap2 = await fetchSnapshotByCommit(server, meta.repoId, commit2, token)
|
|
542
|
+
const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(snap1.files), ...Object.keys(snap2.files)]))
|
|
543
|
+
|
|
544
|
+
let added = 0, deleted = 0, modified = 0
|
|
545
|
+
const stats = []
|
|
546
|
+
|
|
547
|
+
for (const p of files) {
|
|
548
|
+
const content1 = snap1.files[p] !== undefined ? String(snap1.files[p]) : null
|
|
549
|
+
const content2 = snap2.files[p] !== undefined ? String(snap2.files[p]) : null
|
|
550
|
+
if (content1 !== content2) {
|
|
551
|
+
if (content1 === null) {
|
|
552
|
+
added++
|
|
553
|
+
if (showStat) stats.push({ path: p, added: content2.split(/\r?\n/).length, deleted: 0 })
|
|
554
|
+
} else if (content2 === null) {
|
|
555
|
+
deleted++
|
|
556
|
+
if (showStat) stats.push({ path: p, added: 0, deleted: content1.split(/\r?\n/).length })
|
|
557
|
+
} else {
|
|
558
|
+
modified++
|
|
559
|
+
const lines1 = content1.split(/\r?\n/)
|
|
560
|
+
const lines2 = content2.split(/\r?\n/)
|
|
561
|
+
const diff = Math.abs(lines2.length - lines1.length)
|
|
562
|
+
if (showStat) stats.push({ path: p, added: lines2.length > lines1.length ? diff : 0, deleted: lines1.length > lines2.length ? diff : 0 })
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!showStat) {
|
|
566
|
+
if (opts.json === 'true') {
|
|
567
|
+
print({ path: p, old: content1, new: content2 }, true)
|
|
568
|
+
} else {
|
|
569
|
+
process.stdout.write(color(`diff --git a/${p} b/${p}\n`, 'dim'))
|
|
570
|
+
// Show full diff (same as before)
|
|
571
|
+
const oldLines = content1 ? content1.split(/\r?\n/) : []
|
|
572
|
+
const newLines = content2 ? content2.split(/\r?\n/) : []
|
|
573
|
+
const maxLen = Math.max(oldLines.length, newLines.length)
|
|
574
|
+
for (let i = 0; i < maxLen; i++) {
|
|
575
|
+
const oldLine = oldLines[i]
|
|
576
|
+
const newLine = newLines[i]
|
|
577
|
+
if (oldLine !== newLine) {
|
|
578
|
+
if (oldLine !== undefined) process.stdout.write(color(`-${oldLine}\n`, 'red'))
|
|
579
|
+
if (newLine !== undefined) process.stdout.write(color(`+${newLine}\n`, 'green'))
|
|
580
|
+
} else if (oldLine !== undefined) {
|
|
581
|
+
process.stdout.write(` ${oldLine}\n`)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (showStat) {
|
|
590
|
+
if (opts.json === 'true') {
|
|
591
|
+
print({ added, deleted, modified, files: stats }, true)
|
|
592
|
+
} else {
|
|
593
|
+
for (const stat of stats) {
|
|
594
|
+
process.stdout.write(` ${stat.path} | ${stat.added + stat.deleted} ${stat.added > 0 ? color('+' + stat.added, 'green') : ''}${stat.deleted > 0 ? color('-' + stat.deleted, 'red') : ''}\n`)
|
|
595
|
+
}
|
|
596
|
+
process.stdout.write(` ${stats.length} file${stats.length !== 1 ? 's' : ''} changed, ${added} insertion${added !== 1 ? 's' : ''}(+), ${deleted} deletion${deleted !== 1 ? 's' : ''}(-)\n`)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return
|
|
600
|
+
}
|
|
529
601
|
|
|
530
602
|
if (commitId) {
|
|
531
603
|
// Show diff for specific commit
|
|
@@ -535,6 +607,40 @@ async function cmdDiff(opts) {
|
|
|
535
607
|
const parentSnap = parentId ? await fetchSnapshotByCommit(server, meta.repoId, parentId, token) : { files: {}, commitId: '' }
|
|
536
608
|
|
|
537
609
|
const files = filePath ? [filePath] : Object.keys(new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)]))
|
|
610
|
+
|
|
611
|
+
if (showStat) {
|
|
612
|
+
let added = 0, deleted = 0, modified = 0
|
|
613
|
+
const stats = []
|
|
614
|
+
for (const p of files) {
|
|
615
|
+
const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : null
|
|
616
|
+
const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : null
|
|
617
|
+
if (oldContent !== newContent) {
|
|
618
|
+
if (oldContent === null) {
|
|
619
|
+
added++
|
|
620
|
+
if (showStat) stats.push({ path: p, added: newContent.split(/\r?\n/).length, deleted: 0 })
|
|
621
|
+
} else if (newContent === null) {
|
|
622
|
+
deleted++
|
|
623
|
+
if (showStat) stats.push({ path: p, added: 0, deleted: oldContent.split(/\r?\n/).length })
|
|
624
|
+
} else {
|
|
625
|
+
modified++
|
|
626
|
+
const oldLines = oldContent.split(/\r?\n/)
|
|
627
|
+
const newLines = newContent.split(/\r?\n/)
|
|
628
|
+
const diff = Math.abs(newLines.length - oldLines.length)
|
|
629
|
+
if (showStat) stats.push({ path: p, added: newLines.length > oldLines.length ? diff : 0, deleted: oldLines.length > newLines.length ? diff : 0 })
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (opts.json === 'true') {
|
|
634
|
+
print({ added, deleted, modified, files: stats }, true)
|
|
635
|
+
} else {
|
|
636
|
+
for (const stat of stats) {
|
|
637
|
+
process.stdout.write(` ${stat.path} | ${stat.added + stat.deleted} ${stat.added > 0 ? color('+' + stat.added, 'green') : ''}${stat.deleted > 0 ? color('-' + stat.deleted, 'red') : ''}\n`)
|
|
638
|
+
}
|
|
639
|
+
process.stdout.write(` ${stats.length} file${stats.length !== 1 ? 's' : ''} changed, ${added} insertion${added !== 1 ? 's' : ''}(+), ${deleted} deletion${deleted !== 1 ? 's' : ''}(-)\n`)
|
|
640
|
+
}
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
538
644
|
for (const p of files) {
|
|
539
645
|
const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : null
|
|
540
646
|
const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : null
|
|
@@ -620,6 +726,31 @@ async function cmdRm(opts) {
|
|
|
620
726
|
const cfg = loadConfig()
|
|
621
727
|
const server = getServer(opts, cfg) || meta.server
|
|
622
728
|
const token = getToken(opts, cfg) || meta.token
|
|
729
|
+
|
|
730
|
+
// Handle --cached flag (remove from index but keep file)
|
|
731
|
+
if (opts.cached === 'true') {
|
|
732
|
+
// Mark file for deletion in next commit but don't delete from filesystem
|
|
733
|
+
const metaDir = path.join(dir, '.vcs-next')
|
|
734
|
+
const localPath = path.join(metaDir, 'local.json')
|
|
735
|
+
let localMeta = { baseCommitId: '', baseFiles: {}, pendingCommit: null, removedFiles: [] }
|
|
736
|
+
try {
|
|
737
|
+
const s = await fs.promises.readFile(localPath, 'utf8')
|
|
738
|
+
localMeta = JSON.parse(s)
|
|
739
|
+
} catch {}
|
|
740
|
+
if (!localMeta.removedFiles) localMeta.removedFiles = []
|
|
741
|
+
if (!localMeta.removedFiles.includes(pathArg)) {
|
|
742
|
+
localMeta.removedFiles.push(pathArg)
|
|
743
|
+
}
|
|
744
|
+
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
745
|
+
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
746
|
+
if (opts.json === 'true') {
|
|
747
|
+
print({ removed: pathArg, cached: true }, true)
|
|
748
|
+
} else {
|
|
749
|
+
process.stdout.write(color(`Removed '${pathArg}' from index (file kept in working directory)\n`, 'green'))
|
|
750
|
+
}
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
|
|
623
754
|
const u = new URL(`/api/repositories/${meta.repoId}/files`, server)
|
|
624
755
|
u.searchParams.set('branch', meta.branch)
|
|
625
756
|
u.searchParams.set('path', pathArg)
|
|
@@ -640,6 +771,29 @@ async function cmdCommit(opts) {
|
|
|
640
771
|
|
|
641
772
|
const spinner = createSpinner('Preparing commit...', opts.json)
|
|
642
773
|
try {
|
|
774
|
+
// Handle -am flag (add all and commit)
|
|
775
|
+
if (opts.all === 'true' || opts.a === 'true') {
|
|
776
|
+
spinnerUpdate(spinner, 'Staging all changes...')
|
|
777
|
+
await cmdAdd({ dir, all: 'true', json: opts.json })
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Handle --amend flag
|
|
781
|
+
if (opts.amend === 'true') {
|
|
782
|
+
spinnerUpdate(spinner, 'Amending last commit...')
|
|
783
|
+
// Get the last commit and use its message if no new message provided
|
|
784
|
+
const meta = readRemoteMeta(dir)
|
|
785
|
+
const cfg = loadConfig()
|
|
786
|
+
const server = getServer(opts, cfg) || meta.server
|
|
787
|
+
const token = getToken(opts, cfg) || meta.token
|
|
788
|
+
const url = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
789
|
+
url.searchParams.set('branch', meta.branch)
|
|
790
|
+
url.searchParams.set('limit', '1')
|
|
791
|
+
const commits = await request('GET', url.toString(), null, token)
|
|
792
|
+
if (Array.isArray(commits) && commits.length > 0 && !message) {
|
|
793
|
+
opts.message = commits[0].message || 'Amended commit'
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
643
797
|
// Check for unresolved conflicts
|
|
644
798
|
spinnerUpdate(spinner, 'Checking for conflicts...')
|
|
645
799
|
const unresolvedConflicts = await checkForUnresolvedConflicts(dir)
|
|
@@ -669,6 +823,20 @@ async function cmdCommit(opts) {
|
|
|
669
823
|
const local = await collectLocal(dir)
|
|
670
824
|
const files = {}
|
|
671
825
|
for (const [p, v] of Object.entries(local)) files[p] = v.content
|
|
826
|
+
|
|
827
|
+
// Execute pre-commit hook
|
|
828
|
+
spinnerUpdate(spinner, 'Running pre-commit hook...')
|
|
829
|
+
try {
|
|
830
|
+
const hookResult = await hooks.executeHook(dir, 'pre-commit', { message, files: Object.keys(files) })
|
|
831
|
+
if (hookResult.executed && hookResult.exitCode !== 0) {
|
|
832
|
+
spinnerFail(spinner, 'Pre-commit hook failed')
|
|
833
|
+
throw new Error('pre-commit hook failed')
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
if (err.message === 'pre-commit hook failed') throw err
|
|
837
|
+
// Hook doesn't exist or other error, continue
|
|
838
|
+
}
|
|
839
|
+
|
|
672
840
|
localMeta.pendingCommit = { message, files, createdAt: Date.now() }
|
|
673
841
|
// Clear conflicts if they were resolved
|
|
674
842
|
if (localMeta.conflicts) {
|
|
@@ -676,6 +844,12 @@ async function cmdCommit(opts) {
|
|
|
676
844
|
}
|
|
677
845
|
await fs.promises.mkdir(metaDir, { recursive: true })
|
|
678
846
|
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
847
|
+
|
|
848
|
+
// Execute post-commit hook
|
|
849
|
+
try {
|
|
850
|
+
await hooks.executeHook(dir, 'post-commit', { message, files: Object.keys(files) })
|
|
851
|
+
} catch {}
|
|
852
|
+
|
|
679
853
|
spinnerSuccess(spinner, `Staged changes for commit: "${message}"`)
|
|
680
854
|
print({ pendingCommit: message }, opts.json === 'true')
|
|
681
855
|
} catch (err) {
|
|
@@ -1209,6 +1383,19 @@ async function cmdPush(opts) {
|
|
|
1209
1383
|
}
|
|
1210
1384
|
return
|
|
1211
1385
|
}
|
|
1386
|
+
// Execute pre-push hook
|
|
1387
|
+
spinnerUpdate(spinner, 'Running pre-push hook...')
|
|
1388
|
+
try {
|
|
1389
|
+
const hookResult = await hooks.executeHook(dir, 'pre-push', { branch: remoteMeta.branch, files: Object.keys(merged) })
|
|
1390
|
+
if (hookResult.executed && hookResult.exitCode !== 0) {
|
|
1391
|
+
spinnerFail(spinner, 'Pre-push hook failed')
|
|
1392
|
+
throw new Error('pre-push hook failed')
|
|
1393
|
+
}
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
if (err.message === 'pre-push hook failed') throw err
|
|
1396
|
+
// Hook doesn't exist or other error, continue
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1212
1399
|
const body = { message: localMeta.pendingCommit?.message || (opts.message || 'Push'), files: merged, branchName: remoteMeta.branch }
|
|
1213
1400
|
spinnerUpdate(spinner, `Pushing to '${remoteMeta.branch}'...`)
|
|
1214
1401
|
const url = new URL(`/api/repositories/${remoteMeta.repoId}/commits`, server).toString()
|
|
@@ -1217,6 +1404,12 @@ async function cmdPush(opts) {
|
|
|
1217
1404
|
localMeta.baseFiles = merged
|
|
1218
1405
|
localMeta.pendingCommit = null
|
|
1219
1406
|
await fs.promises.writeFile(localPath, JSON.stringify(localMeta, null, 2))
|
|
1407
|
+
|
|
1408
|
+
// Execute post-push hook
|
|
1409
|
+
try {
|
|
1410
|
+
await hooks.executeHook(dir, 'post-push', { branch: remoteMeta.branch, commitId: localMeta.baseCommitId })
|
|
1411
|
+
} catch {}
|
|
1412
|
+
|
|
1220
1413
|
spinnerSuccess(spinner, `Pushed to '${remoteMeta.branch}' (commit: ${(data.id || '').slice(0, 7)})`)
|
|
1221
1414
|
print({ pushed: localMeta.baseCommitId }, opts.json === 'true')
|
|
1222
1415
|
} catch (err) {
|
|
@@ -1564,7 +1757,20 @@ async function cmdBranch(sub, opts) {
|
|
|
1564
1757
|
if (m) current = m[1]
|
|
1565
1758
|
} catch { }
|
|
1566
1759
|
process.stdout.write(color('Branches:\n', 'bold'))
|
|
1567
|
-
|
|
1760
|
+
let list = (data.branches || []).map(b => ({ name: b.name, commitId: b.commitId || '', date: b.lastCommitDate || b.createdAt || '' }))
|
|
1761
|
+
|
|
1762
|
+
// Handle --sort option
|
|
1763
|
+
if (opts.sort) {
|
|
1764
|
+
const sortBy = opts.sort.replace(/^-/, '') // Remove leading dash
|
|
1765
|
+
if (sortBy === 'committerdate' || sortBy === '-committerdate') {
|
|
1766
|
+
list.sort((a, b) => {
|
|
1767
|
+
const dateA = new Date(a.date || 0).getTime()
|
|
1768
|
+
const dateB = new Date(b.date || 0).getTime()
|
|
1769
|
+
return sortBy.startsWith('-') ? dateB - dateA : dateA - dateB
|
|
1770
|
+
})
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1568
1774
|
for (const b of list) {
|
|
1569
1775
|
const isCur = b.name === current
|
|
1570
1776
|
const mark = isCur ? color('*', 'green') : ' '
|
|
@@ -1588,12 +1794,17 @@ async function cmdBranch(sub, opts) {
|
|
|
1588
1794
|
if (sub === 'delete') {
|
|
1589
1795
|
const name = opts.name
|
|
1590
1796
|
if (!name) throw new Error('Missing --name')
|
|
1797
|
+
const force = opts.force === 'true' || opts.D === 'true'
|
|
1591
1798
|
const u = new URL(`/api/repositories/${meta.repoId}/branches`, server)
|
|
1592
1799
|
u.searchParams.set('name', name)
|
|
1800
|
+
u.searchParams.set('force', force ? 'true' : 'false')
|
|
1593
1801
|
const headers = token ? { Authorization: `Bearer ${token}` } : {}
|
|
1594
1802
|
const res = await fetch(u.toString(), { method: 'DELETE', headers })
|
|
1595
1803
|
if (!res.ok) {
|
|
1596
1804
|
const body = await res.text().catch(() => '')
|
|
1805
|
+
if (force) {
|
|
1806
|
+
throw new Error(`Branch force delete failed: ${res.status} ${res.statusText} ${body}`)
|
|
1807
|
+
}
|
|
1597
1808
|
throw new Error(`Branch delete failed: ${res.status} ${res.statusText} ${body}`)
|
|
1598
1809
|
}
|
|
1599
1810
|
const data = await res.json()
|
|
@@ -1622,10 +1833,24 @@ async function cmdSwitch(opts) {
|
|
|
1622
1833
|
const dir = path.resolve(opts.dir || '.')
|
|
1623
1834
|
const meta = readRemoteMeta(dir)
|
|
1624
1835
|
const branch = opts.branch
|
|
1836
|
+
const create = opts.create === 'true' || opts.c === 'true'
|
|
1625
1837
|
if (!branch) throw new Error('Missing --branch')
|
|
1626
1838
|
const cfg = loadConfig()
|
|
1627
1839
|
const server = getServer(opts, cfg) || meta.server
|
|
1628
1840
|
const token = getToken(opts, cfg) || meta.token
|
|
1841
|
+
|
|
1842
|
+
// Handle -c flag (create and switch)
|
|
1843
|
+
if (create) {
|
|
1844
|
+
// Check if branch exists
|
|
1845
|
+
const branchesUrl = new URL(`/api/repositories/${meta.repoId}/branches`, server)
|
|
1846
|
+
const branchesData = await request('GET', branchesUrl.toString(), null, token)
|
|
1847
|
+
const exists = (branchesData.branches || []).some(b => b.name === branch)
|
|
1848
|
+
if (!exists) {
|
|
1849
|
+
// Create the branch
|
|
1850
|
+
await cmdBranch('create', { dir, name: branch, base: meta.branch, repo: meta.repoId, server, token })
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1629
1854
|
await pullToDir(meta.repoId, branch, dir, server, token)
|
|
1630
1855
|
print({ repoId: meta.repoId, branch, dir }, opts.json === 'true')
|
|
1631
1856
|
}
|
|
@@ -1980,10 +2205,28 @@ async function cmdReset(opts) {
|
|
|
1980
2205
|
const server = getServer(opts, cfg) || meta.server
|
|
1981
2206
|
const token = getToken(opts, cfg) || meta.token
|
|
1982
2207
|
|
|
1983
|
-
|
|
2208
|
+
let commitId = opts.commit || 'HEAD'
|
|
1984
2209
|
const mode = opts.mode || 'mixed' // soft, mixed, hard
|
|
1985
2210
|
const filePath = opts.path
|
|
1986
2211
|
|
|
2212
|
+
// Handle HEAD^ syntax (parent commit)
|
|
2213
|
+
if (commitId === 'HEAD^' || commitId.endsWith('^')) {
|
|
2214
|
+
const baseCommit = commitId.replace(/\^+$/, '')
|
|
2215
|
+
const targetCommit = baseCommit === 'HEAD' ? 'HEAD' : baseCommit
|
|
2216
|
+
let targetSnap
|
|
2217
|
+
if (targetCommit === 'HEAD') {
|
|
2218
|
+
targetSnap = await fetchRemoteSnapshot(server, meta.repoId, meta.branch, token)
|
|
2219
|
+
} else {
|
|
2220
|
+
targetSnap = await fetchSnapshotByCommit(server, meta.repoId, targetCommit, token)
|
|
2221
|
+
}
|
|
2222
|
+
const commit = await fetchCommitMeta(server, meta.repoId, targetSnap.commitId, token)
|
|
2223
|
+
const parentId = (Array.isArray(commit.parents) && commit.parents[0]) || ''
|
|
2224
|
+
if (!parentId) {
|
|
2225
|
+
throw new Error('No parent commit found')
|
|
2226
|
+
}
|
|
2227
|
+
commitId = parentId
|
|
2228
|
+
}
|
|
2229
|
+
|
|
1987
2230
|
if (filePath) {
|
|
1988
2231
|
// Reset specific file (unstage)
|
|
1989
2232
|
const metaDir = path.join(dir, '.vcs-next')
|
|
@@ -2354,6 +2597,400 @@ async function cmdRebase(opts) {
|
|
|
2354
2597
|
print({ rebased: sourceBranch, onto }, opts.json === 'true')
|
|
2355
2598
|
}
|
|
2356
2599
|
}
|
|
2600
|
+
|
|
2601
|
+
async function cmdBlame(opts) {
|
|
2602
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2603
|
+
const filePath = opts.path
|
|
2604
|
+
if (!filePath) throw new errors.ValidationError('Missing --path', 'path')
|
|
2605
|
+
|
|
2606
|
+
const validPath = validation.validateFilePath(filePath)
|
|
2607
|
+
const meta = readRemoteMeta(dir)
|
|
2608
|
+
const cfg = loadConfig()
|
|
2609
|
+
const server = getServer(opts, cfg) || meta.server
|
|
2610
|
+
const token = getToken(opts, cfg) || meta.token
|
|
2611
|
+
|
|
2612
|
+
const spinner = createSpinner(`Getting blame for ${validPath}...`, opts.json)
|
|
2613
|
+
|
|
2614
|
+
try {
|
|
2615
|
+
// Get file content
|
|
2616
|
+
const local = await collectLocal(dir)
|
|
2617
|
+
if (!local[validPath]) {
|
|
2618
|
+
throw new errors.FileSystemError(`File not found: ${validPath}`, validPath, 'read')
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// Get commits
|
|
2622
|
+
const commitsUrl = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
2623
|
+
commitsUrl.searchParams.set('branch', meta.branch)
|
|
2624
|
+
const commitsRes = await fetch(commitsUrl.toString(), {
|
|
2625
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
2626
|
+
})
|
|
2627
|
+
|
|
2628
|
+
if (!commitsRes.ok) {
|
|
2629
|
+
throw new errors.NetworkError('Failed to fetch commits', commitsRes.status, commitsUrl.toString())
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const commits = await commitsRes.json()
|
|
2633
|
+
const blameData = parseBlame(local[validPath].content, commits, validPath)
|
|
2634
|
+
|
|
2635
|
+
spinnerSuccess(spinner, `Blame for ${validPath}`)
|
|
2636
|
+
|
|
2637
|
+
if (opts.json === 'true') {
|
|
2638
|
+
print(formatBlameJson(blameData), false)
|
|
2639
|
+
} else {
|
|
2640
|
+
process.stdout.write(formatBlameOutput(blameData) + '\n')
|
|
2641
|
+
}
|
|
2642
|
+
} catch (err) {
|
|
2643
|
+
spinnerFail(spinner, 'Blame failed')
|
|
2644
|
+
throw err
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
async function cmdLog(opts) {
|
|
2649
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2650
|
+
const meta = readRemoteMeta(dir)
|
|
2651
|
+
const cfg = loadConfig()
|
|
2652
|
+
const server = getServer(opts, cfg) || meta.server
|
|
2653
|
+
const token = getToken(opts, cfg) || meta.token
|
|
2654
|
+
|
|
2655
|
+
const spinner = createSpinner('Fetching commit history...', opts.json)
|
|
2656
|
+
|
|
2657
|
+
try {
|
|
2658
|
+
const url = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
2659
|
+
if (opts.branch) url.searchParams.set('branch', opts.branch)
|
|
2660
|
+
else url.searchParams.set('branch', meta.branch)
|
|
2661
|
+
|
|
2662
|
+
const data = await request('GET', url.toString(), null, token)
|
|
2663
|
+
let commits = Array.isArray(data) ? data : []
|
|
2664
|
+
|
|
2665
|
+
// Filter by file path if provided
|
|
2666
|
+
const filePath = opts.path
|
|
2667
|
+
if (filePath) {
|
|
2668
|
+
const filteredCommits = []
|
|
2669
|
+
for (const commit of commits) {
|
|
2670
|
+
const commitSnap = await fetchSnapshotByCommit(server, meta.repoId, commit.id || commit._id, token)
|
|
2671
|
+
if (commitSnap.files[filePath] !== undefined) {
|
|
2672
|
+
filteredCommits.push(commit)
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
commits = filteredCommits
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// Filter by content pattern (-G flag)
|
|
2679
|
+
const pattern = opts.G || opts.pattern
|
|
2680
|
+
if (pattern) {
|
|
2681
|
+
const regex = new RegExp(pattern, opts.ignoreCase === 'true' ? 'i' : '')
|
|
2682
|
+
const filteredCommits = []
|
|
2683
|
+
for (const commit of commits) {
|
|
2684
|
+
const commitSnap = await fetchSnapshotByCommit(server, meta.repoId, commit.id || commit._id, token)
|
|
2685
|
+
const parentId = (Array.isArray(commit.parents) && commit.parents[0]) || ''
|
|
2686
|
+
const parentSnap = parentId ? await fetchSnapshotByCommit(server, meta.repoId, parentId, token) : { files: {} }
|
|
2687
|
+
|
|
2688
|
+
// Check if pattern matches in any file changed in this commit
|
|
2689
|
+
const allPaths = new Set([...Object.keys(parentSnap.files), ...Object.keys(commitSnap.files)])
|
|
2690
|
+
let matches = false
|
|
2691
|
+
for (const p of allPaths) {
|
|
2692
|
+
const oldContent = parentSnap.files[p] !== undefined ? String(parentSnap.files[p]) : ''
|
|
2693
|
+
const newContent = commitSnap.files[p] !== undefined ? String(commitSnap.files[p]) : ''
|
|
2694
|
+
if (regex.test(oldContent) || regex.test(newContent)) {
|
|
2695
|
+
matches = true
|
|
2696
|
+
break
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
if (matches) {
|
|
2700
|
+
filteredCommits.push(commit)
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
commits = filteredCommits
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
spinnerSuccess(spinner, `Found ${commits.length} commits`)
|
|
2707
|
+
|
|
2708
|
+
if (opts.json === 'true') {
|
|
2709
|
+
print(commits, true)
|
|
2710
|
+
} else if (opts.oneline === 'true') {
|
|
2711
|
+
process.stdout.write(formatCompactLog(commits) + '\n')
|
|
2712
|
+
} else if (opts.stats === 'true') {
|
|
2713
|
+
const stats = generateCommitStats(commits)
|
|
2714
|
+
process.stdout.write(color('Commit Statistics\n', 'bold'))
|
|
2715
|
+
process.stdout.write(color('━'.repeat(50) + '\n', 'dim'))
|
|
2716
|
+
process.stdout.write(`Total commits: ${stats.totalCommits}\n`)
|
|
2717
|
+
process.stdout.write(`\nTop authors:\n`)
|
|
2718
|
+
for (const author of stats.topAuthors) {
|
|
2719
|
+
process.stdout.write(` ${author.name.padEnd(30)} ${color(author.commits + ' commits', 'cyan')}\n`)
|
|
2720
|
+
}
|
|
2721
|
+
if (stats.datesRange.earliest && stats.datesRange.latest) {
|
|
2722
|
+
process.stdout.write(`\nDate range: ${stats.datesRange.earliest.toISOString().split('T')[0]} to ${stats.datesRange.latest.toISOString().split('T')[0]}\n`)
|
|
2723
|
+
}
|
|
2724
|
+
} else {
|
|
2725
|
+
const maxCommits = parseInt(opts.max || '50', 10)
|
|
2726
|
+
process.stdout.write(generateLogGraph(commits, { maxCommits }) + '\n')
|
|
2727
|
+
}
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
spinnerFail(spinner, 'Log fetch failed')
|
|
2730
|
+
throw err
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
async function cmdHook(sub, opts) {
|
|
2735
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2736
|
+
|
|
2737
|
+
if (sub === 'list') {
|
|
2738
|
+
const hooksList = await hooks.listHooks(dir)
|
|
2739
|
+
if (opts.json === 'true') {
|
|
2740
|
+
print(hooksList, true)
|
|
2741
|
+
} else {
|
|
2742
|
+
if (hooksList.length === 0) {
|
|
2743
|
+
process.stdout.write('No hooks installed.\n')
|
|
2744
|
+
} else {
|
|
2745
|
+
process.stdout.write(color('Installed Hooks:\n', 'bold'))
|
|
2746
|
+
for (const hook of hooksList) {
|
|
2747
|
+
const exe = hook.executable ? color('✓', 'green') : color('✗', 'red')
|
|
2748
|
+
process.stdout.write(` ${exe} ${color(hook.name, 'cyan')}\n`)
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
if (sub === 'install') {
|
|
2756
|
+
const hookName = opts.name
|
|
2757
|
+
if (!hookName) throw new Error('Missing --name')
|
|
2758
|
+
|
|
2759
|
+
let script = opts.script
|
|
2760
|
+
if (!script && opts.sample === 'true') {
|
|
2761
|
+
script = hooks.SAMPLE_HOOKS[hookName]
|
|
2762
|
+
if (!script) throw new Error(`No sample available for ${hookName}`)
|
|
2763
|
+
}
|
|
2764
|
+
if (!script) throw new Error('Missing --script or use --sample')
|
|
2765
|
+
|
|
2766
|
+
const result = await hooks.installHook(dir, hookName, script)
|
|
2767
|
+
print(result, opts.json === 'true')
|
|
2768
|
+
if (opts.json !== 'true') {
|
|
2769
|
+
process.stdout.write(color(`✓ Hook '${hookName}' installed\n`, 'green'))
|
|
2770
|
+
}
|
|
2771
|
+
return
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
if (sub === 'remove') {
|
|
2775
|
+
const hookName = opts.name
|
|
2776
|
+
if (!hookName) throw new Error('Missing --name')
|
|
2777
|
+
|
|
2778
|
+
const result = await hooks.removeHook(dir, hookName)
|
|
2779
|
+
print(result, opts.json === 'true')
|
|
2780
|
+
return
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
if (sub === 'show') {
|
|
2784
|
+
const hookName = opts.name
|
|
2785
|
+
if (!hookName) throw new Error('Missing --name')
|
|
2786
|
+
|
|
2787
|
+
const result = await hooks.readHook(dir, hookName)
|
|
2788
|
+
if (result.content) {
|
|
2789
|
+
process.stdout.write(result.content + '\n')
|
|
2790
|
+
} else {
|
|
2791
|
+
process.stdout.write(`Hook '${hookName}' not found.\n`)
|
|
2792
|
+
}
|
|
2793
|
+
return
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
throw new Error('Unknown hook subcommand. Use: list, install, remove, show')
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
async function cmdGrep(opts) {
|
|
2800
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2801
|
+
const pattern = opts.pattern || opts.p || ''
|
|
2802
|
+
if (!pattern) throw new Error('Missing --pattern or -p')
|
|
2803
|
+
|
|
2804
|
+
const meta = readRemoteMeta(dir)
|
|
2805
|
+
const local = await collectLocal(dir)
|
|
2806
|
+
const results = []
|
|
2807
|
+
|
|
2808
|
+
const regex = new RegExp(pattern, opts.ignoreCase === 'true' ? 'i' : '')
|
|
2809
|
+
|
|
2810
|
+
for (const [filePath, fileData] of Object.entries(local)) {
|
|
2811
|
+
const lines = fileData.content.split(/\r?\n/)
|
|
2812
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2813
|
+
if (regex.test(lines[i])) {
|
|
2814
|
+
results.push({
|
|
2815
|
+
path: filePath,
|
|
2816
|
+
line: i + 1,
|
|
2817
|
+
content: lines[i].trim()
|
|
2818
|
+
})
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
if (opts.json === 'true') {
|
|
2824
|
+
print(results, true)
|
|
2825
|
+
} else {
|
|
2826
|
+
for (const result of results) {
|
|
2827
|
+
process.stdout.write(color(`${result.path}:${result.line}`, 'cyan'))
|
|
2828
|
+
process.stdout.write(`: ${result.content}\n`)
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
async function cmdLsFiles(opts) {
|
|
2834
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2835
|
+
const meta = readRemoteMeta(dir)
|
|
2836
|
+
const cfg = loadConfig()
|
|
2837
|
+
const server = getServer(opts, cfg) || meta.server
|
|
2838
|
+
const token = getToken(opts, cfg) || meta.token
|
|
2839
|
+
|
|
2840
|
+
const remote = await fetchRemoteFilesMap(server, meta.repoId, meta.branch, token)
|
|
2841
|
+
const files = Object.keys(remote.map)
|
|
2842
|
+
|
|
2843
|
+
if (opts.json === 'true') {
|
|
2844
|
+
print(files.map(f => ({ path: f })), true)
|
|
2845
|
+
} else {
|
|
2846
|
+
for (const file of files) {
|
|
2847
|
+
process.stdout.write(`${file}\n`)
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
async function cmdReflog(opts) {
|
|
2853
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2854
|
+
const metaDir = path.join(dir, '.vcs-next')
|
|
2855
|
+
const reflogPath = path.join(metaDir, 'reflog.json')
|
|
2856
|
+
|
|
2857
|
+
let reflog = []
|
|
2858
|
+
try {
|
|
2859
|
+
const content = await fs.promises.readFile(reflogPath, 'utf8')
|
|
2860
|
+
reflog = JSON.parse(content)
|
|
2861
|
+
} catch {}
|
|
2862
|
+
|
|
2863
|
+
if (opts.json === 'true') {
|
|
2864
|
+
print(reflog, true)
|
|
2865
|
+
} else {
|
|
2866
|
+
if (reflog.length === 0) {
|
|
2867
|
+
process.stdout.write('No reflog entries.\n')
|
|
2868
|
+
} else {
|
|
2869
|
+
for (const entry of reflog) {
|
|
2870
|
+
const date = new Date(entry.timestamp).toLocaleString()
|
|
2871
|
+
process.stdout.write(`${color(entry.commitId.slice(0, 7), 'yellow')} ${entry.action} ${date} ${entry.message || ''}\n`)
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
async function cmdCatFile(opts) {
|
|
2878
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2879
|
+
const type = opts.type || ''
|
|
2880
|
+
const object = opts.object || ''
|
|
2881
|
+
|
|
2882
|
+
if (!type || !object) throw new Error('Missing --type and --object')
|
|
2883
|
+
|
|
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
|
+
if (type === 'blob') {
|
|
2890
|
+
const snap = await fetchSnapshotByCommit(server, meta.repoId, object, token)
|
|
2891
|
+
const filePath = opts.path || ''
|
|
2892
|
+
if (filePath && snap.files[filePath]) {
|
|
2893
|
+
process.stdout.write(String(snap.files[filePath]))
|
|
2894
|
+
} else {
|
|
2895
|
+
throw new Error('File not found in commit')
|
|
2896
|
+
}
|
|
2897
|
+
} else if (type === 'commit') {
|
|
2898
|
+
const commit = await fetchCommitMeta(server, meta.repoId, object, token)
|
|
2899
|
+
if (opts.json === 'true') {
|
|
2900
|
+
print(commit, true)
|
|
2901
|
+
} else {
|
|
2902
|
+
process.stdout.write(`commit ${commit.id || commit._id}\n`)
|
|
2903
|
+
process.stdout.write(`Author: ${commit.author?.name || ''} <${commit.author?.email || ''}>\n`)
|
|
2904
|
+
process.stdout.write(`Date: ${new Date(commit.createdAt || commit.committer?.date || '').toLocaleString()}\n\n`)
|
|
2905
|
+
process.stdout.write(`${commit.message || ''}\n`)
|
|
2906
|
+
}
|
|
2907
|
+
} else {
|
|
2908
|
+
throw new Error(`Unsupported type: ${type}`)
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
async function cmdRevParse(opts) {
|
|
2913
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2914
|
+
const rev = opts.rev || 'HEAD'
|
|
2915
|
+
|
|
2916
|
+
const meta = readRemoteMeta(dir)
|
|
2917
|
+
const cfg = loadConfig()
|
|
2918
|
+
const server = getServer(opts, cfg) || meta.server
|
|
2919
|
+
const token = getToken(opts, cfg) || meta.token
|
|
2920
|
+
|
|
2921
|
+
if (rev === 'HEAD') {
|
|
2922
|
+
const info = await request('GET', new URL(`/api/repositories/${meta.repoId}/branches`, server).toString(), null, token)
|
|
2923
|
+
const found = (info.branches || []).find(b => b.name === meta.branch)
|
|
2924
|
+
const commitId = found ? (found.commitId || '') : ''
|
|
2925
|
+
print(opts.json === 'true' ? { rev, commitId } : commitId, opts.json === 'true')
|
|
2926
|
+
} else if (rev.startsWith('refs/heads/')) {
|
|
2927
|
+
const branchName = rev.replace('refs/heads/', '')
|
|
2928
|
+
const info = await request('GET', new URL(`/api/repositories/${meta.repoId}/branches`, server).toString(), null, token)
|
|
2929
|
+
const found = (info.branches || []).find(b => b.name === branchName)
|
|
2930
|
+
const commitId = found ? (found.commitId || '') : ''
|
|
2931
|
+
print(opts.json === 'true' ? { rev, commitId } : commitId, opts.json === 'true')
|
|
2932
|
+
} else {
|
|
2933
|
+
// Assume it's a commit ID
|
|
2934
|
+
print(opts.json === 'true' ? { rev, commitId: rev } : rev, opts.json === 'true')
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
async function cmdDescribe(opts) {
|
|
2939
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2940
|
+
const commitId = opts.commit || 'HEAD'
|
|
2941
|
+
|
|
2942
|
+
const meta = readRemoteMeta(dir)
|
|
2943
|
+
const cfg = loadConfig()
|
|
2944
|
+
const server = getServer(opts, cfg) || meta.server
|
|
2945
|
+
const token = getToken(opts, cfg) || meta.token
|
|
2946
|
+
|
|
2947
|
+
// Get tags
|
|
2948
|
+
const tagsUrl = new URL(`/api/repositories/${meta.repoId}/tags`, server)
|
|
2949
|
+
const tags = await request('GET', tagsUrl.toString(), null, token)
|
|
2950
|
+
const tagsList = Array.isArray(tags) ? tags : []
|
|
2951
|
+
|
|
2952
|
+
// Find nearest tag (simplified - just find any tag)
|
|
2953
|
+
const nearestTag = tagsList[0]
|
|
2954
|
+
|
|
2955
|
+
if (nearestTag) {
|
|
2956
|
+
const desc = `${nearestTag.name}-0-g${commitId.slice(0, 7)}`
|
|
2957
|
+
print(opts.json === 'true' ? { tag: nearestTag.name, commitId, describe: desc } : desc, opts.json === 'true')
|
|
2958
|
+
} else {
|
|
2959
|
+
print(opts.json === 'true' ? { commitId, describe: commitId.slice(0, 7) } : commitId.slice(0, 7), opts.json === 'true')
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
async function cmdShortlog(opts) {
|
|
2964
|
+
const dir = path.resolve(opts.dir || '.')
|
|
2965
|
+
const meta = readRemoteMeta(dir)
|
|
2966
|
+
const cfg = loadConfig()
|
|
2967
|
+
const server = getServer(opts, cfg) || meta.server
|
|
2968
|
+
const token = getToken(opts, cfg) || meta.token
|
|
2969
|
+
|
|
2970
|
+
const url = new URL(`/api/repositories/${meta.repoId}/commits`, server)
|
|
2971
|
+
if (opts.branch) url.searchParams.set('branch', opts.branch)
|
|
2972
|
+
else url.searchParams.set('branch', meta.branch)
|
|
2973
|
+
|
|
2974
|
+
const commits = await request('GET', url.toString(), null, token)
|
|
2975
|
+
const commitsList = Array.isArray(commits) ? commits : []
|
|
2976
|
+
|
|
2977
|
+
const stats = generateCommitStats(commitsList)
|
|
2978
|
+
|
|
2979
|
+
if (opts.json === 'true') {
|
|
2980
|
+
print(stats, true)
|
|
2981
|
+
} else {
|
|
2982
|
+
for (const author of stats.topAuthors) {
|
|
2983
|
+
process.stdout.write(`\n${color(author.name, 'bold')} (${author.commits} commit${author.commits !== 1 ? 's' : ''})\n`)
|
|
2984
|
+
// Show commit messages for this author
|
|
2985
|
+
const authorCommits = commitsList.filter(c => (c.author?.name || 'Unknown') === author.name)
|
|
2986
|
+
for (const commit of authorCommits.slice(0, opts.max ? parseInt(opts.max, 10) : 10)) {
|
|
2987
|
+
const msg = (commit.message || 'No message').split('\n')[0].slice(0, 60)
|
|
2988
|
+
process.stdout.write(` ${color((commit.id || commit._id || '').slice(0, 7), 'yellow')} ${msg}\n`)
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2357
2994
|
function help() {
|
|
2358
2995
|
const h = [
|
|
2359
2996
|
'Usage: resulgit <group> <command> [options]',
|
|
@@ -2368,26 +3005,37 @@ function help() {
|
|
|
2368
3005
|
' repo log --repo <id> [--branch <name>] [--json]',
|
|
2369
3006
|
' repo head --repo <id> [--branch <name>] [--json]',
|
|
2370
3007
|
' repo select [--workspace] (interactive select and clone/open)',
|
|
2371
|
-
' branch list|create|delete|rename [--dir <path>] [--name <branch>] [--base <branch>] [--old <name>] [--new <name>]',
|
|
2372
|
-
' switch --branch <name> [--dir <path>]',
|
|
3008
|
+
' branch list|create|delete|rename [--dir <path>] [--name <branch>] [--base <branch>] [--old <name>] [--new <name>] [--sort <field>] [--force|-D]',
|
|
3009
|
+
' switch --branch <name> [--create|-c] [--dir <path>]',
|
|
3010
|
+
' checkout --branch <name> [--create|-b] [--commit <id>] [--dir <path>]',
|
|
2373
3011
|
' current [--dir <path>] (show active repo/branch)',
|
|
2374
3012
|
' add <file> [--content <text>] [--from <src>] [--all] [--dir <path>] [--commit-message <text>]',
|
|
2375
3013
|
' tag list|create|delete [--dir <path>] [--name <tag>] [--branch <name>]',
|
|
2376
3014
|
' status [--dir <path>] [--json]',
|
|
2377
|
-
' diff [--dir <path>] [--path <file>] [--commit <id>] [--json]',
|
|
2378
|
-
' commit --message <text> [--dir <path>] [--json]',
|
|
3015
|
+
' diff [--dir <path>] [--path <file>] [--commit <id>] [--commit1 <id>] [--commit2 <id>] [--stat] [--json]',
|
|
3016
|
+
' commit --message <text> [--all|-a] [--amend] [--dir <path>] [--json]',
|
|
2379
3017
|
' push [--dir <path>] [--json]',
|
|
2380
3018
|
' head [--dir <path>] [--json]',
|
|
2381
|
-
' rm --path <file> [--dir <path>] [--json]',
|
|
3019
|
+
' rm --path <file> [--cached] [--dir <path>] [--json]',
|
|
2382
3020
|
' pull [--dir <path>]',
|
|
2383
3021
|
' fetch [--dir <path>]',
|
|
2384
3022
|
' merge --branch <name> [--squash] [--no-push] [--dir <path>]',
|
|
2385
3023
|
' stash [save|list|pop|apply|drop|clear] [--message <msg>] [--index <n>] [--dir <path>]',
|
|
2386
3024
|
' restore --path <file> [--source <commit>] [--dir <path>]',
|
|
2387
3025
|
' revert --commit <id> [--no-push] [--dir <path>]',
|
|
2388
|
-
' reset [--commit <id
|
|
3026
|
+
' reset [--commit <id>|HEAD^] [--mode <soft|mixed|hard>] [--path <file>] [--dir <path>]',
|
|
2389
3027
|
' show --commit <id> [--dir <path>] [--json]',
|
|
2390
3028
|
' mv --from <old> --to <new> [--dir <path>]',
|
|
3029
|
+
' blame --path <file> [--dir <path>] [--json] - Show line-by-line authorship',
|
|
3030
|
+
' log [--branch <name>] [--max <N>] [--oneline] [--stats] [--path <file>] [-G <pattern>] [--json] - Show commit history',
|
|
3031
|
+
' hook list|install|remove|show [--name <hook>] [--script <code>] [--sample] - Manage Git hooks',
|
|
3032
|
+
' grep --pattern <pattern> [--ignore-case] [--dir <path>] [--json] - Search in repository',
|
|
3033
|
+
' ls-files [--dir <path>] [--json] - List tracked files',
|
|
3034
|
+
' reflog [--dir <path>] [--json] - Show reference log',
|
|
3035
|
+
' cat-file --type <type> --object <id> [--path <file>] [--dir <path>] [--json] - Display file contents',
|
|
3036
|
+
' rev-parse --rev <ref> [--dir <path>] [--json] - Parse revision names',
|
|
3037
|
+
' describe [--commit <id>] [--dir <path>] [--json] - Describe a commit',
|
|
3038
|
+
' shortlog [--branch <name>] [--max <N>] [--dir <path>] [--json] - Summarize commit log',
|
|
2391
3039
|
'',
|
|
2392
3040
|
'Conflict Resolution:',
|
|
2393
3041
|
' When conflicts occur (merge/cherry-pick/revert), conflict markers are written to files:',
|
|
@@ -2562,6 +3210,46 @@ async function main() {
|
|
|
2562
3210
|
await cmdAdd(opts)
|
|
2563
3211
|
return
|
|
2564
3212
|
}
|
|
3213
|
+
if (cmd[0] === 'blame') {
|
|
3214
|
+
await cmdBlame(opts)
|
|
3215
|
+
return
|
|
3216
|
+
}
|
|
3217
|
+
if (cmd[0] === 'log') {
|
|
3218
|
+
await cmdLog(opts)
|
|
3219
|
+
return
|
|
3220
|
+
}
|
|
3221
|
+
if (cmd[0] === 'hook') {
|
|
3222
|
+
await cmdHook(cmd[1], opts)
|
|
3223
|
+
return
|
|
3224
|
+
}
|
|
3225
|
+
if (cmd[0] === 'grep') {
|
|
3226
|
+
await cmdGrep(opts)
|
|
3227
|
+
return
|
|
3228
|
+
}
|
|
3229
|
+
if (cmd[0] === 'ls-files') {
|
|
3230
|
+
await cmdLsFiles(opts)
|
|
3231
|
+
return
|
|
3232
|
+
}
|
|
3233
|
+
if (cmd[0] === 'reflog') {
|
|
3234
|
+
await cmdReflog(opts)
|
|
3235
|
+
return
|
|
3236
|
+
}
|
|
3237
|
+
if (cmd[0] === 'cat-file') {
|
|
3238
|
+
await cmdCatFile(opts)
|
|
3239
|
+
return
|
|
3240
|
+
}
|
|
3241
|
+
if (cmd[0] === 'rev-parse') {
|
|
3242
|
+
await cmdRevParse(opts)
|
|
3243
|
+
return
|
|
3244
|
+
}
|
|
3245
|
+
if (cmd[0] === 'describe') {
|
|
3246
|
+
await cmdDescribe(opts)
|
|
3247
|
+
return
|
|
3248
|
+
}
|
|
3249
|
+
if (cmd[0] === 'shortlog') {
|
|
3250
|
+
await cmdShortlog(opts)
|
|
3251
|
+
return
|
|
3252
|
+
}
|
|
2565
3253
|
throw new Error('Unknown command')
|
|
2566
3254
|
}
|
|
2567
3255
|
|