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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ repomix --remote https://github.com/yamadashy/repomix --remote-branch main

# Or use a specific commit hash:
repomix --remote https://github.com/yamadashy/repomix --remote-branch 935b695

# Another convenient way is specifying the branch's URL
repomix --remote https://github.com/yamadashy/repomix/tree/main

# Commit's URL is also supported
repomix --remote https://github.com/yamadashy/repomix/commit/836abcd7335137228ad77feb28655d85712680f1

```

To initialize a new configuration file (`repomix.config.json`):
Expand Down Expand Up @@ -470,13 +477,21 @@ repomix --remote yamadashy/repomix
You can specify the branch name, tag, or commit hash:

```bash
# Using --remote-branch option
repomix --remote https://github.com/yamadashy/repomix --remote-branch main

# Using branch's URL
repomix --remote https://github.com/yamadashy/repomix/tree/main
```

Or use a specific commit hash:

```bash
# Using --remote-branch option
repomix --remote https://github.com/yamadashy/repomix --remote-branch 935b695

# Using commit's URL
repomix --remote https://github.com/yamadashy/repomix/commit/836abcd7335137228ad77feb28655d85712680f1
```

## ⚙️ Configuration
Expand Down
71 changes: 71 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"clipboardy": "^4.0.0",
"commander": "^13.1.0",
"fast-xml-parser": "^4.5.1",
"git-url-parse": "^16.0.0",
"globby": "^14.0.2",
"handlebars": "^4.7.8",
"iconv-lite": "^0.6.3",
Expand All @@ -85,6 +86,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/git-url-parse": "^9.0.3",
"@types/node": "^22.13.0",
"@types/strip-comments": "^2.0.4",
"@vitest/coverage-v8": "^3.0.5",
Expand Down
92 changes: 60 additions & 32 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import GitUrlParse, { type GitUrl } from 'git-url-parse';
import pc from 'picocolors';
import { execGitShallowClone, isGitInstalled } from '../../core/file/gitCommand.js';
import { RepomixError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';
import type { CliOptions } from '../cliRun.js';
import Spinner from '../cliSpinner.js';
import { type DefaultActionRunnerResult, runDefaultAction } from './defaultAction.js';

// Check the short form of the GitHub URL. e.g. yamadashy/repomix
const remoteNamePattern = '[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?';
const remoteNamePatternRegex = new RegExp(`^${remoteNamePattern}/${remoteNamePattern}$`);

interface IGitUrl extends GitUrl {
commit: string | undefined;
}
export const runRemoteAction = async (
repoUrl: string,
options: CliOptions,
Expand All @@ -26,20 +25,16 @@ 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 parsedFields = parseRemoteValue(repoUrl);
const spinner = new Spinner('Cloning repository...');

const tempDirPath = await createTempDirectory();
let result: DefaultActionRunnerResult;

try {
spinner.start();

// Clone the repository
await cloneRepository(formatRemoteValueToUrl(repoUrl), tempDirPath, options.remoteBranch, {
await cloneRepository(parsedFields.repoUrl, tempDirPath, options.remoteBranch || parsedFields.remoteBranch, {
execGitShallowClone: deps.execGitShallowClone,
});

Expand All @@ -60,34 +55,67 @@ export const runRemoteAction = async (
return result;
};

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

export const parseRemoteValue = (remoteValue: string): { repoUrl: string; remoteBranch: string | undefined } => {
if (isValidShorthand(remoteValue)) {
logger.trace(`Formatting GitHub shorthand: ${remoteValue}`);
return {
repoUrl: `https://github.com/${remoteValue}.git`,
remoteBranch: undefined,
};
}

// 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 parsedFields = GitUrlParse(remoteValue) as IGitUrl;

// This will make parsedFields.toString() automatically append '.git' to the returned url
parsedFields.git_suffix = true;

const ownerSlashRepo =
parsedFields.full_name.split('/').length > 1 ? parsedFields.full_name.split('/').slice(-2).join('/') : '';

if (ownerSlashRepo !== '' && !isValidShorthand(ownerSlashRepo)) {
throw new RepomixError('Invalid owner/repo in repo URL');
}

const repoUrl = parsedFields.toString(parsedFields.protocol);

if (parsedFields.ref) {
return {
repoUrl: repoUrl,
remoteBranch: parsedFields.filepath ? `${parsedFields.ref}/${parsedFields.filepath}` : parsedFields.ref,
};
}

if (parsedFields.commit) {
return {
repoUrl: repoUrl,
remoteBranch: parsedFields.commit,
};
}

return {
repoUrl: repoUrl,
remoteBranch: undefined,
};
} catch (error) {
return false;
}
}

export const formatRemoteValueToUrl = (url: string): string => {
// If the URL is in the format owner/repo, convert it to a GitHub URL
if (remoteNamePatternRegex.test(url)) {
logger.trace(`Formatting GitHub shorthand: ${url}`);
return `https://github.com/${url}.git`;
throw new RepomixError('Invalid remote repository URL or repository shorthand (owner/repo)');
}
};

// Add .git to HTTPS URLs if missing
if (url.startsWith('https://') && !url.endsWith('.git')) {
logger.trace(`Adding .git to HTTPS URL: ${url}`);
return `${url}.git`;
export const isValidRemoteValue = (remoteValue: string): boolean => {
try {
parseRemoteValue(remoteValue);
return true;
} catch (error) {
return false;
}

return url;
};

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