Skip to content

Instantly share code, notes, and snippets.

@thoroc
Last active February 12, 2025 11:48
Show Gist options
  • Save thoroc/8aafb33f7d94154b118922bca0d2aa3c to your computer and use it in GitHub Desktop.
Save thoroc/8aafb33f7d94154b118922bca0d2aa3c to your computer and use it in GitHub Desktop.
Git clone script in Deno/Typescript
#!/usr/bin/env -S deno run --allow-env --allow-read --allow-write --allow-run
import { Command } from 'jsr:@cliffy/command@^1.0.0-rc.7';
import { Confirm } from 'jsr:@cliffy/[email protected]/confirm';
import { exists } from 'jsr:@std/fs';
import chalk from 'npm:chalk';
import {
cloneRepo,
fetchRepo,
findExecutablePath,
getLocalPath,
runExecutable,
} from './utils.ts';
interface CloneOptions {
openVsCode?: boolean;
root?: string;
}
const cloneAction = async (options: CloneOptions, repo: string) => {
const homeDir = Deno.env.get('HOME');
const targetBaseDir = options.root || `${homeDir}/Projects`;
const localRepo = getLocalPath(repo, targetBaseDir);
console.log(
`Cloning repository: ${chalk.green(repo)} to ${chalk.green(localRepo)}`
);
const dirAlreadyExists = await exists(localRepo);
if (dirAlreadyExists) {
await fetchRepo(localRepo);
} else {
await cloneRepo(repo, localRepo);
}
const openVsCode =
options.openVsCode ??
(await Confirm.prompt('Open the repository in VS Code?'));
if (openVsCode) {
const vscode = await findExecutablePath('code');
runExecutable(vscode, [localRepo]);
}
console.log(`Move to the project's directory: cd ${chalk.green(localRepo)}`);
};
await new Command()
.name('clone')
.version('0.1.1')
.description('Clone a Git repository into the ~/Projects directory.')
.arguments('<repo:string>')
.option('-r, --root <dir>', 'The root directory.')
.option('-o, --open-vs-code', 'Open the repository in VS Code.', {
default: false,
})
.action(cloneAction)
.parse(Deno.args);

git-mirror

This was now moved to: https://github.com/thoroc/git-mirror

Deno script to clone a github/gitlab repo to ~/Projects while keeping a tree structure close to the remote url. If the project is already present, then it'll fetch from the remote.

WARNING this was developed on a MacOS, so no guaranty are offered to run on a different OS.

install

  1. Copy the scripts to local dir (~/.config/script/git-mirror.ts for example). Don't forget the utils.ts file.
  2. add the following alias to your .gitconfig:
[alias]
  mirror = "!Deno run --allow-run --allow-read --allow-env ~/.config/scripts/git-mirror.ts"

Usage

Call git mirror [email protected]:owner/repo.git will clone to ~/Projects/owner/repo.

Options:

  • -o to open VsCode (default: false)
  • -r to specify a different root (default: ~/Projects)
import chalk from 'npm:chalk';
export const getHostFromRepo = (repo: string): string => {
let host: string;
if (repo.startsWith('git@')) {
const repoParts = repo.split(':');
host = repoParts[0].split('@')[1].split('.')[0];
} else if (repo.startsWith('https://')) {
const repoParts = repo.split('/');
host = repoParts[2].split('.')[0];
} else {
console.error(chalk.bgRedBright(`Invalid Git repository URL: ${repo}`));
Deno.exit(1);
}
return host;
};
export const getDirPathFromRepo = (repo: string): string => {
let dirPath: string;
if (repo.startsWith('git@')) {
const repoParts = repo.split(':');
dirPath = repoParts[1].replace('.git', '');
} else if (repo.startsWith('https://')) {
const repoParts = repo.split('/');
dirPath = repoParts.slice(3).join('/').replace('.git', '');
} else {
console.error(chalk.bgRedBright(`Invalid Git repository URL: ${repo}`));
Deno.exit(1);
}
return dirPath;
};
export const getLocalPath = (repo: string, targetBaseDir: string): string => {
const host = getHostFromRepo(repo);
const dirPath = getDirPathFromRepo(repo);
const targetPath = `${targetBaseDir}/${host}/${dirPath}`;
// find all sequential directories that exist
const dirs = targetPath.split('/');
const remainingPath: string[] = [];
for (const dir of dirs) {
if (!remainingPath.includes(dir)) {
remainingPath.push(dir);
}
}
return remainingPath.join('/');
};
interface ShortenOptions {
separator?: string;
min?: number;
left?: number;
right?: number;
}
export const shortenPath = (path: string, options?: ShortenOptions): string => {
const min = options?.min ?? 3;
const separator = options?.separator ?? '/';
const parts = path.split(separator);
if (parts.length <= min) {
return path;
}
const left = options?.left ?? 1;
const first = parts.slice(0, left + 1).join('/');
const right = options?.right ?? 2;
const last = parts.slice(-right).join('/');
return `${first}/.../${last}`;
};
export const findExecutablePath = async (
executable: string
): Promise<string> => {
try {
const command = await new Deno.Command('which', {
args: [executable],
stdout: 'piped',
});
const { stdout } = await command.output();
return new TextDecoder().decode(stdout).trim();
} catch (error) {
console.error(chalk.bgRedBright(`Error finding git path: ${error}`));
Deno.exit(1);
}
};
export const runExecutable = async (
executable: string,
args: string[]
): Promise<Deno.CommandStatus> => {
const command = await new Deno.Command(executable, {
args,
stdin: 'piped',
});
const child = await command.spawn();
const status = await child.status;
return status;
};
export const fetchRepo = async (localRepo: string): Promise<void> => {
const git = await findExecutablePath('git');
const pull = new Deno.Command(git, {
cwd: localRepo,
args: ['fetch', 'origin'],
stdin: 'piped',
});
const child = await pull.spawn();
const status = await child.status;
if (!status.success) {
console.error(chalk.bgRedBright(`Error fetching repository`));
} else {
console.log(chalk.bgGreenBright(`Repository fetching successfully`));
}
};
export const cloneRepo = async (
repo: string,
localRepo: string
): Promise<void> => {
const git = await findExecutablePath('git');
const clone = new Deno.Command(git, {
args: ['clone', repo, localRepo],
stdin: 'piped',
});
const child = await clone.spawn();
const status = await child.status;
if (!status.success) {
console.error(chalk.bgRedBright(`Error cloning repository`));
} else {
console.log(
chalk.bgGreenBright(`Repository cloned successfully to ${localRepo}`)
);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment