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/index.js
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Gent CLI - A Git-like version control system with cloud backend
|
|
5
6
|
* Main entry point for the CLI application
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* ============================================================================
|
|
8
|
+
*
|
|
9
|
+
* COMMANDS:
|
|
10
|
+
* Repository: init, clone
|
|
11
|
+
* Staging: add, rm, reset, status, diff
|
|
12
|
+
* History: commit, log, show, tag
|
|
13
|
+
* Branching: branch, checkout, merge, stash
|
|
14
|
+
* Remote: remote, push, pull
|
|
15
|
+
* Auth: register, login, logout, whoami
|
|
16
|
+
*
|
|
17
|
+
* @author Abdalrahman Kanawati
|
|
18
|
+
* @version 2.0.0
|
|
9
19
|
*/
|
|
10
20
|
|
|
11
21
|
const { program } = require('commander');
|
|
12
22
|
const chalk = require('chalk');
|
|
13
23
|
const packageJson = require('../package.json');
|
|
14
24
|
|
|
15
|
-
// Import commands
|
|
25
|
+
// Import core commands
|
|
16
26
|
const initCommand = require('./commands/init');
|
|
27
|
+
const cloneCommand = require('./commands/clone');
|
|
17
28
|
const statusCommand = require('./commands/status');
|
|
18
29
|
const addCommand = require('./commands/add');
|
|
30
|
+
const rmCommand = require('./commands/rm');
|
|
31
|
+
const resetCommand = require('./commands/reset');
|
|
32
|
+
const diffCommand = require('./commands/diff');
|
|
19
33
|
const commitCommand = require('./commands/commit');
|
|
20
34
|
const logCommand = require('./commands/log');
|
|
35
|
+
const showCommand = require('./commands/show');
|
|
36
|
+
const tagCommand = require('./commands/tag');
|
|
21
37
|
const branchCommand = require('./commands/branch');
|
|
22
38
|
const checkoutCommand = require('./commands/checkout');
|
|
39
|
+
const mergeCommand = require('./commands/merge');
|
|
40
|
+
const stashCommand = require('./commands/stash');
|
|
41
|
+
const remoteCommand = require('./commands/remote');
|
|
42
|
+
const pushCommand = require('./commands/push');
|
|
43
|
+
const pullCommand = require('./commands/pull');
|
|
23
44
|
|
|
24
45
|
// Import auth commands
|
|
25
46
|
const registerCommand = require('./commands/register');
|
|
@@ -27,30 +48,27 @@ const loginCommand = require('./commands/login');
|
|
|
27
48
|
const logoutCommand = require('./commands/logout');
|
|
28
49
|
const whoamiCommand = require('./commands/whoami');
|
|
29
50
|
|
|
30
|
-
// Import cloud repository commands
|
|
31
|
-
const createCommand = require('./commands/create');
|
|
32
|
-
const listCommand = require('./commands/list');
|
|
33
|
-
const cloneCommand = require('./commands/clone');
|
|
34
|
-
const pushCommand = require('./commands/push');
|
|
35
|
-
const pullCommand = require('./commands/pull');
|
|
36
|
-
const remoteCommand = require('./commands/remote');
|
|
37
|
-
|
|
38
51
|
// Configure CLI
|
|
39
52
|
program
|
|
40
53
|
.name('gent')
|
|
41
|
-
.description(chalk.cyan('
|
|
54
|
+
.description(chalk.cyan('Gent - A Git-like version control CLI with cloud backend'))
|
|
42
55
|
.version(packageJson.version, '-v, --version', 'Output the current version');
|
|
43
56
|
|
|
44
|
-
//
|
|
57
|
+
// ─── Repository Setup ───────────────────────────────────
|
|
58
|
+
|
|
45
59
|
program
|
|
46
60
|
.command('init')
|
|
47
61
|
.description('Initialize a new gent repository')
|
|
48
62
|
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
49
|
-
.option('--cloud', 'Create a corresponding cloud repository')
|
|
50
|
-
.option('-d, --description <description>', 'Repository description (for cloud)')
|
|
51
|
-
.option('-p, --private', 'Make cloud repository private')
|
|
52
63
|
.action(initCommand);
|
|
53
64
|
|
|
65
|
+
program
|
|
66
|
+
.command('clone <url> [directory]')
|
|
67
|
+
.description('Clone a remote repository')
|
|
68
|
+
.action(cloneCommand);
|
|
69
|
+
|
|
70
|
+
// ─── Staging & Working Tree ─────────────────────────────
|
|
71
|
+
|
|
54
72
|
program
|
|
55
73
|
.command('status')
|
|
56
74
|
.description('Show the working tree status')
|
|
@@ -63,12 +81,33 @@ program
|
|
|
63
81
|
.option('-A, --all', 'Add all files')
|
|
64
82
|
.action(addCommand);
|
|
65
83
|
|
|
84
|
+
program
|
|
85
|
+
.command('rm <files...>')
|
|
86
|
+
.description('Remove files from working tree and staging')
|
|
87
|
+
.option('--cached', 'Only remove from staging, keep file on disk')
|
|
88
|
+
.action(rmCommand);
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('reset [files...]')
|
|
92
|
+
.description('Unstage files or reset HEAD to a commit')
|
|
93
|
+
.option('--hard <hash>', 'Reset HEAD and working tree to commit')
|
|
94
|
+
.option('--soft <hash>', 'Reset HEAD but keep staging')
|
|
95
|
+
.action(resetCommand);
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command('diff [files...]')
|
|
99
|
+
.description('Show changes between working tree, staging, and commits')
|
|
100
|
+
.option('--staged', 'Show staged changes vs last commit')
|
|
101
|
+
.option('--stat', 'Show diffstat summary only')
|
|
102
|
+
.action(diffCommand);
|
|
103
|
+
|
|
104
|
+
// ─── History ────────────────────────────────────────────
|
|
105
|
+
|
|
66
106
|
program
|
|
67
107
|
.command('commit')
|
|
68
108
|
.description('Record changes to the repository')
|
|
69
109
|
.option('-m, --message <message>', 'Commit message')
|
|
70
110
|
.option('-a, --all', 'Automatically stage all modified files')
|
|
71
|
-
.option('--push', 'Push to remote after commit')
|
|
72
111
|
.action(commitCommand);
|
|
73
112
|
|
|
74
113
|
program
|
|
@@ -76,8 +115,24 @@ program
|
|
|
76
115
|
.description('Show commit logs')
|
|
77
116
|
.option('-n, --number <count>', 'Limit the number of commits to show', '10')
|
|
78
117
|
.option('--oneline', 'Show each commit on a single line')
|
|
118
|
+
.option('--stat', 'Show file change statistics')
|
|
79
119
|
.action(logCommand);
|
|
80
120
|
|
|
121
|
+
program
|
|
122
|
+
.command('show [ref]')
|
|
123
|
+
.description('Show commit details and diff')
|
|
124
|
+
.option('--no-patch', 'Suppress diff output')
|
|
125
|
+
.action(showCommand);
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.command('tag [name]')
|
|
129
|
+
.description('Create, list, or delete tags')
|
|
130
|
+
.option('-m, --message <message>', 'Create annotated tag with message')
|
|
131
|
+
.option('-d, --delete <name>', 'Delete a tag')
|
|
132
|
+
.action(tagCommand);
|
|
133
|
+
|
|
134
|
+
// ─── Branching & Merging ────────────────────────────────
|
|
135
|
+
|
|
81
136
|
program
|
|
82
137
|
.command('branch')
|
|
83
138
|
.description('List, create, or delete branches')
|
|
@@ -92,7 +147,40 @@ program
|
|
|
92
147
|
.option('-b, --create', 'Create a new branch')
|
|
93
148
|
.action(checkoutCommand);
|
|
94
149
|
|
|
95
|
-
|
|
150
|
+
program
|
|
151
|
+
.command('merge <branch>')
|
|
152
|
+
.description('Merge a branch into the current branch (3-way smart merge)')
|
|
153
|
+
.option('-m, --message <message>', 'Merge commit message')
|
|
154
|
+
.action(mergeCommand);
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command('stash [subcommand]')
|
|
158
|
+
.description('Stash working tree changes (pop|list|drop|apply)')
|
|
159
|
+
.option('-m, --message <message>', 'Stash message')
|
|
160
|
+
.option('-i, --index <index>', 'Stash index for pop/apply/drop')
|
|
161
|
+
.action(stashCommand);
|
|
162
|
+
|
|
163
|
+
// ─── Remote & Sync ──────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
program
|
|
166
|
+
.command('remote [subcommand] [args...]')
|
|
167
|
+
.description('Manage remote connections (add|remove|set-url)')
|
|
168
|
+
.option('-v, --verbose', 'Show remote URLs')
|
|
169
|
+
.action(remoteCommand);
|
|
170
|
+
|
|
171
|
+
program
|
|
172
|
+
.command('push [remote] [branch]')
|
|
173
|
+
.description('Push local commits to remote')
|
|
174
|
+
.option('-f, --force', 'Force push (overwrite remote)')
|
|
175
|
+
.action(pushCommand);
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command('pull [remote] [branch]')
|
|
179
|
+
.description('Pull and merge remote commits')
|
|
180
|
+
.action(pullCommand);
|
|
181
|
+
|
|
182
|
+
// ─── Authentication ─────────────────────────────────────
|
|
183
|
+
|
|
96
184
|
program
|
|
97
185
|
.command('register')
|
|
98
186
|
.description('Create a new user account')
|
|
@@ -115,43 +203,6 @@ program
|
|
|
115
203
|
.description('Display current user information')
|
|
116
204
|
.action(whoamiCommand);
|
|
117
205
|
|
|
118
|
-
// Cloud repository commands
|
|
119
|
-
program
|
|
120
|
-
.command('create <repo-name>')
|
|
121
|
-
.description('Create a new cloud repository')
|
|
122
|
-
.option('-d, --description <description>', 'Repository description')
|
|
123
|
-
.option('-p, --private', 'Make repository private')
|
|
124
|
-
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
125
|
-
.option('--init-local', 'Initialize local repository and link remote')
|
|
126
|
-
.action(createCommand);
|
|
127
|
-
|
|
128
|
-
program
|
|
129
|
-
.command('list')
|
|
130
|
-
.alias('ls')
|
|
131
|
-
.description('List all your cloud repositories')
|
|
132
|
-
.action(listCommand);
|
|
133
|
-
|
|
134
|
-
program
|
|
135
|
-
.command('clone <repo-url> [directory]')
|
|
136
|
-
.description('Clone a cloud repository (format: owner_id/repo_name)')
|
|
137
|
-
.action(cloneCommand);
|
|
138
|
-
|
|
139
|
-
program
|
|
140
|
-
.command('push [remote] [branch]')
|
|
141
|
-
.description('Push local commits to cloud')
|
|
142
|
-
.action(pushCommand);
|
|
143
|
-
|
|
144
|
-
program
|
|
145
|
-
.command('pull [remote] [branch]')
|
|
146
|
-
.description('Pull commits from cloud to local')
|
|
147
|
-
.action(pullCommand);
|
|
148
|
-
|
|
149
|
-
program
|
|
150
|
-
.command('remote [action] [name] [url]')
|
|
151
|
-
.description('Manage remote repositories')
|
|
152
|
-
.option('-v, --verbose', 'Show verbose output')
|
|
153
|
-
.action(remoteCommand);
|
|
154
|
-
|
|
155
206
|
// Help command
|
|
156
207
|
program
|
|
157
208
|
.command('help [command]')
|
package/src/utils/constants.js
CHANGED
|
@@ -11,42 +11,26 @@ module.exports = {
|
|
|
11
11
|
COMMITS_FILE: 'commits.json',
|
|
12
12
|
HEAD_FILE: 'HEAD',
|
|
13
13
|
AUTH_FILE: 'auth.json',
|
|
14
|
-
OBJECTS_DIR: 'objects',
|
|
15
|
-
REMOTE_CONFIG_FILE: 'remote.json',
|
|
16
14
|
|
|
17
15
|
// API Configuration
|
|
18
16
|
API_BASE_URL: 'https://gent-api.onrender.com',
|
|
19
17
|
API_ENDPOINTS: {
|
|
20
|
-
// Auth endpoints
|
|
21
18
|
LOGIN: '/api/auth/login/',
|
|
22
19
|
REGISTER: '/api/auth/register/',
|
|
23
20
|
LOGOUT: '/api/auth/logout/',
|
|
24
21
|
REFRESH: '/api/auth/token/refresh/',
|
|
25
22
|
PROFILE: '/api/auth/profile/',
|
|
26
23
|
|
|
27
|
-
// Repository endpoints
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Tree endpoints
|
|
38
|
-
TREE_GET: '/api/repos/{owner_id}/{name}/tree/{sha}/',
|
|
39
|
-
TREE_CREATE: '/api/repos/{owner_id}/{name}/tree/create/',
|
|
40
|
-
|
|
41
|
-
// Commit endpoints
|
|
42
|
-
COMMITS_LIST: '/api/repos/{owner_id}/{name}/commits/',
|
|
43
|
-
COMMITS_GET: '/api/repos/{owner_id}/{name}/commits/{sha}/',
|
|
44
|
-
COMMITS_CREATE: '/api/repos/{owner_id}/{name}/commits/create/',
|
|
45
|
-
|
|
46
|
-
// Branch endpoints
|
|
47
|
-
BRANCHES_LIST: '/api/repos/{owner_id}/{name}/branches/',
|
|
48
|
-
BRANCHES_GET: '/api/repos/{owner_id}/{name}/branches/{branch_name}/',
|
|
49
|
-
BRANCHES_CREATE: '/api/repos/{owner_id}/{name}/branches/create/'
|
|
24
|
+
// Repository endpoints (used by push/pull/clone)
|
|
25
|
+
// Base: /api/repos/:id/
|
|
26
|
+
REPOS: '/api/repos/',
|
|
27
|
+
REPO_PUSH: '/push/', // POST - upload commits + objects
|
|
28
|
+
REPO_PULL: '/pull/', // GET - download commits + objects since hash
|
|
29
|
+
REPO_CLONE: '/clone/', // GET - full repo download
|
|
30
|
+
REPO_REFS: '/refs/', // GET - list remote branch refs
|
|
31
|
+
REPO_TAGS: '/tags/', // GET/POST/DELETE - tag management
|
|
32
|
+
REPO_MERGE: '/merge/', // POST - server-side merge request
|
|
33
|
+
REPO_COMMITS: '/commits/', // GET - commit history
|
|
50
34
|
},
|
|
51
35
|
|
|
52
36
|
// Default ignore patterns
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Diff Engine - Line-level LCS diff with hunk generation and unified format
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Compute minimal edit scripts between two text files, classify each line
|
|
8
|
+
* as insert / delete / equal, generate unified diff output.
|
|
9
|
+
*
|
|
10
|
+
* ALGORITHM: Longest Common Subsequence (LCS)
|
|
11
|
+
* - Build M×N dynamic programming matrix where M,N = line counts
|
|
12
|
+
* - dp[i][j] = length of LCS of first i lines of A and first j lines of B
|
|
13
|
+
* - Recurrence:
|
|
14
|
+
* if A[i] == B[j]: dp[i][j] = dp[i-1][j-1] + 1
|
|
15
|
+
* else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
|
16
|
+
* - Backtrack from dp[M][N] to produce edit operations
|
|
17
|
+
* - Time: O(M*N), Space: O(M*N) — uses Uint32Array for memory efficiency
|
|
18
|
+
*
|
|
19
|
+
* INSERTION/DELETION CLASSIFICATION:
|
|
20
|
+
* During backtrack:
|
|
21
|
+
* - A[i]==B[j] → EQUAL (line unchanged)
|
|
22
|
+
* - Move up (i-1) → DELETE (line only in old version)
|
|
23
|
+
* - Move left (j-1) → INSERT (line only in new version)
|
|
24
|
+
*
|
|
25
|
+
* HUNK GENERATION:
|
|
26
|
+
* Groups adjacent changes with N context lines (default 3) into hunks.
|
|
27
|
+
* Changes within 2*N+1 lines of each other merge into one hunk.
|
|
28
|
+
* Output format matches unified diff:
|
|
29
|
+
* @@ -oldStart,oldCount +newStart,newCount @@
|
|
30
|
+
*
|
|
31
|
+
* BACKEND EXPECTATIONS:
|
|
32
|
+
* Diffs are computed locally. Backend does NOT need diff support.
|
|
33
|
+
* Backend stores blob objects; clients compute diffs on demand.
|
|
34
|
+
*
|
|
35
|
+
* ============================================================================
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const { splitLines } = require('./hash-engine');
|
|
39
|
+
|
|
40
|
+
// ─── Core LCS / Diff ────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build LCS length matrix.
|
|
44
|
+
* @param {String[]} a
|
|
45
|
+
* @param {String[]} b
|
|
46
|
+
* @returns {Array<Uint32Array>}
|
|
47
|
+
*/
|
|
48
|
+
function buildLcsMatrix(a, b) {
|
|
49
|
+
const rows = a.length + 1;
|
|
50
|
+
const cols = b.length + 1;
|
|
51
|
+
const matrix = Array.from({ length: rows }, () => new Uint32Array(cols));
|
|
52
|
+
|
|
53
|
+
for (let i = 1; i < rows; i++) {
|
|
54
|
+
for (let j = 1; j < cols; j++) {
|
|
55
|
+
if (a[i - 1] === b[j - 1]) {
|
|
56
|
+
matrix[i][j] = matrix[i - 1][j - 1] + 1;
|
|
57
|
+
} else {
|
|
58
|
+
matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return matrix;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Backtrack LCS matrix → line operations.
|
|
67
|
+
* @param {String[]} oldLines
|
|
68
|
+
* @param {String[]} newLines
|
|
69
|
+
* @returns {Array<{type: 'equal'|'insert'|'delete', oldLine: number, newLine: number, content: String}>}
|
|
70
|
+
*/
|
|
71
|
+
function buildLineOperations(oldLines, newLines) {
|
|
72
|
+
const matrix = buildLcsMatrix(oldLines, newLines);
|
|
73
|
+
const ops = [];
|
|
74
|
+
let i = oldLines.length;
|
|
75
|
+
let j = newLines.length;
|
|
76
|
+
|
|
77
|
+
while (i > 0 || j > 0) {
|
|
78
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
79
|
+
ops.push({ type: 'equal', oldLine: i, newLine: j, content: oldLines[i - 1] });
|
|
80
|
+
i--; j--;
|
|
81
|
+
} else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
|
|
82
|
+
ops.push({ type: 'insert', oldLine: i, newLine: j, content: newLines[j - 1] });
|
|
83
|
+
j--;
|
|
84
|
+
} else {
|
|
85
|
+
ops.push({ type: 'delete', oldLine: i, newLine: j, content: oldLines[i - 1] });
|
|
86
|
+
i--;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ops.reverse();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Diff two texts → operations + stats.
|
|
95
|
+
* @param {String} oldText
|
|
96
|
+
* @param {String} newText
|
|
97
|
+
* @returns {{ algorithm: string, operations: Array, stats: Object }}
|
|
98
|
+
*/
|
|
99
|
+
function diffText(oldText, newText) {
|
|
100
|
+
const oldLines = splitLines(oldText);
|
|
101
|
+
const newLines = splitLines(newText);
|
|
102
|
+
const operations = buildLineOperations(oldLines, newLines);
|
|
103
|
+
const stats = summarizeOperations(operations);
|
|
104
|
+
return { algorithm: 'lcs-line-v1', operations, stats };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Count insert/delete/equal ops.
|
|
109
|
+
* @param {Array} operations
|
|
110
|
+
* @returns {{insertions, deletions, unchanged, changes}}
|
|
111
|
+
*/
|
|
112
|
+
function summarizeOperations(operations) {
|
|
113
|
+
let insertions = 0, deletions = 0, unchanged = 0;
|
|
114
|
+
for (const op of operations) {
|
|
115
|
+
if (op.type === 'insert') insertions++;
|
|
116
|
+
else if (op.type === 'delete') deletions++;
|
|
117
|
+
else unchanged++;
|
|
118
|
+
}
|
|
119
|
+
return { insertions, deletions, unchanged, changes: insertions + deletions };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Hunk Generation ────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Group diff ops into hunks with context lines (like git diff).
|
|
126
|
+
* @param {Array} ops - From buildLineOperations
|
|
127
|
+
* @param {Number} contextLines - Context around changes (default 3)
|
|
128
|
+
* @returns {Array<{oldStart, oldCount, newStart, newCount, lines: String[]}>}
|
|
129
|
+
*/
|
|
130
|
+
function generateHunks(ops, contextLines = 3) {
|
|
131
|
+
const changeIndices = [];
|
|
132
|
+
for (let i = 0; i < ops.length; i++) {
|
|
133
|
+
if (ops[i].type !== 'equal') changeIndices.push(i);
|
|
134
|
+
}
|
|
135
|
+
if (changeIndices.length === 0) return [];
|
|
136
|
+
|
|
137
|
+
// Group changes within contextLines*2 of each other
|
|
138
|
+
const groups = [];
|
|
139
|
+
let group = [changeIndices[0]];
|
|
140
|
+
for (let i = 1; i < changeIndices.length; i++) {
|
|
141
|
+
if (changeIndices[i] - changeIndices[i - 1] <= contextLines * 2 + 1) {
|
|
142
|
+
group.push(changeIndices[i]);
|
|
143
|
+
} else {
|
|
144
|
+
groups.push(group);
|
|
145
|
+
group = [changeIndices[i]];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
groups.push(group);
|
|
149
|
+
|
|
150
|
+
const hunks = [];
|
|
151
|
+
for (const g of groups) {
|
|
152
|
+
const first = g[0];
|
|
153
|
+
const last = g[g.length - 1];
|
|
154
|
+
const start = Math.max(0, first - contextLines);
|
|
155
|
+
const end = Math.min(ops.length - 1, last + contextLines);
|
|
156
|
+
|
|
157
|
+
let oldLine = 0, newLine = 0;
|
|
158
|
+
for (let i = 0; i < start; i++) {
|
|
159
|
+
if (ops[i].type === 'equal' || ops[i].type === 'delete') oldLine++;
|
|
160
|
+
if (ops[i].type === 'equal' || ops[i].type === 'insert') newLine++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const hunkOldStart = oldLine + 1;
|
|
164
|
+
const hunkNewStart = newLine + 1;
|
|
165
|
+
let hunkOldCount = 0, hunkNewCount = 0;
|
|
166
|
+
const lines = [];
|
|
167
|
+
|
|
168
|
+
for (let i = start; i <= end; i++) {
|
|
169
|
+
const op = ops[i];
|
|
170
|
+
if (op.type === 'equal') {
|
|
171
|
+
lines.push(` ${op.content}`);
|
|
172
|
+
hunkOldCount++; hunkNewCount++;
|
|
173
|
+
} else if (op.type === 'delete') {
|
|
174
|
+
lines.push(`-${op.content}`);
|
|
175
|
+
hunkOldCount++;
|
|
176
|
+
} else {
|
|
177
|
+
lines.push(`+${op.content}`);
|
|
178
|
+
hunkNewCount++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
hunks.push({ oldStart: hunkOldStart, oldCount: hunkOldCount, newStart: hunkNewStart, newCount: hunkNewCount, lines });
|
|
183
|
+
}
|
|
184
|
+
return hunks;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Unified Diff Format ────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Format as unified diff string (like `git diff`).
|
|
191
|
+
* @param {String} filePath
|
|
192
|
+
* @param {String} oldText
|
|
193
|
+
* @param {String} newText
|
|
194
|
+
* @returns {String}
|
|
195
|
+
*/
|
|
196
|
+
function formatUnifiedDiff(filePath, oldText, newText) {
|
|
197
|
+
const oldLines = splitLines(oldText);
|
|
198
|
+
const newLines = splitLines(newText);
|
|
199
|
+
const ops = buildLineOperations(oldLines, newLines);
|
|
200
|
+
const hunks = generateHunks(ops);
|
|
201
|
+
if (hunks.length === 0) return '';
|
|
202
|
+
|
|
203
|
+
const out = [`--- a/${filePath}`, `+++ b/${filePath}`];
|
|
204
|
+
for (const h of hunks) {
|
|
205
|
+
out.push(`@@ -${h.oldStart},${h.oldCount} +${h.newStart},${h.newCount} @@`);
|
|
206
|
+
out.push(...h.lines);
|
|
207
|
+
}
|
|
208
|
+
return out.join('\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Patch Application ──────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Apply operations to reconstruct target from source.
|
|
215
|
+
* @param {Array} ops
|
|
216
|
+
* @returns {String[]} Reconstructed lines
|
|
217
|
+
*/
|
|
218
|
+
function applyOperations(ops) {
|
|
219
|
+
const result = [];
|
|
220
|
+
for (const op of ops) {
|
|
221
|
+
if (op.type === 'equal' || op.type === 'insert') {
|
|
222
|
+
result.push(op.content);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
diffText,
|
|
230
|
+
summarizeOperations,
|
|
231
|
+
buildLcsMatrix,
|
|
232
|
+
buildLineOperations,
|
|
233
|
+
generateHunks,
|
|
234
|
+
formatUnifiedDiff,
|
|
235
|
+
applyOperations
|
|
236
|
+
};
|
package/src/utils/fileSystem.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs').promises;
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const { GENT_DIR, DEFAULT_IGNORE_PATTERNS, IGNORE_FILE
|
|
8
|
+
const { GENT_DIR, DEFAULT_IGNORE_PATTERNS, IGNORE_FILE } = require('./constants');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Check if a path exists
|
|
@@ -113,31 +113,23 @@ async function getAllFiles(dir, ignorePatterns = []) {
|
|
|
113
113
|
*/
|
|
114
114
|
function shouldIgnore(filePath, patterns) {
|
|
115
115
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
116
|
-
const segments = normalizedPath.split('/');
|
|
117
116
|
|
|
118
117
|
for (const pattern of patterns) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Check if any segment matches the pattern (for directory-level ignores like node_modules)
|
|
122
|
-
if (segments.some(segment => segment === normalizedPattern)) {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Exact match or starts with pattern
|
|
127
|
-
if (normalizedPath === normalizedPattern || normalizedPath.startsWith(normalizedPattern + '/')) {
|
|
118
|
+
// Exact match
|
|
119
|
+
if (normalizedPath === pattern || normalizedPath.startsWith(pattern + '/')) {
|
|
128
120
|
return true;
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
// Wildcard match
|
|
132
|
-
if (
|
|
133
|
-
const regex = new RegExp('^' +
|
|
124
|
+
if (pattern.includes('*')) {
|
|
125
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
134
126
|
if (regex.test(normalizedPath)) {
|
|
135
127
|
return true;
|
|
136
128
|
}
|
|
137
129
|
}
|
|
138
130
|
|
|
139
131
|
// Extension match
|
|
140
|
-
if (
|
|
132
|
+
if (pattern.startsWith('*.') && normalizedPath.endsWith(pattern.substring(1))) {
|
|
141
133
|
return true;
|
|
142
134
|
}
|
|
143
135
|
}
|
|
@@ -178,53 +170,11 @@ async function getTrackedFiles(gentPath, commitHash) {
|
|
|
178
170
|
}
|
|
179
171
|
|
|
180
172
|
const repository = await readJSON(path.join(gentPath, 'commits.json'));
|
|
181
|
-
const commit = repository.commits.find(c => c.
|
|
173
|
+
const commit = repository.commits.find(c => c.hash === commitHash);
|
|
182
174
|
|
|
183
175
|
return commit ? commit.files : [];
|
|
184
176
|
}
|
|
185
177
|
|
|
186
|
-
/**
|
|
187
|
-
* Get path to the objects directory
|
|
188
|
-
* @returns {Promise<String>}
|
|
189
|
-
*/
|
|
190
|
-
async function getObjectsPath() {
|
|
191
|
-
const gentPath = await getGentPath();
|
|
192
|
-
const objectsPath = path.join(gentPath, OBJECTS_DIR);
|
|
193
|
-
await ensureDir(objectsPath);
|
|
194
|
-
return objectsPath;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Save file content as a blob
|
|
199
|
-
* @param {String} content - File content
|
|
200
|
-
* @param {String} hash - Content hash
|
|
201
|
-
*/
|
|
202
|
-
async function saveBlob(content, hash) {
|
|
203
|
-
const objectsPath = await getObjectsPath();
|
|
204
|
-
const blobPath = path.join(objectsPath, hash);
|
|
205
|
-
|
|
206
|
-
// Only write if it doesn't exist (content-addressable, immutable)
|
|
207
|
-
if (!await pathExists(blobPath)) {
|
|
208
|
-
await fs.writeFile(blobPath, content, 'utf-8');
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Get file content from blob
|
|
214
|
-
* @param {String} hash - Content hash
|
|
215
|
-
* @returns {Promise<String>}
|
|
216
|
-
*/
|
|
217
|
-
async function getBlob(hash) {
|
|
218
|
-
const objectsPath = await getObjectsPath();
|
|
219
|
-
const blobPath = path.join(objectsPath, hash);
|
|
220
|
-
|
|
221
|
-
if (!await pathExists(blobPath)) {
|
|
222
|
-
return ''; // Return empty string if blob missing (shouldn't happen in healthy repo)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return fs.readFile(blobPath, 'utf-8');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
178
|
module.exports = {
|
|
229
179
|
pathExists,
|
|
230
180
|
ensureDir,
|
|
@@ -234,7 +184,5 @@ module.exports = {
|
|
|
234
184
|
getAllFiles,
|
|
235
185
|
shouldIgnore,
|
|
236
186
|
getIgnorePatterns,
|
|
237
|
-
getTrackedFiles
|
|
238
|
-
saveBlob,
|
|
239
|
-
getBlob
|
|
187
|
+
getTrackedFiles
|
|
240
188
|
};
|