Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2914ce5
feat(remote): Implement GitHub archive download with git clone fallback
yamadashy Jun 15, 2025
5270bbc
refactor(remote): Use fflate for ZIP extraction and improve GitHub UR…
yamadashy Jun 15, 2025
3f0d125
fix(archive): Improve ZIP extraction error handling and directory cre…
yamadashy Jun 15, 2025
8696b32
fix(github): Address PR review feedback for archive download feature
yamadashy Jun 15, 2025
9e81c89
security(git): Fix URL substring sanitization vulnerability
yamadashy Jun 15, 2025
0ca4f30
feat(remote): Optimize git dependency check timing for archive downloads
yamadashy Jun 15, 2025
70e6822
fix(github): Address additional code review feedback on async handling
yamadashy Jun 15, 2025
6a1980c
perf(archive): Implement concurrent file extraction and memory-effici…
yamadashy Jun 15, 2025
b1dc1ef
docs(claude): Add AI review request commands to CLAUDE.md
yamadashy Jun 15, 2025
148dc80
security(archive): Fix path traversal vulnerability and URI encoding …
yamadashy Jun 15, 2025
328f541
docs(commands): Add PR review request command template
yamadashy Jun 15, 2025
d25c830
refactor(archive): Address comprehensive security and performance imp…
yamadashy Jun 15, 2025
b659d9a
fix(archive): Limit concurrent file writes to prevent EMFILE errors
yamadashy Jun 15, 2025
6651756
docs(claude): Document file concurrency limits to prevent EMFILE errors
yamadashy Jun 15, 2025
cc35e20
fix(archive): Revert to sequential processing to eliminate EMFILE errors
yamadashy Jun 15, 2025
69e8cb7
feat(archive): Implement true streaming ZIP extraction for optimal pe…
yamadashy Jun 15, 2025
cab24c6
fix(git): Remove streaming ZIP extraction implementation
yamadashy Jun 15, 2025
f4f911e
test(git): Add comprehensive test suite for gitHubArchive.ts
yamadashy Jun 15, 2025
9708e1c
perf(test): Optimize gitHubArchive test execution time
yamadashy Jun 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/commands/pr-review-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Please request a review of this pull request from Gemini and Coderabbit.
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,21 @@ This repository uses several automated review tools:
- **Copilot Pull Request Reviewer**: GitHub's automated review suggestions
- **Codecov**: Test coverage analysis and reporting

### Requesting Additional Reviews
You can request additional AI reviews manually:

**CodeRabbit Review Request:**
```
@coderabbitai review
```

**Gemini Review Request:**
```
/gemini review
```

**Important**: Post each review request in separate comments for proper processing.

### Responding to Review Feedback

**1. Address Technical Issues First:**
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"clipboardy": "^4.0.0",
"commander": "^14.0.0",
"fast-xml-parser": "^5.2.0",
"fflate": "^0.8.2",
"git-url-parse": "^16.1.0",
"globby": "^14.1.0",
"handlebars": "^4.7.8",
Expand All @@ -98,12 +99,12 @@
"zod": "^3.24.3"
},
"devDependencies": {
"git-up": "^8.1.1",
"@secretlint/types": "^9.3.4",
"@biomejs/biome": "^1.9.4",
"@secretlint/types": "^9.3.4",
"@types/node": "^22.14.1",
"@types/strip-comments": "^2.0.4",
"@vitest/coverage-v8": "^3.1.1",
"git-up": "^8.1.1",
"rimraf": "^6.0.1",
"secretlint": "^9.3.1",
"tsx": "^4.19.4",
Expand Down
107 changes: 94 additions & 13 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import path from 'node:path';
import pc from 'picocolors';
import { execGitShallowClone } from '../../core/git/gitCommand.js';
import { downloadGitHubArchive, isArchiveDownloadSupported } from '../../core/git/gitHubArchive.js';
import { getRemoteRefs } from '../../core/git/gitRemoteHandle.js';
import { parseRemoteValue } from '../../core/git/gitRemoteParse.js';
import { isGitHubRepository, parseGitHubRepoInfo, parseRemoteValue } from '../../core/git/gitRemoteParse.js';
Comment thread
yamadashy marked this conversation as resolved.
import { isGitInstalled } from '../../core/git/gitRepositoryHandle.js';
import { RepomixError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';
Expand All @@ -20,12 +21,103 @@
execGitShallowClone,
getRemoteRefs,
runDefaultAction,
downloadGitHubArchive,
isGitHubRepository,
parseGitHubRepoInfo,
isArchiveDownloadSupported,
},
): Promise<DefaultActionRunnerResult> => {
let tempDirPath = await createTempDirectory();
let result: DefaultActionRunnerResult;
let downloadMethod: 'archive' | 'git' = 'git';

try {
// Check if this is a GitHub repository and archive download is supported
const githubRepoInfo = deps.parseGitHubRepoInfo(repoUrl);
const shouldTryArchive = githubRepoInfo && deps.isArchiveDownloadSupported(githubRepoInfo);

if (shouldTryArchive) {
// Try GitHub archive download first
const spinner = new Spinner('Downloading repository archive...', cliOptions);

try {
spinner.start();

// Override ref with CLI option if provided
const repoInfoWithBranch = {
...githubRepoInfo,
ref: cliOptions.remoteBranch ?? githubRepoInfo.ref,
};

await deps.downloadGitHubArchive(
repoInfoWithBranch,
tempDirPath,
{
timeout: 60000, // 1 minute timeout for large repos
retries: 2,
},
(progress) => {
if (progress.percentage !== null) {
spinner.update(`Downloading repository archive... (${progress.percentage}%)`);
} else {

Check warning on line 62 in src/cli/actions/remoteAction.ts

View check run for this annotation

Codecov / codecov/patch

src/cli/actions/remoteAction.ts#L60-L62

Added lines #L60 - L62 were not covered by tests
// Show downloaded bytes when percentage is not available
const downloadedMB = (progress.downloaded / 1024 / 1024).toFixed(1);
spinner.update(`Downloading repository archive... (${downloadedMB} MB)`);
}
},

Check warning on line 67 in src/cli/actions/remoteAction.ts

View check run for this annotation

Codecov / codecov/patch

src/cli/actions/remoteAction.ts#L64-L67

Added lines #L64 - L67 were not covered by tests
);

downloadMethod = 'archive';
spinner.succeed('Repository archive downloaded successfully!');
logger.log('');
} catch (archiveError) {
spinner.fail('Archive download failed, trying git clone...');
logger.trace('Archive download error:', (archiveError as Error).message);

// Clear the temp directory for git clone attempt
await cleanupTempDirectory(tempDirPath);
tempDirPath = await createTempDirectory();

// Fall back to git clone
await performGitClone(repoUrl, tempDirPath, cliOptions, deps);
downloadMethod = 'git';
}
} else {
// Use git clone directly
await performGitClone(repoUrl, tempDirPath, cliOptions, deps);
downloadMethod = 'git';
}

// Run the default action on the downloaded/cloned repository
result = await deps.runDefaultAction([tempDirPath], tempDirPath, cliOptions);
await copyOutputToCurrentDirectory(tempDirPath, process.cwd(), result.config.output.filePath);

logger.trace(`Repository obtained via ${downloadMethod} method`);
} finally {
// Cleanup the temporary directory
await cleanupTempDirectory(tempDirPath);
}

return result;
};

/**
* Performs git clone operation with spinner and error handling
*/
const performGitClone = async (
repoUrl: string,
tempDirPath: string,
cliOptions: CliOptions,
deps: {
isGitInstalled: typeof isGitInstalled;
getRemoteRefs: typeof getRemoteRefs;
execGitShallowClone: typeof execGitShallowClone;
},
): Promise<void> => {
// Check if git is installed only when we actually need to use git
if (!(await deps.isGitInstalled())) {
throw new RepomixError('Git is not installed or not in the system PATH.');
}

// Get remote refs
let refs: string[] = [];
try {
Expand All @@ -39,8 +131,6 @@
const parsedFields = parseRemoteValue(repoUrl, refs);

const spinner = new Spinner('Cloning repository...', cliOptions);
const tempDirPath = await createTempDirectory();
let result: DefaultActionRunnerResult;

try {
spinner.start();
Expand All @@ -52,19 +142,10 @@

spinner.succeed('Repository cloned successfully!');
logger.log('');

// Run the default action on the cloned repository
result = await deps.runDefaultAction([tempDirPath], tempDirPath, cliOptions);
await copyOutputToCurrentDirectory(tempDirPath, process.cwd(), result.config.output.filePath);
} catch (error) {
spinner.fail('Error during repository cloning. cleanup...');
throw error;
} finally {
// Cleanup the temporary directory
await cleanupTempDirectory(tempDirPath);
}

return result;
};

export const createTempDirectory = async (): Promise<string> => {
Expand Down
Loading
Loading