diff --git a/packages/git/src/subcommands/__exports__.js b/packages/git/src/subcommands/__exports__.js index 5082c2cf..05024347 100644 --- a/packages/git/src/subcommands/__exports__.js +++ b/packages/git/src/subcommands/__exports__.js @@ -19,6 +19,7 @@ // Generated by /tools/gen.js import module_add from './add.js' import module_branch from './branch.js' +import module_checkout from './checkout.js' import module_clone from './clone.js' import module_commit from './commit.js' import module_config from './config.js' @@ -34,6 +35,7 @@ import module_version from './version.js' export default { "add": module_add, "branch": module_branch, + "checkout": module_checkout, "clone": module_clone, "commit": module_commit, "config": module_config, diff --git a/packages/git/src/subcommands/checkout.js b/packages/git/src/subcommands/checkout.js new file mode 100644 index 00000000..9c1f274a --- /dev/null +++ b/packages/git/src/subcommands/checkout.js @@ -0,0 +1,155 @@ +/* + * 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 { find_repo_root } from '../git-helpers.js'; +import { SHOW_USAGE } from '../help.js'; + +const CHECKOUT = { + name: 'checkout', + usage: [ + 'git checkout [--force] ', + 'git checkout (-b | -B) [--force] []', + ], + description: `Switch branches.`, + args: { + allowPositionals: true, + tokens: true, + strict: false, + options: { + 'new-branch': { + description: 'Create a new branch and then check it out.', + type: 'boolean', + short: 'b', + default: false, + }, + 'force': { + description: 'Perform the checkout forcefully. For --new-branch, ignores whether the branch already exists. For checking out branches, ignores and overwrites any unstaged changes.', + type: 'boolean', + short: 'f', + }, + }, + }, + execute: async (ctx) => { + const { io, fs, env, args } = ctx; + const { stdout, stderr } = io; + const { options, positionals, tokens } = args; + const cache = {}; + + for (const token of tokens) { + if (token.kind !== 'option') continue; + + if (token.name === 'B') { + options['new-branch'] = true; + options.force = true; + delete options['B']; + continue; + } + + // Report any options that we don't recognize + let option_recognized = false; + for (const [key, value] of Object.entries(CHECKOUT.args.options)) { + if (key === token.name || value.short === token.name) { + option_recognized = true; + break; + } + } + if (!option_recognized) { + stderr(`Unrecognized option: ${token.rawName}`); + throw SHOW_USAGE; + } + } + + const { repository_dir, git_dir } = await find_repo_root(fs, env.PWD); + + // DRY: Copied from branch.js + const get_current_branch = async () => git.currentBranch({ + fs, + dir: repository_dir, + gitdir: git_dir, + test: true, + }); + const get_all_branches = async () => git.listBranches({ + fs, + dir: repository_dir, + gitdir: git_dir, + }); + const get_branch_data = async () => { + const [branches, current_branch] = await Promise.all([ + get_all_branches(), + get_current_branch(), + ]); + return { branches, current_branch }; + } + + if (options['new-branch']) { + const { branches, current_branch } = await get_branch_data(); + if (positionals.length === 0 || positionals.length > 2) { + stderr('error: Expected 1 or 2 arguments, for [].'); + throw SHOW_USAGE; + } + const branch_name = positionals.shift(); + const starting_point = positionals.shift() ?? current_branch; + + if (branches.includes(branch_name) && !options.force) + throw new Error(`A branch named '${branch_name}' already exists.`); + + await git.branch({ + fs, + dir: repository_dir, + gitdir: git_dir, + ref: branch_name, + object: starting_point, + checkout: true, + force: options.force, + }); + stdout(`Switched to a new branch '${branch_name}'`); + return; + } + + // Check out a branch + // TODO: Check out files. + { + if (positionals.length === 0 || positionals.length > 1) { + stderr('error: Expected 1 argument, for .'); + throw SHOW_USAGE; + } + const { branches, current_branch } = await get_branch_data(); + const branch_name = positionals.shift(); + + if (branch_name === current_branch) { + stdout(`Already on '${branch_name}'`); + return; + } + + if (!branches.includes(branch_name)) + throw new Error(`Branch '${branch_name}' not found.`); + + await git.checkout({ + fs, + dir: repository_dir, + gitdir: git_dir, + cache, + ref: branch_name, + force: options.force, + }); + stdout(`Switched to branch '${branch_name}'`); + } + } +}; +export default CHECKOUT;