Skip to content

Commit 35e4453

Browse files
AtkinsSJKernelDeimos
authored andcommitted
feat(git): Implement git checkout
For now this only lets you check out branches, not files.
1 parent 98a4b9e commit 35e4453

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

packages/git/src/subcommands/__exports__.js

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
// Generated by /tools/gen.js
2020
import module_add from './add.js'
2121
import module_branch from './branch.js'
22+
import module_checkout from './checkout.js'
2223
import module_clone from './clone.js'
2324
import module_commit from './commit.js'
2425
import module_config from './config.js'
@@ -34,6 +35,7 @@ import module_version from './version.js'
3435
export default {
3536
"add": module_add,
3637
"branch": module_branch,
38+
"checkout": module_checkout,
3739
"clone": module_clone,
3840
"commit": module_commit,
3941
"config": module_config,
+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (C) 2024 Puter Technologies Inc.
3+
*
4+
* This file is part of Puter's Git client.
5+
*
6+
* Puter's Git client is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
import git from 'isomorphic-git';
20+
import { find_repo_root } from '../git-helpers.js';
21+
import { SHOW_USAGE } from '../help.js';
22+
23+
const CHECKOUT = {
24+
name: 'checkout',
25+
usage: [
26+
'git checkout [--force] <branch>',
27+
'git checkout (-b | -B) [--force] <new-branch> [<start-point>]',
28+
],
29+
description: `Switch branches.`,
30+
args: {
31+
allowPositionals: true,
32+
tokens: true,
33+
strict: false,
34+
options: {
35+
'new-branch': {
36+
description: 'Create a new branch and then check it out.',
37+
type: 'boolean',
38+
short: 'b',
39+
default: false,
40+
},
41+
'force': {
42+
description: 'Perform the checkout forcefully. For --new-branch, ignores whether the branch already exists. For checking out branches, ignores and overwrites any unstaged changes.',
43+
type: 'boolean',
44+
short: 'f',
45+
},
46+
},
47+
},
48+
execute: async (ctx) => {
49+
const { io, fs, env, args } = ctx;
50+
const { stdout, stderr } = io;
51+
const { options, positionals, tokens } = args;
52+
const cache = {};
53+
54+
for (const token of tokens) {
55+
if (token.kind !== 'option') continue;
56+
57+
if (token.name === 'B') {
58+
options['new-branch'] = true;
59+
options.force = true;
60+
delete options['B'];
61+
continue;
62+
}
63+
64+
// Report any options that we don't recognize
65+
let option_recognized = false;
66+
for (const [key, value] of Object.entries(CHECKOUT.args.options)) {
67+
if (key === token.name || value.short === token.name) {
68+
option_recognized = true;
69+
break;
70+
}
71+
}
72+
if (!option_recognized) {
73+
stderr(`Unrecognized option: ${token.rawName}`);
74+
throw SHOW_USAGE;
75+
}
76+
}
77+
78+
const { repository_dir, git_dir } = await find_repo_root(fs, env.PWD);
79+
80+
// DRY: Copied from branch.js
81+
const get_current_branch = async () => git.currentBranch({
82+
fs,
83+
dir: repository_dir,
84+
gitdir: git_dir,
85+
test: true,
86+
});
87+
const get_all_branches = async () => git.listBranches({
88+
fs,
89+
dir: repository_dir,
90+
gitdir: git_dir,
91+
});
92+
const get_branch_data = async () => {
93+
const [branches, current_branch] = await Promise.all([
94+
get_all_branches(),
95+
get_current_branch(),
96+
]);
97+
return { branches, current_branch };
98+
}
99+
100+
if (options['new-branch']) {
101+
const { branches, current_branch } = await get_branch_data();
102+
if (positionals.length === 0 || positionals.length > 2) {
103+
stderr('error: Expected 1 or 2 arguments, for <new-branch> [<start-point>].');
104+
throw SHOW_USAGE;
105+
}
106+
const branch_name = positionals.shift();
107+
const starting_point = positionals.shift() ?? current_branch;
108+
109+
if (branches.includes(branch_name) && !options.force)
110+
throw new Error(`A branch named '${branch_name}' already exists.`);
111+
112+
await git.branch({
113+
fs,
114+
dir: repository_dir,
115+
gitdir: git_dir,
116+
ref: branch_name,
117+
object: starting_point,
118+
checkout: true,
119+
force: options.force,
120+
});
121+
stdout(`Switched to a new branch '${branch_name}'`);
122+
return;
123+
}
124+
125+
// Check out a branch
126+
// TODO: Check out files.
127+
{
128+
if (positionals.length === 0 || positionals.length > 1) {
129+
stderr('error: Expected 1 argument, for <branch>.');
130+
throw SHOW_USAGE;
131+
}
132+
const { branches, current_branch } = await get_branch_data();
133+
const branch_name = positionals.shift();
134+
135+
if (branch_name === current_branch) {
136+
stdout(`Already on '${branch_name}'`);
137+
return;
138+
}
139+
140+
if (!branches.includes(branch_name))
141+
throw new Error(`Branch '${branch_name}' not found.`);
142+
143+
await git.checkout({
144+
fs,
145+
dir: repository_dir,
146+
gitdir: git_dir,
147+
cache,
148+
ref: branch_name,
149+
force: options.force,
150+
});
151+
stdout(`Switched to branch '${branch_name}'`);
152+
}
153+
}
154+
};
155+
export default CHECKOUT;

0 commit comments

Comments
 (0)