Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const runRemoteAction = async (
throw new RepomixError('Git is not installed or not in the system PATH.');
}

if (!isValidRemoteValue(repoUrl)) {
throw new RepomixError('Invalid repository URL or user/repo format');
}

const spinner = new Spinner('Cloning repository...');

const tempDirPath = await createTempDirectory();
Expand Down Expand Up @@ -111,3 +115,20 @@ export const copyOutputToCurrentDirectory = async (
throw new RepomixError(`Failed to copy output file: ${(error as Error).message}`);
}
};

export function isValidRemoteValue(remoteValue: string): boolean {
// Check the short form of the GitHub URL. e.g. yamadashy/repomix
const namePattern = '[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?';
const shortFormRegex = new RegExp(`^${namePattern}/${namePattern}$`);
if (shortFormRegex.test(remoteValue)) {
return true;
}

// Check the direct form of the GitHub URL. e.g. https://github.com/yamadashy/repomix or https://gist.github.com/yamadashy/1234567890abcdef
try {
new URL(remoteValue);
return true;
} catch (error) {
return false;
}
Comment on lines +127 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve URL validation for Git repositories.

The current URL validation accepts any valid URL. Consider adding specific checks for Git repository URLs.

-  // Check the direct form of the GitHub URL. e.g.  https://github.com/yamadashy/repomix or https://gist.github.com/yamadashy/1234567890abcdef
   try {
-    new URL(remoteValue);
-    return true;
+    const url = new URL(remoteValue);
+    // Check if the URL is a valid Git repository URL
+    const isGitUrl = /^https?:\/\/([^/]+\/){2}/.test(url.toString()) || // Has at least two path segments
+                     /^git@[^:]+:.+\/.+$/.test(remoteValue);           // SSH format
+    return isGitUrl;
   } catch (error) {
     return false;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check the direct form of the GitHub URL. e.g. https://github.com/yamadashy/repomix or https://gist.github.com/yamadashy/1234567890abcdef
try {
new URL(remoteValue);
return true;
} catch (error) {
return false;
}
try {
const url = new URL(remoteValue);
// Check if the URL is a valid Git repository URL
const isGitUrl = /^https?:\/\/([^/]+\/){2}/.test(url.toString()) || // Has at least two path segments
/^git@[^:]+:.+\/.+$/.test(remoteValue); // SSH format
return isGitUrl;
} catch (error) {
return false;
}

}
24 changes: 6 additions & 18 deletions src/core/file/gitCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,11 @@ import { execFile } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { RepomixError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';

const execFileAsync = promisify(execFile);

export function isValidRemoteUrl(url: string): boolean {
// Check the short form of the GitHub URL. e.g. yamadashy/repomix
const shortFormRegex = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
if (shortFormRegex.test(url)) {
return true;
}

// Check the direct form of the GitHub URL. e.g. https://github.com/yamadashy/repomix or https://gist.github.com/yamadashy/1234567890abcdef
try {
new URL(url);
return true;
} catch (error) {
return false;
}
}

export const isGitInstalled = async (
deps = {
execFileAsync,
Expand All @@ -44,8 +29,11 @@ export const execGitShallowClone = async (
execFileAsync,
},
) => {
if (!isValidRemoteUrl(url)) {
throw new Error('Invalid repository URL or user/repo format');
// Check if the URL is valid
try {
new URL(url);
} catch (error) {
throw new RepomixError(`Invalid repository URL. Please provide a valid URL. url: ${url}`);
}

if (remoteBranch) {
Expand Down
17 changes: 12 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
export type { RepomixConfigFile as RepomixConfig } from './config/configSchema.js';

export { pack } from './core/packager.js';

// CLI
export { run as cli } from './cli/cliRun.js';
export type { CliOptions } from './cli/cliRun.js';

// Config
export type { RepomixConfigFile as RepomixConfig } from './config/configSchema.js';

// Init action
export { runInitAction } from './cli/actions/initAction.js';
export { runRemoteAction } from './cli/actions/remoteAction.js';

// Default action
export { runDefaultAction } from './cli/actions/defaultAction.js';
export { pack } from './core/packager.js';

// Remote action
export { runRemoteAction } from './cli/actions/remoteAction.js';
export { isValidRemoteValue } from './cli/actions/remoteAction.js';
79 changes: 78 additions & 1 deletion tests/cli/actions/remoteAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { copyOutputToCurrentDirectory, formatGitUrl, runRemoteAction } from '../../../src/cli/actions/remoteAction.js';
import {
copyOutputToCurrentDirectory,
formatGitUrl,
isValidRemoteValue,
runRemoteAction,
} from '../../../src/cli/actions/remoteAction.js';

vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>();
Expand Down Expand Up @@ -76,4 +81,76 @@ describe('remoteAction functions', () => {
);
});
});

describe('isValidRemoteValue', () => {
describe('GitHub shorthand format (user/repo)', () => {
test('should accept valid repository names', () => {
// Test cases for valid repository names with various allowed characters
const validUrls = [
'user/repo',
'user123/repo-name',
'org-name/repo_name',
'user.name/repo.test',
'user_name/repo_test',
'a/b', // Minimum length case
'user-name123/repo-test123.sub_123', // Complex case
];

for (const url of validUrls) {
expect(isValidRemoteValue(url), `URL should be valid: ${url}`).toBe(true);
}
});

test('should reject invalid repository names', () => {
// Test cases for invalid patterns and disallowed characters
const invalidUrls = [
'', // Empty string
'user', // Missing slash
'/repo', // Missing username
'user/', // Missing repository name
'-user/repo', // Starts with hyphen
'user/-repo', // Repository starts with hyphen
'user./repo', // Username ends with dot
'user/repo.', // Repository ends with dot
'user/repo#branch', // Contains invalid character
'user/repo/extra', // Extra path segment
'us!er/repo', // Contains invalid character
'user/re*po', // Contains invalid character
'user//repo', // Double slash
'.user/repo', // Starts with dot
'user/.repo', // Repository starts with dot
];

for (const url of invalidUrls) {
expect(isValidRemoteValue(url), `URL should be invalid: ${url}`).toBe(false);
}
});
});

describe('Full URL format', () => {
test('should accept valid URLs', () => {
// Test cases for standard URL formats
const validUrls = [
'https://example.com',
'http://localhost',
'https://github.com/user/repo',
'https://gitlab.com/user/repo',
'https://domain.com/path/to/something',
];

for (const url of validUrls) {
expect(isValidRemoteValue(url), `URL should be valid: ${url}`).toBe(true);
}
});

test('should reject invalid URLs', () => {
// Test cases for malformed URLs
const invalidUrls = ['not-a-url', 'http://', 'https://', '://no-protocol.com', 'http://[invalid]'];

for (const url of invalidUrls) {
expect(isValidRemoteValue(url), `URL should be invalid: ${url}`).toBe(false);
}
});
});
});
});