gent-cli 2.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -42
- package/package.json +1 -1
- package/src/commands/add.js +102 -23
- package/src/commands/clone.js +137 -86
- package/src/commands/commit.js +89 -81
- package/src/commands/diff.js +257 -0
- package/src/commands/init.js +5 -36
- package/src/commands/log.js +57 -13
- package/src/commands/merge.js +245 -0
- package/src/commands/pull.js +176 -86
- package/src/commands/push.js +172 -79
- package/src/commands/remote.js +97 -113
- package/src/commands/reset.js +149 -0
- package/src/commands/rm.js +85 -0
- package/src/commands/show.js +167 -0
- package/src/commands/stash.js +255 -0
- package/src/commands/status.js +80 -46
- package/src/commands/tag.js +146 -0
- package/src/index.js +108 -57
- package/src/utils/constants.js +10 -26
- package/src/utils/diff-engine.js +236 -0
- package/src/utils/fileSystem.js +8 -60
- package/src/utils/hash-engine.js +337 -0
- package/src/utils/merge-engine.js +379 -0
- package/src/utils/object-store.js +54 -0
- package/src/commands/create.js +0 -121
- package/src/commands/list.js +0 -67
- package/src/services/repo-service.js +0 -284
- package/src/utils/cloud-sync.js +0 -323
- package/src/utils/diff.js +0 -121
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Rm Command - Remove files from working tree and staging
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Remove files from tracking and optionally from disk. Like `git rm`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent rm <file...> → Remove file(s) from tracking + disk
|
|
11
|
+
* gent rm --cached <file...> → Remove from staging only (keep on disk)
|
|
12
|
+
*
|
|
13
|
+
* ALGORITHM:
|
|
14
|
+
* Adds a "deleted" entry to staging so commit records the removal.
|
|
15
|
+
* Without --cached, also deletes the file from the filesystem.
|
|
16
|
+
*
|
|
17
|
+
* BACKEND EXPECTATIONS:
|
|
18
|
+
* Deletion is recorded as absence in the commit tree. Backend removes
|
|
19
|
+
* file entry from tree when processing the push.
|
|
20
|
+
*
|
|
21
|
+
* ============================================================================
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs').promises;
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const chalk = require('chalk');
|
|
27
|
+
const { getGentPath, readJSON, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
28
|
+
const { STAGING_FILE } = require('../utils/constants');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Remove files
|
|
32
|
+
* @param {Array} files
|
|
33
|
+
* @param {Object} options
|
|
34
|
+
*/
|
|
35
|
+
async function rm(files, options) {
|
|
36
|
+
try {
|
|
37
|
+
const gentPath = await getGentPath();
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const stagingPath = path.join(gentPath, STAGING_FILE);
|
|
40
|
+
const staging = await readJSON(stagingPath);
|
|
41
|
+
|
|
42
|
+
const entries = staging.entries || [];
|
|
43
|
+
const entryMap = new Map(entries.map(e => [e.path, e]));
|
|
44
|
+
let removed = 0;
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const relPath = path.relative(cwd, path.resolve(cwd, file));
|
|
48
|
+
|
|
49
|
+
// Stage deletion
|
|
50
|
+
entryMap.set(relPath, {
|
|
51
|
+
path: relPath,
|
|
52
|
+
hash: null,
|
|
53
|
+
status: 'deleted',
|
|
54
|
+
binary: false,
|
|
55
|
+
stats: { insertions: 0, deletions: 0 }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Delete from disk unless --cached
|
|
59
|
+
if (!options.cached) {
|
|
60
|
+
const fullPath = path.join(cwd, relPath);
|
|
61
|
+
if (await pathExists(fullPath)) {
|
|
62
|
+
await fs.unlink(fullPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
removed++;
|
|
67
|
+
console.log(chalk.red(` rm ${relPath}`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
staging.entries = Array.from(entryMap.values());
|
|
71
|
+
staging.files = staging.entries.filter(e => e.status !== 'deleted').map(e => e.path);
|
|
72
|
+
await writeJSON(stagingPath, staging);
|
|
73
|
+
|
|
74
|
+
console.log(chalk.green(`\nRemoved ${removed} file(s)`));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
77
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
78
|
+
} else {
|
|
79
|
+
console.error(chalk.red('Error:'), error.message);
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = rm;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Show Command - Show details of a commit, tag, or tree
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Display detailed information about a specific commit including author,
|
|
8
|
+
* message, tree hash, parent, and full diff. Like `git show`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent show → Show HEAD commit details with diff
|
|
12
|
+
* gent show <hash> → Show specific commit
|
|
13
|
+
* gent show <tag> → Show commit referenced by tag
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* Retrieves commit from commits.json, reads blob content from object store,
|
|
17
|
+
* computes diff against parent commit's tree, and displays unified diff.
|
|
18
|
+
*
|
|
19
|
+
* BACKEND EXPECTATIONS:
|
|
20
|
+
* GET /api/repos/:id/commits/:hash/ — returns commit object with tree
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const chalk = require('chalk');
|
|
27
|
+
const { getGentPath, readJSON } = require('../utils/fileSystem');
|
|
28
|
+
const { COMMITS_FILE } = require('../utils/constants');
|
|
29
|
+
const { readBlobAsString } = require('../utils/hash-engine');
|
|
30
|
+
const { formatUnifiedDiff } = require('../utils/diff-engine');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Show commit details
|
|
34
|
+
* @param {String} ref - Commit hash, tag name, or empty for HEAD
|
|
35
|
+
* @param {Object} options
|
|
36
|
+
*/
|
|
37
|
+
async function show(ref, options) {
|
|
38
|
+
try {
|
|
39
|
+
const gentPath = await getGentPath();
|
|
40
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
41
|
+
const commits = repository.commits || [];
|
|
42
|
+
const tags = repository.tags || {};
|
|
43
|
+
|
|
44
|
+
let targetHash = ref;
|
|
45
|
+
|
|
46
|
+
// Default to HEAD
|
|
47
|
+
if (!targetHash) {
|
|
48
|
+
targetHash = repository.branches[repository.currentBranch];
|
|
49
|
+
if (!targetHash) {
|
|
50
|
+
console.log(chalk.yellow('No commits yet'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Resolve tag name → hash
|
|
56
|
+
if (tags[targetHash]) {
|
|
57
|
+
targetHash = tags[targetHash].hash;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find commit (support short hashes)
|
|
61
|
+
const commit = commits.find(c =>
|
|
62
|
+
c.hash === targetHash || c.hash.startsWith(targetHash)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!commit) {
|
|
66
|
+
console.error(chalk.red(`Commit '${ref || 'HEAD'}' not found`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Display commit header
|
|
71
|
+
console.log(chalk.yellow(`commit ${commit.hash}`));
|
|
72
|
+
if (commit.mergeParent) {
|
|
73
|
+
console.log(chalk.gray(`Merge: ${commit.parent?.substring(0, 7)} ${commit.mergeParent.substring(0, 7)}`));
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk.white(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
76
|
+
console.log(chalk.white(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
77
|
+
if (commit.treeHash) {
|
|
78
|
+
console.log(chalk.gray(`Tree: ${commit.treeHash.substring(0, 7)}`));
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(chalk.white(` ${commit.message}`));
|
|
82
|
+
console.log('');
|
|
83
|
+
|
|
84
|
+
if (commit.stats) {
|
|
85
|
+
console.log(chalk.gray(` ${commit.stats.filesChanged} file(s), `) +
|
|
86
|
+
chalk.green(`+${commit.stats.insertions}`) + ' ' +
|
|
87
|
+
chalk.red(`-${commit.stats.deletions}`));
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Show diff against parent
|
|
92
|
+
if (!options.noPatch) {
|
|
93
|
+
await showCommitDiff(gentPath, commits, commit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
98
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
99
|
+
} else {
|
|
100
|
+
console.error(chalk.red('Error:'), error.message);
|
|
101
|
+
}
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Display diff between commit and its parent
|
|
108
|
+
*/
|
|
109
|
+
async function showCommitDiff(gentPath, commits, commit) {
|
|
110
|
+
const parentCommit = commit.parent
|
|
111
|
+
? commits.find(c => c.hash === commit.parent)
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
const currentTree = commit.tree || (commit.files || []).map(f => ({
|
|
115
|
+
name: f.path || f.name, hash: f.hash
|
|
116
|
+
}));
|
|
117
|
+
const parentTree = parentCommit
|
|
118
|
+
? (parentCommit.tree || (parentCommit.files || []).map(f => ({
|
|
119
|
+
name: f.path || f.name, hash: f.hash
|
|
120
|
+
})))
|
|
121
|
+
: [];
|
|
122
|
+
|
|
123
|
+
const parentMap = new Map(parentTree.map(f => [f.name || f.path, f.hash]));
|
|
124
|
+
const currentMap = new Map(currentTree.map(f => [f.name || f.path, f.hash]));
|
|
125
|
+
|
|
126
|
+
// Files changed
|
|
127
|
+
const allPaths = new Set([...parentMap.keys(), ...currentMap.keys()]);
|
|
128
|
+
|
|
129
|
+
for (const filePath of allPaths) {
|
|
130
|
+
const oldHash = parentMap.get(filePath);
|
|
131
|
+
const newHash = currentMap.get(filePath);
|
|
132
|
+
|
|
133
|
+
if (oldHash === newHash) continue;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const oldContent = oldHash ? await readBlobAsString(gentPath, oldHash) : '';
|
|
137
|
+
const newContent = newHash ? await readBlobAsString(gentPath, newHash) : '';
|
|
138
|
+
|
|
139
|
+
const unified = formatUnifiedDiff(filePath, oldContent, newContent);
|
|
140
|
+
if (unified) {
|
|
141
|
+
printColorizedDiff(unified);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Blob missing for legacy commits
|
|
145
|
+
if (!oldHash && newHash) {
|
|
146
|
+
console.log(chalk.green(`+ new file: ${filePath}`));
|
|
147
|
+
} else if (oldHash && !newHash) {
|
|
148
|
+
console.log(chalk.red(`- deleted: ${filePath}`));
|
|
149
|
+
} else {
|
|
150
|
+
console.log(chalk.yellow(`~ modified: ${filePath} (blob not available)`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function printColorizedDiff(unifiedDiff) {
|
|
157
|
+
for (const line of unifiedDiff.split('\n')) {
|
|
158
|
+
if (line.startsWith('---') || line.startsWith('+++')) console.log(chalk.bold(line));
|
|
159
|
+
else if (line.startsWith('@@')) console.log(chalk.cyan(line));
|
|
160
|
+
else if (line.startsWith('+')) console.log(chalk.green(line));
|
|
161
|
+
else if (line.startsWith('-')) console.log(chalk.red(line));
|
|
162
|
+
else console.log(line);
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = show;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Stash Command - Temporarily shelve working tree changes
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Save uncommitted changes on a stack so you can switch branches cleanly,
|
|
8
|
+
* then restore them later. Like `git stash`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent stash → Stash all modified tracked files
|
|
12
|
+
* gent stash pop → Restore most recent stash and remove it
|
|
13
|
+
* gent stash list → List all stashed entries
|
|
14
|
+
* gent stash drop [index] → Drop a specific stash entry
|
|
15
|
+
* gent stash apply [index] → Apply stash without removing it
|
|
16
|
+
*
|
|
17
|
+
* ALGORITHM:
|
|
18
|
+
* Saved as JSON stack in .gent/stash.json. Each entry stores:
|
|
19
|
+
* - Snapshot of staged entries (staging.entries)
|
|
20
|
+
* - Blob hashes of modified working tree files
|
|
21
|
+
* After stashing, staging is cleared and files are restored to HEAD state.
|
|
22
|
+
*
|
|
23
|
+
* BACKEND EXPECTATIONS:
|
|
24
|
+
* Stash is local only. Backend does not need stash support.
|
|
25
|
+
*
|
|
26
|
+
* ============================================================================
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs').promises;
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const chalk = require('chalk');
|
|
32
|
+
const ora = require('ora');
|
|
33
|
+
const { getGentPath, readJSON, writeJSON, pathExists, getAllFiles, getIgnorePatterns } = require('../utils/fileSystem');
|
|
34
|
+
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
35
|
+
const { storeBlob, readBlobAsString, hashBlob } = require('../utils/hash-engine');
|
|
36
|
+
|
|
37
|
+
const STASH_FILE = 'stash.json';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stash management
|
|
41
|
+
* @param {String} subcommand - pop|list|drop|apply (null = stash push)
|
|
42
|
+
* @param {Object} options
|
|
43
|
+
*/
|
|
44
|
+
async function stash(subcommand, options) {
|
|
45
|
+
try {
|
|
46
|
+
const gentPath = await getGentPath();
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
|
|
49
|
+
switch (subcommand) {
|
|
50
|
+
case 'pop':
|
|
51
|
+
await stashPop(gentPath, cwd, options);
|
|
52
|
+
break;
|
|
53
|
+
case 'list':
|
|
54
|
+
await stashList(gentPath);
|
|
55
|
+
break;
|
|
56
|
+
case 'drop':
|
|
57
|
+
await stashDrop(gentPath, options);
|
|
58
|
+
break;
|
|
59
|
+
case 'apply':
|
|
60
|
+
await stashApply(gentPath, cwd, options, false);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
await stashPush(gentPath, cwd, options);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
68
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
69
|
+
} else {
|
|
70
|
+
console.error(chalk.red('Error:'), error.message);
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Push current changes onto stash stack
|
|
78
|
+
*/
|
|
79
|
+
async function stashPush(gentPath, cwd, options) {
|
|
80
|
+
const spinner = ora('Stashing changes...').start();
|
|
81
|
+
|
|
82
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
83
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
84
|
+
const headHash = repository.branches[repository.currentBranch] || null;
|
|
85
|
+
const headCommit = headHash ? (repository.commits || []).find(c => c.hash === headHash) : null;
|
|
86
|
+
|
|
87
|
+
// Get HEAD tree
|
|
88
|
+
const headTree = new Map();
|
|
89
|
+
if (headCommit) {
|
|
90
|
+
const tree = headCommit.tree || headCommit.files || [];
|
|
91
|
+
for (const f of tree) headTree.set(f.path || f.name, f.hash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Collect modified working tree files
|
|
95
|
+
const ignorePatterns = await getIgnorePatterns(cwd);
|
|
96
|
+
const allFiles = await getAllFiles(cwd, ignorePatterns);
|
|
97
|
+
const workingChanges = [];
|
|
98
|
+
|
|
99
|
+
for (const absPath of allFiles) {
|
|
100
|
+
const relPath = path.relative(cwd, absPath);
|
|
101
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
102
|
+
const currentHash = hashBlob(content);
|
|
103
|
+
const headBlobHash = headTree.get(relPath);
|
|
104
|
+
|
|
105
|
+
if (headBlobHash && currentHash !== headBlobHash) {
|
|
106
|
+
const blobHash = await storeBlob(gentPath, content);
|
|
107
|
+
workingChanges.push({ path: relPath, hash: blobHash });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const stagedEntries = staging.entries || [];
|
|
112
|
+
|
|
113
|
+
if (workingChanges.length === 0 && stagedEntries.length === 0) {
|
|
114
|
+
spinner.info(chalk.yellow('No local changes to stash'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Save stash entry
|
|
119
|
+
const stashes = await readStashes(gentPath);
|
|
120
|
+
stashes.unshift({
|
|
121
|
+
message: options.message || `WIP on ${repository.currentBranch}`,
|
|
122
|
+
branch: repository.currentBranch,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
stagedEntries: stagedEntries,
|
|
125
|
+
workingChanges: workingChanges
|
|
126
|
+
});
|
|
127
|
+
await writeStashes(gentPath, stashes);
|
|
128
|
+
|
|
129
|
+
// Clear staging
|
|
130
|
+
staging.entries = [];
|
|
131
|
+
staging.files = [];
|
|
132
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
133
|
+
|
|
134
|
+
// Restore working tree to HEAD state
|
|
135
|
+
for (const change of workingChanges) {
|
|
136
|
+
const headBlobHash = headTree.get(change.path);
|
|
137
|
+
if (headBlobHash) {
|
|
138
|
+
try {
|
|
139
|
+
const content = await readBlobAsString(gentPath, headBlobHash);
|
|
140
|
+
await fs.writeFile(path.join(cwd, change.path), content, 'utf-8');
|
|
141
|
+
} catch { /* best effort */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
spinner.succeed(chalk.green(`Stashed ${stagedEntries.length} staged + ${workingChanges.length} working tree changes`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pop: apply and remove most recent stash
|
|
150
|
+
*/
|
|
151
|
+
async function stashPop(gentPath, cwd, options) {
|
|
152
|
+
await stashApply(gentPath, cwd, options, true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Apply stash entry to working tree
|
|
157
|
+
*/
|
|
158
|
+
async function stashApply(gentPath, cwd, options, removAfter) {
|
|
159
|
+
const index = options.index ? parseInt(options.index) : 0;
|
|
160
|
+
const stashes = await readStashes(gentPath);
|
|
161
|
+
|
|
162
|
+
if (stashes.length === 0) {
|
|
163
|
+
console.log(chalk.yellow('No stash entries'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (index >= stashes.length) {
|
|
168
|
+
console.error(chalk.red(`stash@{${index}} does not exist`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const entry = stashes[index];
|
|
173
|
+
|
|
174
|
+
// Restore working tree changes
|
|
175
|
+
for (const change of (entry.workingChanges || [])) {
|
|
176
|
+
try {
|
|
177
|
+
const content = await readBlobAsString(gentPath, change.hash);
|
|
178
|
+
const fullPath = path.join(cwd, change.path);
|
|
179
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
180
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
181
|
+
} catch { /* blob missing */ }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Restore staged entries
|
|
185
|
+
if (entry.stagedEntries && entry.stagedEntries.length > 0) {
|
|
186
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
187
|
+
staging.entries = entry.stagedEntries;
|
|
188
|
+
staging.files = entry.stagedEntries.map(e => e.path);
|
|
189
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (removAfter) {
|
|
193
|
+
stashes.splice(index, 1);
|
|
194
|
+
await writeStashes(gentPath, stashes);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(chalk.green(`Applied stash@{${index}}: ${entry.message}`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* List stash entries
|
|
202
|
+
*/
|
|
203
|
+
async function stashList(gentPath) {
|
|
204
|
+
const stashes = await readStashes(gentPath);
|
|
205
|
+
|
|
206
|
+
if (stashes.length === 0) {
|
|
207
|
+
console.log(chalk.gray('No stash entries'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
stashes.forEach((entry, i) => {
|
|
212
|
+
const ts = new Date(entry.timestamp).toLocaleString();
|
|
213
|
+
console.log(
|
|
214
|
+
chalk.yellow(`stash@{${i}}: `) +
|
|
215
|
+
chalk.white(entry.message) +
|
|
216
|
+
chalk.gray(` (${entry.branch}, ${ts})`)
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Drop stash entry
|
|
223
|
+
*/
|
|
224
|
+
async function stashDrop(gentPath, options) {
|
|
225
|
+
const index = options.index ? parseInt(options.index) : 0;
|
|
226
|
+
const stashes = await readStashes(gentPath);
|
|
227
|
+
|
|
228
|
+
if (index >= stashes.length) {
|
|
229
|
+
console.error(chalk.red(`stash@{${index}} does not exist`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const removed = stashes.splice(index, 1);
|
|
234
|
+
await writeStashes(gentPath, stashes);
|
|
235
|
+
console.log(chalk.green(`Dropped stash@{${index}}: ${removed[0].message}`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read stash stack from disk
|
|
240
|
+
*/
|
|
241
|
+
async function readStashes(gentPath) {
|
|
242
|
+
const stashPath = path.join(gentPath, STASH_FILE);
|
|
243
|
+
if (!await pathExists(stashPath)) return [];
|
|
244
|
+
const data = await readJSON(stashPath);
|
|
245
|
+
return data.stashes || [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Write stash stack to disk
|
|
250
|
+
*/
|
|
251
|
+
async function writeStashes(gentPath, stashes) {
|
|
252
|
+
await writeJSON(path.join(gentPath, STASH_FILE), { stashes });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = stash;
|