@cyanheads/git-mcp-server 1.2.4
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/LICENSE +201 -0
- package/README.md +383 -0
- package/build/index.js +54 -0
- package/build/resources/descriptors.js +77 -0
- package/build/resources/diff.js +241 -0
- package/build/resources/file.js +222 -0
- package/build/resources/history.js +242 -0
- package/build/resources/index.js +99 -0
- package/build/resources/repository.js +286 -0
- package/build/server.js +120 -0
- package/build/services/error-service.js +73 -0
- package/build/services/git-service.js +965 -0
- package/build/tools/advanced.js +526 -0
- package/build/tools/branch.js +296 -0
- package/build/tools/index.js +29 -0
- package/build/tools/remote.js +279 -0
- package/build/tools/repository.js +170 -0
- package/build/tools/workdir.js +445 -0
- package/build/types/git.js +7 -0
- package/build/utils/global-settings.js +64 -0
- package/build/utils/validation.js +108 -0
- package/package.json +39 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Service
|
|
3
|
+
* ===========
|
|
4
|
+
*
|
|
5
|
+
* An abstraction layer for Git operations using simple-git.
|
|
6
|
+
* Provides a clean interface for the MCP server to interact with Git repositories.
|
|
7
|
+
*/
|
|
8
|
+
import { simpleGit } from 'simple-git';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { PathValidation } from '../utils/validation.js'; // Import PathValidation
|
|
13
|
+
import { getGlobalSettings } from '../utils/global-settings.js'; // Import GlobalSettings
|
|
14
|
+
import { createGitError, createSuccessResult, createFailureResult, wrapExceptionAsStandardizedError, createStandardizedError, // Use the correct function
|
|
15
|
+
ErrorCategoryType, // Import category type
|
|
16
|
+
ErrorSeverityLevel // Import severity level
|
|
17
|
+
} from './error-service.js';
|
|
18
|
+
export class GitService {
|
|
19
|
+
git;
|
|
20
|
+
repoPath;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new GitService instance for a specific repository path
|
|
23
|
+
*
|
|
24
|
+
* @param repoPath - Path to the git repository
|
|
25
|
+
* @param options - Additional simple-git options
|
|
26
|
+
*/
|
|
27
|
+
constructor(repoPath, options = {}) {
|
|
28
|
+
// --- Path Sandboxing Validation ---
|
|
29
|
+
const settings = getGlobalSettings();
|
|
30
|
+
const normalizedRepoPath = PathValidation.normalizePath(repoPath);
|
|
31
|
+
if (!PathValidation.isWithinDirectory(normalizedRepoPath, settings.allowedBaseDir)) {
|
|
32
|
+
// Use createStandardizedError with appropriate parameters
|
|
33
|
+
throw createStandardizedError(`Access denied: Repository path '${repoPath}' (resolved to '${normalizedRepoPath}') is outside the allowed base directory '${settings.allowedBaseDir}'.`, 'PATH_OUTSIDE_BASEDIR', // Error code
|
|
34
|
+
ErrorCategoryType.CATEGORY_VALIDATION, // Category
|
|
35
|
+
ErrorSeverityLevel.SEVERITY_ERROR, // Severity
|
|
36
|
+
{ requestedPath: repoPath, resolvedPath: normalizedRepoPath, allowedBaseDir: settings.allowedBaseDir } // Context
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
this.repoPath = normalizedRepoPath; // Use the validated, normalized path
|
|
40
|
+
// --- End Path Sandboxing Validation ---
|
|
41
|
+
try {
|
|
42
|
+
// Try to get the global git user configuration
|
|
43
|
+
const globalUserName = execSync('git config --global user.name').toString().trim();
|
|
44
|
+
const globalUserEmail = execSync('git config --global user.email').toString().trim();
|
|
45
|
+
// Initialize git with this configuration to ensure it uses the global values
|
|
46
|
+
this.git = simpleGit(this.repoPath, {
|
|
47
|
+
...options,
|
|
48
|
+
config: [
|
|
49
|
+
`user.name=${globalUserName}`,
|
|
50
|
+
`user.email=${globalUserEmail}`
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// If we can't get the global config, fall back to standard initialization
|
|
56
|
+
console.error('Failed to get global git config, using default initialization', error);
|
|
57
|
+
this.git = simpleGit(this.repoPath, {
|
|
58
|
+
...options,
|
|
59
|
+
baseDir: this.repoPath
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validates a path relative to the repository root to prevent traversal.
|
|
65
|
+
* Throws a standardized error if validation fails.
|
|
66
|
+
*
|
|
67
|
+
* @param relativePath - The path relative to the repository root.
|
|
68
|
+
* @param paramName - The name of the parameter being validated (for error messages).
|
|
69
|
+
* @returns The validated and normalized relative path.
|
|
70
|
+
*/
|
|
71
|
+
validateRelativePath(relativePath, paramName) {
|
|
72
|
+
// Avoid validating special case '.' which means 'current directory' or 'all' in some contexts
|
|
73
|
+
if (relativePath === '.' || relativePath === '') {
|
|
74
|
+
return relativePath;
|
|
75
|
+
}
|
|
76
|
+
// Normalize the input relative path *before* resolving against the repo path
|
|
77
|
+
// This handles inputs like 'subdir/../file' correctly within the context of the repo
|
|
78
|
+
const normalizedRelativePath = path.normalize(relativePath);
|
|
79
|
+
// Resolve the normalized relative path against the repository path
|
|
80
|
+
const resolvedPath = path.resolve(this.repoPath, normalizedRelativePath);
|
|
81
|
+
// Use the existing validation utility
|
|
82
|
+
// Check if the resolved path is the repo path itself OR is within it
|
|
83
|
+
if (resolvedPath !== this.repoPath && !PathValidation.isWithinDirectory(resolvedPath, this.repoPath)) {
|
|
84
|
+
throw createStandardizedError(`Access denied: Path '${relativePath}' in parameter '${paramName}' (resolved to '${resolvedPath}') attempts to traverse outside the repository root '${this.repoPath}'.`, 'PATH_TRAVERSAL_ATTEMPT', // Keep specific error code
|
|
85
|
+
ErrorCategoryType.CATEGORY_VALIDATION, // Use existing validation category
|
|
86
|
+
ErrorSeverityLevel.SEVERITY_ERROR, // Use existing error severity
|
|
87
|
+
{ requestedPath: relativePath, resolvedPath: resolvedPath, repoPath: this.repoPath, parameter: paramName });
|
|
88
|
+
}
|
|
89
|
+
// Return the normalized relative path, as simple-git expects paths relative to repoPath
|
|
90
|
+
return normalizedRelativePath;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Handles Git errors in a standardized way
|
|
94
|
+
*
|
|
95
|
+
* @param error - The error to handle
|
|
96
|
+
* @param defaultMessage - Default message if error is not a Git error
|
|
97
|
+
* @returns Standardized error object
|
|
98
|
+
*/
|
|
99
|
+
handleGitError(error, defaultMessage) {
|
|
100
|
+
if (error.code) {
|
|
101
|
+
const gitError = error;
|
|
102
|
+
return createGitError(gitError.message || defaultMessage, gitError.code || 'GIT_ERROR', {
|
|
103
|
+
command: gitError.command,
|
|
104
|
+
args: gitError.args,
|
|
105
|
+
stderr: gitError.stderr
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return wrapExceptionAsStandardizedError(error, defaultMessage);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Ensures the repository directory exists
|
|
112
|
+
*
|
|
113
|
+
* @returns Promise resolving when directory exists or is created
|
|
114
|
+
*/
|
|
115
|
+
async ensureRepoPathExists() {
|
|
116
|
+
try {
|
|
117
|
+
await fs.access(this.repoPath);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// Create directory if it doesn't exist
|
|
121
|
+
await fs.mkdir(this.repoPath, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Checks if a path is a Git repository
|
|
126
|
+
*
|
|
127
|
+
* @param dirPath - Path to check
|
|
128
|
+
* @returns Promise resolving to true if path is a Git repository
|
|
129
|
+
*/
|
|
130
|
+
async isGitRepository(dirPath = this.repoPath) {
|
|
131
|
+
// --- Path Sandboxing Validation ---
|
|
132
|
+
const settings = getGlobalSettings();
|
|
133
|
+
const normalizedDirPath = PathValidation.normalizePath(dirPath);
|
|
134
|
+
if (!PathValidation.isWithinDirectory(normalizedDirPath, settings.allowedBaseDir)) {
|
|
135
|
+
console.error(`Security Warning: isGitRepository check attempted outside allowed base directory. Path: ${dirPath}, Base: ${settings.allowedBaseDir}`);
|
|
136
|
+
// Return false instead of throwing an error, as this might be used for checks
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// --- End Path Sandboxing Validation ---
|
|
140
|
+
try {
|
|
141
|
+
const gitDir = path.join(normalizedDirPath, '.git'); // Use normalized path
|
|
142
|
+
await fs.access(gitDir);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
try {
|
|
147
|
+
// Check if it's a bare repository by looking for common Git files
|
|
148
|
+
const gitFiles = ['HEAD', 'config', 'objects', 'refs'];
|
|
149
|
+
for (const file of gitFiles) {
|
|
150
|
+
await fs.access(path.join(normalizedDirPath, file)); // Use normalized path
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ==========================================
|
|
160
|
+
// Repository Operations
|
|
161
|
+
// ==========================================
|
|
162
|
+
/**
|
|
163
|
+
* Initializes a new Git repository
|
|
164
|
+
*
|
|
165
|
+
* @param bare - Whether to create a bare repository
|
|
166
|
+
* @param initialBranch - Initial branch name (default: main)
|
|
167
|
+
* @returns Promise resolving to operation result
|
|
168
|
+
*/
|
|
169
|
+
async initRepo(bare = false, initialBranch = 'main') {
|
|
170
|
+
try {
|
|
171
|
+
await this.ensureRepoPathExists();
|
|
172
|
+
// Use init with options to set the initial branch name
|
|
173
|
+
const initOptions = {
|
|
174
|
+
'--initial-branch': initialBranch,
|
|
175
|
+
'--bare': bare ? true : undefined,
|
|
176
|
+
};
|
|
177
|
+
const result = await this.git.init(initOptions);
|
|
178
|
+
// If we're not in a bare repository, make an initial commit to establish the branch
|
|
179
|
+
if (!bare) {
|
|
180
|
+
try {
|
|
181
|
+
// Create a README.md file as first commit to establish the branch
|
|
182
|
+
const readmePath = path.join(this.repoPath, 'README.md');
|
|
183
|
+
await fs.writeFile(readmePath, `# Git Repository\n\nInitialized with branch '${initialBranch}'.`);
|
|
184
|
+
await this.git.add('README.md');
|
|
185
|
+
await this.git.commit(`Initial commit`, { '--allow-empty': null });
|
|
186
|
+
}
|
|
187
|
+
catch (commitError) {
|
|
188
|
+
// If initial commit fails, it's not critical - the repo is still initialized
|
|
189
|
+
console.error('Failed to create initial commit:', commitError);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return createSuccessResult(result);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return createFailureResult(this.handleGitError(error, 'Failed to initialize repository'));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Clones a Git repository
|
|
200
|
+
*
|
|
201
|
+
* @param url - URL of the repository to clone
|
|
202
|
+
* @param options - Clone options
|
|
203
|
+
* @returns Promise resolving to operation result
|
|
204
|
+
*/
|
|
205
|
+
async cloneRepo(url, options = {}) {
|
|
206
|
+
try {
|
|
207
|
+
await this.ensureRepoPathExists();
|
|
208
|
+
const result = await this.git.clone(url, this.repoPath, options);
|
|
209
|
+
return createSuccessResult(result);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return createFailureResult(this.handleGitError(error, `Failed to clone repository from ${url}`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Gets the status of the repository
|
|
217
|
+
*
|
|
218
|
+
* @returns Promise resolving to repository status
|
|
219
|
+
*/
|
|
220
|
+
async getStatus() {
|
|
221
|
+
try {
|
|
222
|
+
const status = await this.git.status();
|
|
223
|
+
return createSuccessResult(status);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return createFailureResult(this.handleGitError(error, 'Failed to get repository status'));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ==========================================
|
|
230
|
+
// Commit Operations
|
|
231
|
+
// ==========================================
|
|
232
|
+
/**
|
|
233
|
+
* Stages files for commit
|
|
234
|
+
*
|
|
235
|
+
* @param files - Array of file paths to stage, or '.' for all
|
|
236
|
+
* @returns Promise resolving to operation result
|
|
237
|
+
*/
|
|
238
|
+
async stageFiles(files = '.') {
|
|
239
|
+
try {
|
|
240
|
+
let validatedFiles;
|
|
241
|
+
if (Array.isArray(files)) {
|
|
242
|
+
if (files.length === 0) {
|
|
243
|
+
return createSuccessResult('No files to stage');
|
|
244
|
+
}
|
|
245
|
+
// Validate each file path in the array
|
|
246
|
+
validatedFiles = files.map(file => this.validateRelativePath(file, 'files'));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Validate the single file path (unless it's '.')
|
|
250
|
+
validatedFiles = this.validateRelativePath(files, 'files');
|
|
251
|
+
}
|
|
252
|
+
const result = await this.git.add(validatedFiles); // Use validated files
|
|
253
|
+
return createSuccessResult(result);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
257
|
+
if (error?.errorCode) {
|
|
258
|
+
throw error; // Rethrow standardized errors directly
|
|
259
|
+
}
|
|
260
|
+
return createFailureResult(this.handleGitError(error, 'Failed to stage files'));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Unstages files
|
|
265
|
+
*
|
|
266
|
+
* @param files - Array of file paths to unstage, or '.' for all
|
|
267
|
+
* @returns Promise resolving to operation result
|
|
268
|
+
*/
|
|
269
|
+
async unstageFiles(files = '.') {
|
|
270
|
+
try {
|
|
271
|
+
let validatedFiles;
|
|
272
|
+
if (Array.isArray(files)) {
|
|
273
|
+
if (files.length === 0) {
|
|
274
|
+
return createSuccessResult('No files to unstage');
|
|
275
|
+
}
|
|
276
|
+
// Validate each file path in the array
|
|
277
|
+
validatedFiles = files.map(file => this.validateRelativePath(file, 'files'));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Validate the single file path (unless it's '.')
|
|
281
|
+
validatedFiles = this.validateRelativePath(files, 'files');
|
|
282
|
+
}
|
|
283
|
+
// Use reset to unstage files
|
|
284
|
+
const result = await this.git.reset(['--', ...(Array.isArray(validatedFiles) ? validatedFiles : [validatedFiles])]); // Use validated files
|
|
285
|
+
return createSuccessResult(result);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
289
|
+
if (error?.errorCode) {
|
|
290
|
+
throw error; // Rethrow standardized errors directly
|
|
291
|
+
}
|
|
292
|
+
return createFailureResult(this.handleGitError(error, 'Failed to unstage files'));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Creates a commit
|
|
297
|
+
*
|
|
298
|
+
* @param options - Commit options
|
|
299
|
+
* @returns Promise resolving to commit hash
|
|
300
|
+
*/
|
|
301
|
+
async commit(options) {
|
|
302
|
+
try {
|
|
303
|
+
// Simple-git uses the underlying git config for author information
|
|
304
|
+
// when these specific options aren't provided, so we'll only set
|
|
305
|
+
// them when explicitly specified
|
|
306
|
+
const commitOptions = {
|
|
307
|
+
'--allow-empty': options.allowEmpty ? null : undefined,
|
|
308
|
+
'--amend': options.amend ? null : undefined
|
|
309
|
+
};
|
|
310
|
+
if (options.author && options.author.name) {
|
|
311
|
+
commitOptions['--author'] = `${options.author.name} <${options.author.email || ''}>`;
|
|
312
|
+
}
|
|
313
|
+
const result = await this.git.commit(options.message, commitOptions);
|
|
314
|
+
return createSuccessResult(result.commit || '');
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return createFailureResult(this.handleGitError(error, 'Failed to create commit'));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// ==========================================
|
|
321
|
+
// Branch Operations
|
|
322
|
+
// ==========================================
|
|
323
|
+
/**
|
|
324
|
+
* Creates a new branch
|
|
325
|
+
*
|
|
326
|
+
* @param options - Branch options
|
|
327
|
+
* @returns Promise resolving to operation result
|
|
328
|
+
*/
|
|
329
|
+
async createBranch(options) {
|
|
330
|
+
try {
|
|
331
|
+
const branchParams = [options.name];
|
|
332
|
+
if (options.startPoint)
|
|
333
|
+
branchParams.push(options.startPoint);
|
|
334
|
+
await this.git.branch(branchParams);
|
|
335
|
+
if (options.checkout) {
|
|
336
|
+
await this.git.checkout(options.name);
|
|
337
|
+
}
|
|
338
|
+
return createSuccessResult(`Branch '${options.name}' created successfully`);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return createFailureResult(this.handleGitError(error, `Failed to create branch '${options.name}'`));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Lists all branches
|
|
346
|
+
*
|
|
347
|
+
* @param all - Whether to include remote branches
|
|
348
|
+
* @returns Promise resolving to branch summary including current branch
|
|
349
|
+
*/
|
|
350
|
+
async listBranches(all = false) {
|
|
351
|
+
try {
|
|
352
|
+
const branchSummary = await this.git.branch(all ? ['-a'] : []);
|
|
353
|
+
// Directly return the summary object provided by simple-git
|
|
354
|
+
return createSuccessResult(branchSummary);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
return createFailureResult(this.handleGitError(error, 'Failed to list branches'));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Checkout a branch or commit
|
|
362
|
+
*
|
|
363
|
+
* @param target - Branch name, commit hash, or reference to checkout
|
|
364
|
+
* @param createBranch - Whether to create the branch if it doesn't exist
|
|
365
|
+
* @returns Promise resolving to operation result
|
|
366
|
+
*/
|
|
367
|
+
async checkout(target, createBranch = false) {
|
|
368
|
+
try {
|
|
369
|
+
const options = createBranch ? ['-b'] : [];
|
|
370
|
+
const result = await this.git.checkout([...options, target]);
|
|
371
|
+
return createSuccessResult(result);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
return createFailureResult(this.handleGitError(error, `Failed to checkout '${target}'`));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Delete a branch
|
|
379
|
+
*
|
|
380
|
+
* @param branchName - Name of the branch to delete
|
|
381
|
+
* @param force - Whether to force delete
|
|
382
|
+
* @returns Promise resolving to operation result
|
|
383
|
+
*/
|
|
384
|
+
async deleteBranch(branchName, force = false) {
|
|
385
|
+
try {
|
|
386
|
+
const options = force ? ['-D'] : ['-d'];
|
|
387
|
+
const result = await this.git.branch([...options, branchName]);
|
|
388
|
+
return createSuccessResult(result);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
return createFailureResult(this.handleGitError(error, `Failed to delete branch '${branchName}'`));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Merge a branch into the current branch
|
|
396
|
+
*
|
|
397
|
+
* @param options - Merge options
|
|
398
|
+
* @returns Promise resolving to merge result
|
|
399
|
+
*/
|
|
400
|
+
async merge(options) {
|
|
401
|
+
try {
|
|
402
|
+
const mergeParams = [options.branch];
|
|
403
|
+
if (options.fastForwardOnly)
|
|
404
|
+
mergeParams.unshift('--ff-only');
|
|
405
|
+
if (options.noFastForward)
|
|
406
|
+
mergeParams.unshift('--no-ff');
|
|
407
|
+
if (options.message) {
|
|
408
|
+
mergeParams.unshift('-m', options.message);
|
|
409
|
+
}
|
|
410
|
+
const result = await this.git.merge(mergeParams);
|
|
411
|
+
return createSuccessResult(result);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return createFailureResult(this.handleGitError(error, `Failed to merge branch '${options.branch}'`));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// ==========================================
|
|
418
|
+
// Remote Operations
|
|
419
|
+
// ==========================================
|
|
420
|
+
/**
|
|
421
|
+
* Add a remote
|
|
422
|
+
*
|
|
423
|
+
* @param options - Remote options
|
|
424
|
+
* @returns Promise resolving to operation result
|
|
425
|
+
*/
|
|
426
|
+
async addRemote(options) {
|
|
427
|
+
try {
|
|
428
|
+
const result = await this.git.addRemote(options.name, options.url);
|
|
429
|
+
return createSuccessResult(result);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
return createFailureResult(this.handleGitError(error, `Failed to add remote '${options.name}'`));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* List remotes
|
|
437
|
+
*
|
|
438
|
+
* @returns Promise resolving to list of remotes
|
|
439
|
+
*/
|
|
440
|
+
async listRemotes() {
|
|
441
|
+
try {
|
|
442
|
+
const remotes = await this.git.getRemotes(true);
|
|
443
|
+
return createSuccessResult(remotes);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
return createFailureResult(this.handleGitError(error, 'Failed to list remotes'));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Fetch from a remote
|
|
451
|
+
*
|
|
452
|
+
* @param remote - Remote to fetch from (default: origin)
|
|
453
|
+
* @param branch - Branch to fetch (default: all branches)
|
|
454
|
+
* @returns Promise resolving to fetch result
|
|
455
|
+
*/
|
|
456
|
+
async fetch(remote = 'origin', branch) {
|
|
457
|
+
try {
|
|
458
|
+
const options = branch ? [remote, branch] : [remote];
|
|
459
|
+
const result = await this.git.fetch(options);
|
|
460
|
+
return createSuccessResult(result);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return createFailureResult(this.handleGitError(error, `Failed to fetch from remote '${remote}'`));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Pull from a remote
|
|
468
|
+
*
|
|
469
|
+
* @param options - Pull options
|
|
470
|
+
* @returns Promise resolving to pull result
|
|
471
|
+
*/
|
|
472
|
+
async pull(options = {}) {
|
|
473
|
+
try {
|
|
474
|
+
const pullOptions = {};
|
|
475
|
+
if (options.remote)
|
|
476
|
+
pullOptions.remote = options.remote;
|
|
477
|
+
if (options.branch)
|
|
478
|
+
pullOptions.branch = options.branch;
|
|
479
|
+
if (options.rebase)
|
|
480
|
+
pullOptions['--rebase'] = null;
|
|
481
|
+
const result = await this.git.pull(pullOptions);
|
|
482
|
+
return createSuccessResult(result);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
return createFailureResult(this.handleGitError(error, 'Failed to pull changes'));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Push to a remote
|
|
490
|
+
*
|
|
491
|
+
* @param options - Push options
|
|
492
|
+
* @returns Promise resolving to push result
|
|
493
|
+
*/
|
|
494
|
+
async push(options = {}) {
|
|
495
|
+
try {
|
|
496
|
+
const pushOptions = [];
|
|
497
|
+
if (options.force)
|
|
498
|
+
pushOptions.push('--force');
|
|
499
|
+
if (options.setUpstream)
|
|
500
|
+
pushOptions.push('--set-upstream');
|
|
501
|
+
const remote = options.remote || 'origin';
|
|
502
|
+
const branch = options.branch || 'HEAD';
|
|
503
|
+
const result = await this.git.push(remote, branch, pushOptions);
|
|
504
|
+
return createSuccessResult(result);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
return createFailureResult(this.handleGitError(error, 'Failed to push changes'));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// ==========================================
|
|
511
|
+
// History Operations
|
|
512
|
+
// ==========================================
|
|
513
|
+
/**
|
|
514
|
+
* Get commit history
|
|
515
|
+
*
|
|
516
|
+
* @param options - Options for git log
|
|
517
|
+
* @returns Promise resolving to commit history
|
|
518
|
+
*/
|
|
519
|
+
async getLog(options = {}) {
|
|
520
|
+
try {
|
|
521
|
+
let validatedFilePath;
|
|
522
|
+
if (options.file) {
|
|
523
|
+
validatedFilePath = this.validateRelativePath(options.file, 'options.file');
|
|
524
|
+
}
|
|
525
|
+
// Build options object for simple-git in the format it expects
|
|
526
|
+
const logOptions = {
|
|
527
|
+
maxCount: options.maxCount || 50,
|
|
528
|
+
format: {
|
|
529
|
+
hash: '%H',
|
|
530
|
+
abbrevHash: '%h',
|
|
531
|
+
author_name: '%an',
|
|
532
|
+
author_email: '%ae',
|
|
533
|
+
date: '%ai',
|
|
534
|
+
message: '%s'
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
if (validatedFilePath) {
|
|
538
|
+
logOptions.file = validatedFilePath; // Use validated path
|
|
539
|
+
}
|
|
540
|
+
const result = await this.git.log(logOptions);
|
|
541
|
+
// Parse the log output into structured data
|
|
542
|
+
const entries = result.all.map((entry) => ({
|
|
543
|
+
hash: entry.hash,
|
|
544
|
+
abbrevHash: entry.hash.substring(0, 7),
|
|
545
|
+
author: entry.author_name,
|
|
546
|
+
authorEmail: entry.author_email,
|
|
547
|
+
date: new Date(entry.date),
|
|
548
|
+
message: entry.message,
|
|
549
|
+
refs: entry.refs
|
|
550
|
+
}));
|
|
551
|
+
return createSuccessResult(entries);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
555
|
+
if (error?.errorCode) {
|
|
556
|
+
throw error; // Rethrow standardized errors directly
|
|
557
|
+
}
|
|
558
|
+
return createFailureResult(this.handleGitError(error, 'Failed to get commit history'));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get file blame information
|
|
563
|
+
*
|
|
564
|
+
* @param filePath - Path to the file
|
|
565
|
+
* @returns Promise resolving to blame information
|
|
566
|
+
*/
|
|
567
|
+
async getBlame(filePath) {
|
|
568
|
+
try {
|
|
569
|
+
const validatedPath = this.validateRelativePath(filePath, 'filePath');
|
|
570
|
+
const result = await this.git.raw(['blame', validatedPath]); // Use validated path
|
|
571
|
+
return createSuccessResult(result);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
575
|
+
if (error?.errorCode) {
|
|
576
|
+
throw error; // Rethrow standardized errors directly
|
|
577
|
+
}
|
|
578
|
+
return createFailureResult(this.handleGitError(error, `Failed to get blame for file '${filePath}'`));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get the diff between commits
|
|
583
|
+
*
|
|
584
|
+
* @param fromRef - Starting reference (commit, branch, etc.)
|
|
585
|
+
* @param toRef - Ending reference (default: current working tree)
|
|
586
|
+
* @param path - Optional path to restrict the diff to
|
|
587
|
+
* @returns Promise resolving to diff information
|
|
588
|
+
*/
|
|
589
|
+
async getDiff(fromRef, toRef = 'HEAD', filePath) {
|
|
590
|
+
try {
|
|
591
|
+
let validatedPath;
|
|
592
|
+
if (filePath) {
|
|
593
|
+
validatedPath = this.validateRelativePath(filePath, 'filePath');
|
|
594
|
+
}
|
|
595
|
+
const args = ['diff', '--name-status', fromRef];
|
|
596
|
+
if (toRef !== 'HEAD') {
|
|
597
|
+
args.push(toRef);
|
|
598
|
+
}
|
|
599
|
+
if (validatedPath) {
|
|
600
|
+
args.push('--', validatedPath); // Use validated path
|
|
601
|
+
}
|
|
602
|
+
const result = await this.git.raw(args);
|
|
603
|
+
const entries = [];
|
|
604
|
+
// Parse the diff output into structured data
|
|
605
|
+
const lines = result.split('\n').filter((line) => line.trim() !== '');
|
|
606
|
+
for (const line of lines) {
|
|
607
|
+
const [status, ...pathParts] = line.split('\t');
|
|
608
|
+
const filePath = pathParts.join('\t');
|
|
609
|
+
entries.push({
|
|
610
|
+
path: filePath,
|
|
611
|
+
// Mapping status letters to more descriptive terms
|
|
612
|
+
status: status === 'A' ? 'added' :
|
|
613
|
+
status === 'M' ? 'modified' :
|
|
614
|
+
status === 'D' ? 'deleted' :
|
|
615
|
+
status === 'R' ? 'renamed' :
|
|
616
|
+
status === 'C' ? 'copied' : status
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return createSuccessResult(entries);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
623
|
+
if (error?.errorCode) {
|
|
624
|
+
throw error; // Rethrow standardized errors directly
|
|
625
|
+
}
|
|
626
|
+
return createFailureResult(this.handleGitError(error, 'Failed to get diff'));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Get the content of a file at a specific reference
|
|
631
|
+
*
|
|
632
|
+
* @param filePath - Path to the file
|
|
633
|
+
* @param ref - Git reference (commit, branch, etc.)
|
|
634
|
+
* @returns Promise resolving to file content
|
|
635
|
+
*/
|
|
636
|
+
async getFileAtRef(filePath, ref = 'HEAD') {
|
|
637
|
+
try {
|
|
638
|
+
const validatedPath = this.validateRelativePath(filePath, 'filePath');
|
|
639
|
+
// Note: simple-git's show command expects the path within the ref string itself.
|
|
640
|
+
// We validated the path component, but we still construct the argument as needed.
|
|
641
|
+
const result = await this.git.show([`${ref}:${validatedPath}`]); // Use validated path component
|
|
642
|
+
return createSuccessResult(result);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
646
|
+
if (error?.errorCode) {
|
|
647
|
+
throw error; // Rethrow standardized errors directly
|
|
648
|
+
}
|
|
649
|
+
return createFailureResult(this.handleGitError(error, `Failed to get file '${filePath}' at ref '${ref}'`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get unstaged diff (changes in working directory)
|
|
654
|
+
*
|
|
655
|
+
* @param path - Optional path to restrict the diff to
|
|
656
|
+
* @param showUntracked - Whether to include information about untracked files
|
|
657
|
+
* @returns Promise resolving to diff content
|
|
658
|
+
*/
|
|
659
|
+
async getUnstagedDiff(filePath, showUntracked = true) {
|
|
660
|
+
try {
|
|
661
|
+
let validatedPath;
|
|
662
|
+
if (filePath) {
|
|
663
|
+
validatedPath = this.validateRelativePath(filePath, 'filePath');
|
|
664
|
+
}
|
|
665
|
+
const args = ['diff'];
|
|
666
|
+
if (validatedPath) {
|
|
667
|
+
args.push('--', validatedPath); // Use validated path
|
|
668
|
+
}
|
|
669
|
+
let diffResult = await this.git.raw(args);
|
|
670
|
+
// If requested, also include information about untracked files
|
|
671
|
+
if (showUntracked) {
|
|
672
|
+
try {
|
|
673
|
+
// Get status to find untracked files
|
|
674
|
+
const statusResult = await this.getStatus();
|
|
675
|
+
if (statusResult.resultSuccessful && statusResult.resultData.not_added.length > 0) {
|
|
676
|
+
// Filter untracked files by validatedPath if specified
|
|
677
|
+
const untrackedFiles = validatedPath
|
|
678
|
+
? statusResult.resultData.not_added.filter(file => file === validatedPath || file.startsWith(validatedPath + '/'))
|
|
679
|
+
: statusResult.resultData.not_added;
|
|
680
|
+
if (untrackedFiles.length > 0) {
|
|
681
|
+
// Add header for untracked files if we have a diff and untracked files
|
|
682
|
+
if (diffResult.trim() !== '') {
|
|
683
|
+
diffResult += '\n\n';
|
|
684
|
+
}
|
|
685
|
+
diffResult += '# Untracked files:\n';
|
|
686
|
+
for (const file of untrackedFiles) {
|
|
687
|
+
diffResult += `# - ${file}\n`;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
// Silently ignore errors with listing untracked files
|
|
694
|
+
console.error('Error listing untracked files:', error);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return createSuccessResult(diffResult);
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
701
|
+
if (error?.errorCode) {
|
|
702
|
+
throw error; // Rethrow standardized errors directly
|
|
703
|
+
}
|
|
704
|
+
return createFailureResult(this.handleGitError(error, 'Failed to get unstaged diff'));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Get staged diff (changes in index)
|
|
709
|
+
*
|
|
710
|
+
* @param path - Optional path to restrict the diff to
|
|
711
|
+
* @returns Promise resolving to diff content
|
|
712
|
+
*/
|
|
713
|
+
async getStagedDiff(filePath) {
|
|
714
|
+
try {
|
|
715
|
+
let validatedPath;
|
|
716
|
+
if (filePath) {
|
|
717
|
+
validatedPath = this.validateRelativePath(filePath, 'filePath');
|
|
718
|
+
}
|
|
719
|
+
const args = ['diff', '--cached'];
|
|
720
|
+
if (validatedPath) {
|
|
721
|
+
args.push('--', validatedPath); // Use validated path
|
|
722
|
+
}
|
|
723
|
+
const result = await this.git.raw(args);
|
|
724
|
+
return createSuccessResult(result);
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
728
|
+
if (error?.errorCode) {
|
|
729
|
+
throw error; // Rethrow standardized errors directly
|
|
730
|
+
}
|
|
731
|
+
return createFailureResult(this.handleGitError(error, 'Failed to get staged diff'));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* List files in a directory at a specific reference
|
|
736
|
+
*
|
|
737
|
+
* @param dirPath - Path to the directory (relative to the repo root)
|
|
738
|
+
* @param ref - Git reference (commit, branch, etc.)
|
|
739
|
+
* @returns Promise resolving to list of immediate files/directories within the specified path
|
|
740
|
+
*/
|
|
741
|
+
async listFilesAtRef(dirPath = '.', ref = 'HEAD') {
|
|
742
|
+
try {
|
|
743
|
+
const validatedDirPath = this.validateRelativePath(dirPath, 'dirPath');
|
|
744
|
+
// Remove '-r' to list only immediate children, not recursive
|
|
745
|
+
// Use the validated path
|
|
746
|
+
const result = await this.git.raw(['ls-tree', '--name-only', ref, validatedDirPath]);
|
|
747
|
+
// Parse the output
|
|
748
|
+
const files = result.split('\n')
|
|
749
|
+
.filter((line) => line.trim() !== '');
|
|
750
|
+
// No need for post-filtering, ls-tree with validatedDirPath handles it.
|
|
751
|
+
// .filter((file: string) => {
|
|
752
|
+
// // If validatedDirPath is empty or root, include all files
|
|
753
|
+
// if (!validatedDirPath || validatedDirPath === '.') {
|
|
754
|
+
// return true;
|
|
755
|
+
// }
|
|
756
|
+
// // Otherwise, only include files that are within the directory
|
|
757
|
+
// // This check might be redundant now with ls-tree using the validated path
|
|
758
|
+
// return file.startsWith(validatedDirPath + '/');
|
|
759
|
+
// });
|
|
760
|
+
return createSuccessResult(files);
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
// Check if it's our standardized error by looking for errorCode property
|
|
764
|
+
if (error?.errorCode) {
|
|
765
|
+
throw error; // Rethrow standardized errors directly
|
|
766
|
+
}
|
|
767
|
+
return createFailureResult(this.handleGitError(error, `Failed to list files in directory '${dirPath}' at ref '${ref}'`));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// ==========================================
|
|
771
|
+
// Advanced Operations
|
|
772
|
+
// ==========================================
|
|
773
|
+
/**
|
|
774
|
+
* Create a tag
|
|
775
|
+
*
|
|
776
|
+
* @param options - Tag options
|
|
777
|
+
* @returns Promise resolving to operation result
|
|
778
|
+
*/
|
|
779
|
+
async createTag(options) {
|
|
780
|
+
try {
|
|
781
|
+
const tagArgs = [options.name];
|
|
782
|
+
if (options.ref) {
|
|
783
|
+
tagArgs.push(options.ref);
|
|
784
|
+
}
|
|
785
|
+
if (options.message) {
|
|
786
|
+
tagArgs.unshift('-m', options.message);
|
|
787
|
+
// -a creates an annotated tag
|
|
788
|
+
tagArgs.unshift('-a');
|
|
789
|
+
}
|
|
790
|
+
const result = await this.git.tag(tagArgs);
|
|
791
|
+
return createSuccessResult(result);
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
return createFailureResult(this.handleGitError(error, `Failed to create tag '${options.name}'`));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* List tags
|
|
799
|
+
*
|
|
800
|
+
* @returns Promise resolving to list of tags
|
|
801
|
+
*/
|
|
802
|
+
async listTags() {
|
|
803
|
+
try {
|
|
804
|
+
const tags = await this.git.tags();
|
|
805
|
+
return createSuccessResult(tags.all);
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
return createFailureResult(this.handleGitError(error, 'Failed to list tags'));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Create a stash
|
|
813
|
+
*
|
|
814
|
+
* @param options - Stash options
|
|
815
|
+
* @returns Promise resolving to operation result
|
|
816
|
+
*/
|
|
817
|
+
async createStash(options = {}) {
|
|
818
|
+
try {
|
|
819
|
+
const stashArgs = [];
|
|
820
|
+
if (options.message) {
|
|
821
|
+
stashArgs.push('save', options.message);
|
|
822
|
+
}
|
|
823
|
+
if (options.includeUntracked) {
|
|
824
|
+
stashArgs.push('--include-untracked');
|
|
825
|
+
}
|
|
826
|
+
const result = await this.git.stash(stashArgs);
|
|
827
|
+
return createSuccessResult(result);
|
|
828
|
+
}
|
|
829
|
+
catch (error) {
|
|
830
|
+
return createFailureResult(this.handleGitError(error, 'Failed to create stash'));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* List stashes
|
|
835
|
+
*
|
|
836
|
+
* @returns Promise resolving to list of stashes
|
|
837
|
+
*/
|
|
838
|
+
async listStashes() {
|
|
839
|
+
try {
|
|
840
|
+
const result = await this.git.stash(['list']);
|
|
841
|
+
return createSuccessResult(result);
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
return createFailureResult(this.handleGitError(error, 'Failed to list stashes'));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Show a commit's details
|
|
849
|
+
*
|
|
850
|
+
* @param commitHash - Hash of the commit to show
|
|
851
|
+
* @returns Promise resolving to commit details
|
|
852
|
+
*/
|
|
853
|
+
async showCommit(commitHash) {
|
|
854
|
+
try {
|
|
855
|
+
const result = await this.git.raw(['show', commitHash]);
|
|
856
|
+
return createSuccessResult(result);
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
return createFailureResult(this.handleGitError(error, `Failed to show commit '${commitHash}'`));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Apply a stash
|
|
864
|
+
*
|
|
865
|
+
* @param stashId - Stash identifier (default: most recent stash)
|
|
866
|
+
* @returns Promise resolving to operation result
|
|
867
|
+
*/
|
|
868
|
+
async applyStash(stashId = 'stash@{0}') {
|
|
869
|
+
try {
|
|
870
|
+
const result = await this.git.stash(['apply', stashId]);
|
|
871
|
+
return createSuccessResult(result);
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
return createFailureResult(this.handleGitError(error, `Failed to apply stash '${stashId}'`));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Pop a stash
|
|
879
|
+
*
|
|
880
|
+
* @param stashId - Stash identifier (default: most recent stash)
|
|
881
|
+
* @returns Promise resolving to operation result
|
|
882
|
+
*/
|
|
883
|
+
async popStash(stashId = 'stash@{0}') {
|
|
884
|
+
try {
|
|
885
|
+
const result = await this.git.stash(['pop', stashId]);
|
|
886
|
+
return createSuccessResult(result);
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
return createFailureResult(this.handleGitError(error, `Failed to pop stash '${stashId}'`));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Cherry-pick commits
|
|
894
|
+
*
|
|
895
|
+
* @param commits - Array of commit hashes to cherry-pick
|
|
896
|
+
* @returns Promise resolving to operation result
|
|
897
|
+
*/
|
|
898
|
+
async cherryPick(commits) {
|
|
899
|
+
try {
|
|
900
|
+
if (commits.length === 0) {
|
|
901
|
+
return createSuccessResult('No commits specified for cherry-pick');
|
|
902
|
+
}
|
|
903
|
+
const result = await this.git.raw(['cherry-pick', ...commits]);
|
|
904
|
+
return createSuccessResult(result);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
return createFailureResult(this.handleGitError(error, 'Failed to cherry-pick commits'));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Rebase the current branch
|
|
912
|
+
*
|
|
913
|
+
* @param branch - Branch to rebase onto
|
|
914
|
+
* @param interactive - Whether to use interactive rebase
|
|
915
|
+
* @returns Promise resolving to operation result
|
|
916
|
+
*/
|
|
917
|
+
async rebase(branch, interactive = false) {
|
|
918
|
+
try {
|
|
919
|
+
const args = interactive ? ['-i', branch] : [branch];
|
|
920
|
+
const result = await this.git.rebase(args);
|
|
921
|
+
return createSuccessResult(result);
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
return createFailureResult(this.handleGitError(error, `Failed to rebase onto '${branch}'`));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Reset the repository to a specific commit
|
|
929
|
+
*
|
|
930
|
+
* @param ref - Reference to reset to
|
|
931
|
+
* @param mode - Reset mode (hard, soft, mixed)
|
|
932
|
+
* @returns Promise resolving to operation result
|
|
933
|
+
*/
|
|
934
|
+
async reset(ref = 'HEAD', mode = 'mixed') {
|
|
935
|
+
try {
|
|
936
|
+
const args = [`--${mode}`, ref];
|
|
937
|
+
const result = await this.git.reset(args);
|
|
938
|
+
return createSuccessResult(result);
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
return createFailureResult(this.handleGitError(error, `Failed to reset to '${ref}'`));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Clean the working directory
|
|
946
|
+
*
|
|
947
|
+
* @param directories - Whether to remove directories too
|
|
948
|
+
* @param force - Whether to force clean
|
|
949
|
+
* @returns Promise resolving to operation result
|
|
950
|
+
*/
|
|
951
|
+
async clean(directories = false, force = false) {
|
|
952
|
+
try {
|
|
953
|
+
const args = ['-f'];
|
|
954
|
+
if (directories)
|
|
955
|
+
args.push('-d');
|
|
956
|
+
if (force)
|
|
957
|
+
args.push('-x');
|
|
958
|
+
const result = await this.git.clean(args);
|
|
959
|
+
return createSuccessResult(result);
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
return createFailureResult(this.handleGitError(error, 'Failed to clean working directory'));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|