gent-cli 2.1.0 → 5.0.1
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 +215 -65
- 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 -291
- package/src/utils/cloud-sync.js +0 -323
- package/src/utils/diff.js +0 -121
package/src/commands/pull.js
CHANGED
|
@@ -1,123 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Pull Command - Fetch and merge remote commits into local branch
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Download new commits from remote and merge into current branch.
|
|
8
|
+
* Like `git pull` (fetch + merge in one step).
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent pull → Pull from origin/current-branch
|
|
12
|
+
* gent pull <remote> <branch> → Pull specific remote/branch
|
|
13
|
+
*
|
|
14
|
+
* ALGORITHM:
|
|
15
|
+
* 1. GET /api/repos/:id/pull/?branch=<branch>&since=<lastKnownHash>
|
|
16
|
+
* 2. Receive commits + blob objects
|
|
17
|
+
* 3. Store blobs in local object store
|
|
18
|
+
* 4. Append commits to local history
|
|
19
|
+
* 5. If diverged: run 3-way merge (same as gent merge)
|
|
20
|
+
* 6. If fast-forward: just advance pointer
|
|
21
|
+
*
|
|
22
|
+
* BACKEND EXPECTATIONS:
|
|
23
|
+
* GET /api/repos/:id/pull/?branch=main&since=abc1234
|
|
24
|
+
* Returns:
|
|
25
|
+
* {
|
|
26
|
+
* branch: "main",
|
|
27
|
+
* commits: [...],
|
|
28
|
+
* objects: [ { hash, type, data: "<base64>" } ],
|
|
29
|
+
* head: "<remoteHeadHash>"
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* ============================================================================
|
|
4
33
|
*/
|
|
5
34
|
|
|
6
|
-
const
|
|
35
|
+
const fs = require('fs').promises;
|
|
7
36
|
const path = require('path');
|
|
37
|
+
const chalk = require('chalk');
|
|
8
38
|
const ora = require('ora');
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
const
|
|
39
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
40
|
+
const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
41
|
+
const apiClient = require('../utils/api-client');
|
|
12
42
|
const authStorage = require('../utils/auth-storage');
|
|
13
|
-
const
|
|
43
|
+
const { storeBlob, objectExists } = require('../utils/hash-engine');
|
|
44
|
+
const { findMergeBase, mergeTreeEntries } = require('../utils/merge-engine');
|
|
45
|
+
const { generateCommitHash } = require('../utils/helpers');
|
|
14
46
|
|
|
15
47
|
/**
|
|
16
|
-
* Pull commits
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {Object} options
|
|
48
|
+
* Pull remote commits
|
|
49
|
+
* @param {String} remoteName
|
|
50
|
+
* @param {String} branchName
|
|
51
|
+
* @param {Object} options
|
|
20
52
|
*/
|
|
21
53
|
async function pull(remoteName, branchName, options) {
|
|
54
|
+
const spinner = ora('Pulling from remote...').start();
|
|
55
|
+
|
|
22
56
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.error(chalk.red('Error: Not a gent repository'));
|
|
29
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent init'), chalk.yellow('to initialize a repository'));
|
|
30
|
-
process.exit(1);
|
|
57
|
+
const isAuth = await authStorage.isAuthenticated();
|
|
58
|
+
if (!isAuth) {
|
|
59
|
+
spinner.fail(chalk.red('Not authenticated'));
|
|
60
|
+
console.log(chalk.yellow('Run "gent login" first'));
|
|
61
|
+
return;
|
|
31
62
|
}
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
const gentPath = await getGentPath();
|
|
65
|
+
const config = await readJSON(path.join(gentPath, CONFIG_FILE));
|
|
66
|
+
config.remotes = config.remotes || {};
|
|
67
|
+
|
|
68
|
+
const remote = remoteName || 'origin';
|
|
69
|
+
const remoteConfig = config.remotes[remote];
|
|
70
|
+
if (!remoteConfig) {
|
|
71
|
+
spinner.fail(chalk.red(`Remote '${remote}' not found`));
|
|
72
|
+
return;
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
|
|
75
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
76
|
+
const branch = branchName || repository.currentBranch;
|
|
77
|
+
const localHead = repository.branches[branch] || null;
|
|
43
78
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.error(chalk.red(`Error: Remote '${remoteName}' not found`));
|
|
48
|
-
console.log(chalk.yellow('Add a remote with:'), chalk.cyan('gent remote add origin <owner_id>/<repo_name>'));
|
|
49
|
-
console.log(chalk.yellow('Or list remotes with:'), chalk.cyan('gent remote'));
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
79
|
+
// Fetch from remote
|
|
80
|
+
config.remoteRefs = config.remoteRefs || {};
|
|
81
|
+
const since = config.remoteRefs[`${remote}/${branch}`] || localHead || '';
|
|
52
82
|
|
|
53
|
-
|
|
54
|
-
const commitsData = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
55
|
-
branchName = branchName || commitsData.currentBranch;
|
|
83
|
+
spinner.text = `Fetching from ${remote}/${branch}...`;
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
const response = await apiClient.get(
|
|
86
|
+
`${remoteConfig.url}/pull/`,
|
|
87
|
+
{ params: { branch, since } }
|
|
88
|
+
);
|
|
61
89
|
|
|
62
|
-
|
|
90
|
+
const remoteCommits = response.commits || [];
|
|
91
|
+
const remoteObjects = response.objects || [];
|
|
92
|
+
const remoteHead = response.head;
|
|
63
93
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await repoService.getRepository(remote.owner_id, remote.repo_name);
|
|
68
|
-
spinner.succeed('Remote repository verified');
|
|
69
|
-
} catch (error) {
|
|
70
|
-
spinner.fail('Remote repository not found or access denied');
|
|
71
|
-
throw error;
|
|
94
|
+
if (remoteCommits.length === 0) {
|
|
95
|
+
spinner.succeed(chalk.green('Already up-to-date'));
|
|
96
|
+
return;
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Add cloud commits that don't exist locally
|
|
82
|
-
for (const cloudCommit of cloudCommits) {
|
|
83
|
-
const exists = localCommits.find(c => c.sha === cloudCommit.sha);
|
|
84
|
-
if (!exists) {
|
|
85
|
-
// Convert cloud commit format to local format
|
|
86
|
-
mergedCommits.push({
|
|
87
|
-
sha: cloudCommit.sha,
|
|
88
|
-
message: cloudCommit.message,
|
|
89
|
-
author: {
|
|
90
|
-
name: cloudCommit.author_name,
|
|
91
|
-
email: cloudCommit.author_email
|
|
92
|
-
},
|
|
93
|
-
timestamp: cloudCommit.committed_at,
|
|
94
|
-
parent: cloudCommit.parent_shas || [],
|
|
95
|
-
files: cloudCommit.files || []
|
|
96
|
-
});
|
|
99
|
+
// Store received blob objects
|
|
100
|
+
spinner.text = `Storing ${remoteObjects.length} object(s)...`;
|
|
101
|
+
for (const obj of remoteObjects) {
|
|
102
|
+
if (obj.type === 'blob' && obj.data) {
|
|
103
|
+
const buf = Buffer.from(obj.data, 'base64');
|
|
104
|
+
await storeBlob(gentPath, buf);
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
// Check if fast-forward is possible
|
|
109
|
+
const allCommits = [...(repository.commits || []), ...remoteCommits];
|
|
110
|
+
const commitSet = new Set((repository.commits || []).map(c => c.hash));
|
|
111
|
+
|
|
112
|
+
// Add new commits (dedup)
|
|
113
|
+
let newCount = 0;
|
|
114
|
+
for (const commit of remoteCommits) {
|
|
115
|
+
if (!commitSet.has(commit.hash)) {
|
|
116
|
+
repository.commits.push(commit);
|
|
117
|
+
commitSet.add(commit.hash);
|
|
118
|
+
newCount++;
|
|
119
|
+
}
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
|
|
122
|
+
if (!localHead || isAncestor(repository.commits, localHead, remoteHead)) {
|
|
123
|
+
// Fast-forward
|
|
124
|
+
repository.branches[branch] = remoteHead;
|
|
125
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
107
126
|
|
|
108
|
-
|
|
127
|
+
config.remoteRefs[`${remote}/${branch}`] = remoteHead;
|
|
128
|
+
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
109
129
|
|
|
110
|
-
|
|
111
|
-
console.log(chalk.
|
|
130
|
+
spinner.succeed(chalk.green(`Fast-forward: ${newCount} new commit(s)`));
|
|
131
|
+
console.log(chalk.gray(` ${remote}/${branch} → ${remoteHead.substring(0, 7)}`));
|
|
112
132
|
} else {
|
|
113
|
-
|
|
133
|
+
// Diverged — need 3-way merge
|
|
134
|
+
spinner.text = 'Branches diverged, merging...';
|
|
135
|
+
|
|
136
|
+
const baseHash = findMergeBase(repository.commits, localHead, remoteHead);
|
|
137
|
+
const getTree = (hash) => {
|
|
138
|
+
const c = repository.commits.find(x => x.hash === hash);
|
|
139
|
+
if (!c) return [];
|
|
140
|
+
return c.tree || (c.files || []).map(f => ({
|
|
141
|
+
mode: '100644', name: f.path || f.name, hash: f.hash, type: 'blob'
|
|
142
|
+
}));
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const baseTree = baseHash ? getTree(baseHash) : [];
|
|
146
|
+
const oursTree = getTree(localHead);
|
|
147
|
+
const theirsTree = getTree(remoteHead);
|
|
148
|
+
|
|
149
|
+
const mergeResult = await mergeTreeEntries(gentPath, baseTree, oursTree, theirsTree);
|
|
150
|
+
|
|
151
|
+
// Build merge commit
|
|
152
|
+
const { storeTree } = require('../utils/hash-engine');
|
|
153
|
+
const mergedTreeHash = await storeTree(gentPath, mergeResult.mergedEntries);
|
|
154
|
+
|
|
155
|
+
const mergeCommit = {
|
|
156
|
+
hash: generateCommitHash(),
|
|
157
|
+
message: `Merge remote-tracking branch '${remote}/${branch}'`,
|
|
158
|
+
author: (repository.commits.find(c => c.hash === localHead) || {}).author || { name: 'Unknown', email: '' },
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
parent: localHead,
|
|
161
|
+
mergeParent: remoteHead,
|
|
162
|
+
treeHash: mergedTreeHash,
|
|
163
|
+
tree: mergeResult.mergedEntries,
|
|
164
|
+
files: mergeResult.mergedEntries.map(e => ({ path: e.name, hash: e.hash })),
|
|
165
|
+
stats: { filesChanged: mergeResult.mergedEntries.length, insertions: 0, deletions: 0 }
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
repository.commits.push(mergeCommit);
|
|
169
|
+
repository.branches[branch] = mergeCommit.hash;
|
|
170
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
171
|
+
|
|
172
|
+
config.remoteRefs[`${remote}/${branch}`] = remoteHead;
|
|
173
|
+
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
174
|
+
|
|
175
|
+
if (mergeResult.hasConflicts) {
|
|
176
|
+
spinner.warn(chalk.yellow(`Pulled with ${mergeResult.conflicts.length} conflict(s)`));
|
|
177
|
+
for (const c of mergeResult.conflicts) {
|
|
178
|
+
console.log(chalk.red(` CONFLICT: ${c.file} (${c.type})`));
|
|
179
|
+
}
|
|
180
|
+
console.log(chalk.yellow('\nResolve conflicts, then "gent add" + "gent commit"'));
|
|
181
|
+
} else {
|
|
182
|
+
spinner.succeed(chalk.green(`Merged ${newCount} remote commit(s)`));
|
|
183
|
+
console.log(chalk.gray(` Merge commit: ${mergeCommit.hash.substring(0, 7)}`));
|
|
184
|
+
}
|
|
114
185
|
}
|
|
115
|
-
|
|
116
186
|
} catch (error) {
|
|
117
|
-
|
|
118
|
-
|
|
187
|
+
spinner.fail(chalk.red('Pull failed'));
|
|
188
|
+
if (error.response?.status === 401) {
|
|
189
|
+
console.error(chalk.red('Authentication failed — run "gent login"'));
|
|
190
|
+
} else if (error.response?.data?.message) {
|
|
191
|
+
console.error(chalk.red(error.response.data.message));
|
|
192
|
+
} else {
|
|
193
|
+
console.error(chalk.red('Error:'), error.message);
|
|
194
|
+
}
|
|
119
195
|
process.exit(1);
|
|
120
196
|
}
|
|
121
197
|
}
|
|
122
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Check if hashA is ancestor of hashB
|
|
201
|
+
*/
|
|
202
|
+
function isAncestor(commits, hashA, hashB) {
|
|
203
|
+
const commitMap = new Map(commits.map(c => [c.hash, c]));
|
|
204
|
+
let cur = hashB;
|
|
205
|
+
while (cur) {
|
|
206
|
+
if (cur === hashA) return true;
|
|
207
|
+
const c = commitMap.get(cur);
|
|
208
|
+
cur = c ? c.parent : null;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
123
213
|
module.exports = pull;
|
package/src/commands/push.js
CHANGED
|
@@ -1,113 +1,206 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Push Command - Upload local commits and objects to remote
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Send local commits, blobs, and tree objects to the backend API server.
|
|
8
|
+
* Like `git push`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent push → Push current branch to origin
|
|
12
|
+
* gent push <remote> <branch> → Push specific branch to remote
|
|
13
|
+
* gent push --force → Force push (overwrite remote)
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* 1. Read local commits since last known remote HEAD
|
|
17
|
+
* 2. Collect all blob objects referenced by those commits
|
|
18
|
+
* 3. POST packfile (commits + blobs + trees) to remote /push/ endpoint
|
|
19
|
+
* 4. Remote updates branch pointer
|
|
20
|
+
*
|
|
21
|
+
* DATA FORMAT SENT TO BACKEND:
|
|
22
|
+
* POST /api/repos/:id/push/
|
|
23
|
+
* {
|
|
24
|
+
* branch: "main",
|
|
25
|
+
* force: false,
|
|
26
|
+
* commits: [ { hash, message, author, timestamp, parent, treeHash, tree, files, stats } ],
|
|
27
|
+
* objects: [ { hash, type: "blob", data: "<base64>" } ],
|
|
28
|
+
* tags: { "v1.0": { hash, message, ... } }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* BACKEND EXPECTATIONS:
|
|
32
|
+
* - Validate auth (JWT Bearer token)
|
|
33
|
+
* - Verify fast-forward (reject non-ff unless force=true)
|
|
34
|
+
* - Store blob objects in backend object store
|
|
35
|
+
* - Append commits to branch history
|
|
36
|
+
* - Update branch refs
|
|
37
|
+
* - Return { success, ref, hash }
|
|
38
|
+
*
|
|
39
|
+
* ============================================================================
|
|
4
40
|
*/
|
|
5
41
|
|
|
6
|
-
const
|
|
42
|
+
const fs = require('fs').promises;
|
|
7
43
|
const path = require('path');
|
|
44
|
+
const chalk = require('chalk');
|
|
8
45
|
const ora = require('ora');
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
const
|
|
46
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
47
|
+
const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
48
|
+
const apiClient = require('../utils/api-client');
|
|
12
49
|
const authStorage = require('../utils/auth-storage');
|
|
13
|
-
const
|
|
50
|
+
const { readBlob, objectExists } = require('../utils/hash-engine');
|
|
14
51
|
|
|
15
52
|
/**
|
|
16
|
-
* Push
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {Object} options
|
|
53
|
+
* Push commits to remote
|
|
54
|
+
* @param {String} remoteName
|
|
55
|
+
* @param {String} branchName
|
|
56
|
+
* @param {Object} options
|
|
20
57
|
*/
|
|
21
58
|
async function push(remoteName, branchName, options) {
|
|
22
|
-
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
const gentPath = path.join(cwd, GENT_DIR);
|
|
25
|
-
|
|
26
|
-
// Check if in a gent repository
|
|
27
|
-
if (!(await pathExists(gentPath))) {
|
|
28
|
-
console.error(chalk.red('Error: Not a gent repository'));
|
|
29
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent init'), chalk.yellow('to initialize a repository'));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check authentication
|
|
34
|
-
const user = await authStorage.getUser();
|
|
35
|
-
if (!user) {
|
|
36
|
-
console.error(chalk.red('Error: You must be logged in to push'));
|
|
37
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
59
|
+
const spinner = ora('Preparing push...').start();
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(chalk.yellow('Add a remote with:'), chalk.cyan('gent remote add origin <owner_id>/<repo_name>'));
|
|
49
|
-
console.log(chalk.yellow('Or list remotes with:'), chalk.cyan('gent remote'));
|
|
50
|
-
process.exit(1);
|
|
61
|
+
try {
|
|
62
|
+
// Auth check
|
|
63
|
+
const isAuth = await authStorage.isAuthenticated();
|
|
64
|
+
if (!isAuth) {
|
|
65
|
+
spinner.fail(chalk.red('Not authenticated'));
|
|
66
|
+
console.log(chalk.yellow('Run "gent login" first'));
|
|
67
|
+
return;
|
|
51
68
|
}
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
const gentPath = await getGentPath();
|
|
71
|
+
const configPath = path.join(gentPath, CONFIG_FILE);
|
|
72
|
+
const config = await readJSON(configPath);
|
|
73
|
+
config.remotes = config.remotes || {};
|
|
74
|
+
|
|
75
|
+
// Resolve remote
|
|
76
|
+
const remote = remoteName || 'origin';
|
|
77
|
+
const remoteConfig = config.remotes[remote];
|
|
78
|
+
if (!remoteConfig) {
|
|
79
|
+
spinner.fail(chalk.red(`Remote '${remote}' not found`));
|
|
80
|
+
console.log(chalk.yellow('Use "gent remote add origin <url>" to configure'));
|
|
81
|
+
return;
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
84
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
85
|
+
const branch = branchName || repository.currentBranch;
|
|
86
|
+
const localHead = repository.branches[branch];
|
|
65
87
|
|
|
66
|
-
if (!
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
if (!localHead) {
|
|
89
|
+
spinner.fail(chalk.red(`Branch '${branch}' has no commits`));
|
|
90
|
+
return;
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const commit = commits.find(c => (c.sha || c.hash) === currentSha);
|
|
77
|
-
if (!commit) break;
|
|
78
|
-
|
|
79
|
-
commitsToPush.unshift(commit); // Add to beginning to maintain order
|
|
80
|
-
const parents = Array.isArray(commit.parent) ? commit.parent : (commit.parent ? [commit.parent] : []);
|
|
81
|
-
currentSha = parents.length > 0 ? parents[0] : null;
|
|
82
|
-
}
|
|
93
|
+
// Determine which commits to push (since last pushed ref)
|
|
94
|
+
config.remoteRefs = config.remoteRefs || {};
|
|
95
|
+
const lastPushed = config.remoteRefs[`${remote}/${branch}`] || null;
|
|
96
|
+
const commits = repository.commits || [];
|
|
97
|
+
const commitsToPush = getCommitsSince(commits, localHead, lastPushed);
|
|
83
98
|
|
|
84
99
|
if (commitsToPush.length === 0) {
|
|
85
|
-
|
|
100
|
+
spinner.succeed(chalk.green('Everything up-to-date'));
|
|
86
101
|
return;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
|
|
104
|
+
spinner.text = `Pushing ${commitsToPush.length} commit(s) to ${remote}/${branch}...`;
|
|
90
105
|
|
|
91
|
-
//
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
// Collect all blob hashes from commits to push
|
|
107
|
+
const blobHashes = new Set();
|
|
108
|
+
for (const commit of commitsToPush) {
|
|
109
|
+
const tree = commit.tree || commit.files || [];
|
|
110
|
+
for (const entry of tree) {
|
|
111
|
+
const h = entry.hash;
|
|
112
|
+
if (h) blobHashes.add(h);
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
//
|
|
102
|
-
|
|
116
|
+
// Read blob data for transfer
|
|
117
|
+
const objects = [];
|
|
118
|
+
for (const hash of blobHashes) {
|
|
119
|
+
try {
|
|
120
|
+
if (await objectExists(gentPath, hash)) {
|
|
121
|
+
const data = await readBlob(gentPath, hash);
|
|
122
|
+
objects.push({
|
|
123
|
+
hash,
|
|
124
|
+
type: 'blob',
|
|
125
|
+
data: data.toString('base64')
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Skip missing blobs
|
|
130
|
+
}
|
|
131
|
+
}
|
|
103
132
|
|
|
104
|
-
|
|
133
|
+
// Build push payload
|
|
134
|
+
const payload = {
|
|
135
|
+
branch,
|
|
136
|
+
force: !!options.force,
|
|
137
|
+
commits: commitsToPush.map(c => ({
|
|
138
|
+
hash: c.hash,
|
|
139
|
+
message: c.message,
|
|
140
|
+
author: c.author,
|
|
141
|
+
timestamp: c.timestamp,
|
|
142
|
+
parent: c.parent,
|
|
143
|
+
mergeParent: c.mergeParent || null,
|
|
144
|
+
treeHash: c.treeHash || null,
|
|
145
|
+
tree: c.tree || null,
|
|
146
|
+
files: c.files || [],
|
|
147
|
+
stats: c.stats || {}
|
|
148
|
+
})),
|
|
149
|
+
objects,
|
|
150
|
+
tags: repository.tags || {}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Send to backend
|
|
154
|
+
const response = await apiClient.post(
|
|
155
|
+
`${remoteConfig.url}/push/`,
|
|
156
|
+
payload
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Update remote ref
|
|
160
|
+
config.remoteRefs[`${remote}/${branch}`] = localHead;
|
|
161
|
+
await writeJSON(configPath, config);
|
|
162
|
+
|
|
163
|
+
spinner.succeed(chalk.green(`Pushed ${commitsToPush.length} commit(s) to ${remote}/${branch}`));
|
|
164
|
+
console.log(chalk.gray(` ${localHead.substring(0, 7)} → ${remote}/${branch}`));
|
|
165
|
+
console.log(chalk.gray(` ${objects.length} object(s) transferred`));
|
|
105
166
|
|
|
106
167
|
} catch (error) {
|
|
107
|
-
|
|
108
|
-
|
|
168
|
+
spinner.fail(chalk.red('Push failed'));
|
|
169
|
+
|
|
170
|
+
if (error.response?.status === 409) {
|
|
171
|
+
console.error(chalk.red('Remote has changes you don\'t have locally'));
|
|
172
|
+
console.log(chalk.yellow('Run "gent pull" first, or use "gent push --force"'));
|
|
173
|
+
} else if (error.response?.status === 401) {
|
|
174
|
+
console.error(chalk.red('Authentication failed — run "gent login"'));
|
|
175
|
+
} else if (error.response?.data?.message) {
|
|
176
|
+
console.error(chalk.red(error.response.data.message));
|
|
177
|
+
} else {
|
|
178
|
+
console.error(chalk.red('Error:'), error.message);
|
|
179
|
+
}
|
|
109
180
|
process.exit(1);
|
|
110
181
|
}
|
|
111
182
|
}
|
|
112
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Get commits from tip back to (but excluding) stopHash.
|
|
186
|
+
* @param {Array} allCommits
|
|
187
|
+
* @param {String} tipHash
|
|
188
|
+
* @param {String|null} stopHash
|
|
189
|
+
* @returns {Array}
|
|
190
|
+
*/
|
|
191
|
+
function getCommitsSince(allCommits, tipHash, stopHash) {
|
|
192
|
+
const commitMap = new Map(allCommits.map(c => [c.hash, c]));
|
|
193
|
+
const result = [];
|
|
194
|
+
let current = tipHash;
|
|
195
|
+
|
|
196
|
+
while (current && current !== stopHash) {
|
|
197
|
+
const commit = commitMap.get(current);
|
|
198
|
+
if (!commit) break;
|
|
199
|
+
result.push(commit);
|
|
200
|
+
current = commit.parent;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result.reverse(); // oldest first
|
|
204
|
+
}
|
|
205
|
+
|
|
113
206
|
module.exports = push;
|