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); },
+ });
+ }
+}