diff --git a/packages/git/src/git-helpers.js b/packages/git/src/git-helpers.js index 87e008622..ba4b62ba6 100644 --- a/packages/git/src/git-helpers.js +++ b/packages/git/src/git-helpers.js @@ -18,6 +18,8 @@ */ import path from 'path-browserify'; +export const PROXY_URL = 'https://cors.isomorphic-git.org'; + /** * Attempt to locate the git repository directory. * @throws Error If no git repository could be found, or another error occurred. diff --git a/packages/git/src/subcommands/__exports__.js b/packages/git/src/subcommands/__exports__.js index 789c592a6..e9caea944 100644 --- a/packages/git/src/subcommands/__exports__.js +++ b/packages/git/src/subcommands/__exports__.js @@ -18,6 +18,7 @@ */ // Generated by /tools/gen.js import module_add from './add.js' +import module_clone from './clone.js' import module_commit from './commit.js' import module_config from './config.js' import module_help from './help.js' @@ -29,6 +30,7 @@ import module_version from './version.js' export default { "add": module_add, + "clone": module_clone, "commit": module_commit, "config": module_config, "help": module_help, diff --git a/packages/git/src/subcommands/clone.js b/packages/git/src/subcommands/clone.js new file mode 100644 index 000000000..7a81b4945 --- /dev/null +++ b/packages/git/src/subcommands/clone.js @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Puter's Git client. + * + * Puter's Git client is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import git from 'isomorphic-git'; +import http from 'isomorphic-git/http/web'; +import { PROXY_URL } from '../git-helpers.js'; +import { SHOW_USAGE } from '../help.js'; +import path from 'path-browserify'; + +export default { + name: 'clone', + usage: 'git clone []', + description: 'Clone a repository into a new directory.', + args: { + allowPositionals: true, + options: { + depth: { + description: 'Only clone the specified number of commits. Implies --single-branch unless --no-single-branch is given.', + type: 'string', + }, + 'single-branch': { + description: 'Only clone the history of the primary branch', + type: 'boolean', + default: false, + }, + 'no-single-branch': { + description: 'Clone all history (default)', + type: 'boolean', + }, + 'no-tags': { + description: 'Do not clone any tags from the remote', + type: 'boolean', + default: false, + }, + }, + }, + execute: async (ctx) => { + const { io, fs, env, args } = ctx; + const { stdout, stderr } = io; + const { options, positionals } = args; + + if (options.depth) { + const depth = Number.parseInt(options.depth); + if (!depth) { + stderr('Invalid --depth: Must be an integer greater than 0.'); + return 1; + } + options.depth = depth; + options['single-branch'] = true; + } + + if (options['no-single-branch']) { + options['single-branch'] = false; + delete options['no-single-branch']; + } + + const [repository, directory] = positionals; + if (!repository) { + stderr('fatal: You must specify a repository to clone.'); + throw SHOW_USAGE; + } + + let repo_path; + if (directory) { + repo_path = path.resolve(env.PWD, directory); + } else { + // Try to extract directory from the repository url + let repo_name = repository.slice(repository.lastIndexOf('/') + 1); + if (repo_name.endsWith('.git')) { + repo_name = repo_name.slice(0, -4); + } + + repo_path = path.resolve(env.PWD, repo_name); + } + + // The path must either not exist, or be a directory that is empty + try { + const readdir = await fs.promises.readdir(repo_path); + if (readdir.length !== 0) { + stderr(`fatal: ${repo_path} is not empty.`); + return 1; + } + } catch (e) { + if (e.code !== 'ENOENT') { + stderr(`fatal: ${repo_path} is a file.`); + return 1; + } + } + + stdout(`Cloning into '${path.relative(env.PWD, repo_path)}'...`); + + await git.clone({ + fs, + http, + corsProxy: PROXY_URL, + dir: repo_path, + url: repository, + depth: options.depth, + singleBranch: options['single-branch'], + noTags: options['no-tags'], + onMessage: (message) => { stdout(message); }, + }); + } +}