Skip to content

Commit 4ba8a32

Browse files
AtkinsSJKernelDeimos
authored andcommitted
feat(git): Implement git restore
I tried implementing --source=foo too, but was bumping into weird behaviour with isomorphic-git, so I've removed that for now. Eventually we'll want it, but it seems fairly niche.
1 parent a680371 commit 4ba8a32

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

packages/git/src/subcommands/__exports__.js

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import module_log from './log.js'
3131
import module_pull from './pull.js'
3232
import module_push from './push.js'
3333
import module_remote from './remote.js'
34+
import module_restore from './restore.js'
3435
import module_show from './show.js'
3536
import module_status from './status.js'
3637
import module_version from './version.js'
@@ -50,6 +51,7 @@ export default {
5051
"pull": module_pull,
5152
"push": module_push,
5253
"remote": module_remote,
54+
"restore": module_restore,
5355
"show": module_show,
5456
"status": module_status,
5557
"version": module_version,
+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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, { STAGE, TREE, WORKDIR } from 'isomorphic-git';
20+
import { find_repo_root } from '../git-helpers.js';
21+
import path from 'path-browserify';
22+
23+
export default {
24+
name: 'restore',
25+
usage: 'git restore [--staged] [--worktree] [--] [<pathspec>...]',
26+
description: 'Add file contents to the index.',
27+
args: {
28+
allowPositionals: true,
29+
options: {
30+
'staged': {
31+
description: 'Restore the file in the index.',
32+
type: 'boolean',
33+
short: 'S',
34+
},
35+
'worktree': {
36+
description: 'Restore the file in the working tree.',
37+
type: 'boolean',
38+
short: 'W',
39+
},
40+
'overlay': {
41+
description: 'Enable overlay mode. In overlay mode, files that do not exist in the source are not deleted.',
42+
type: 'boolean',
43+
value: false,
44+
},
45+
'no-overlay': {
46+
description: 'Disable overlay mode. Any files not in the source will be deleted.',
47+
type: 'boolean',
48+
},
49+
},
50+
},
51+
execute: async (ctx) => {
52+
const { io, fs, env, args } = ctx;
53+
const { stdout, stderr } = io;
54+
const { options, positionals } = args;
55+
const cache = {};
56+
57+
if (!options.staged && !options.worktree)
58+
options.worktree = true;
59+
60+
if (options['no-overlay'])
61+
options.overlay = false;
62+
63+
const FROM_INDEX = Symbol('FROM_INDEX');
64+
const source_ref = options.staged ? 'HEAD' : FROM_INDEX;
65+
66+
const pathspecs = positionals.map(it => path.resolve(env.PWD, it));
67+
if (pathspecs.length === 0)
68+
throw new Error(`you must specify path(s) to restore`);
69+
70+
const { dir, gitdir } = await find_repo_root(fs, env.PWD);
71+
72+
const operations = await git.walk({
73+
fs, dir, gitdir, cache,
74+
trees: [
75+
source_ref === FROM_INDEX ? STAGE() : TREE({ ref: source_ref }),
76+
TREE({ ref: 'HEAD' }), // Only required to check if a file is tracked.
77+
STAGE(),
78+
WORKDIR(),
79+
],
80+
map: async (filepath, [ source, head, staged, workdir]) => {
81+
// Reject paths that don't match pathspecs.
82+
const abs_filepath = path.resolve(env.PWD, filepath);
83+
if (!pathspecs.some(abs_path =>
84+
(filepath === '.') || (abs_filepath.startsWith(abs_path)) || (path.dirname(abs_filepath) === abs_path),
85+
)) {
86+
return null;
87+
}
88+
89+
if (await git.isIgnored({ fs, dir, gitdir, filepath }))
90+
return null;
91+
92+
const [
93+
source_type, staged_type, workdir_type
94+
] = await Promise.all([
95+
source?.type(), staged?.type(), workdir?.type()
96+
]);
97+
98+
// Exclude directories from results, but still iterate them.
99+
if ((!source_type || source_type === 'tree')
100+
&& (!staged_type || staged_type === 'tree')
101+
&& (!workdir_type || workdir_type === 'tree')) {
102+
return;
103+
}
104+
105+
// We need to modify the index or working tree if their oid doesn't match the source's.
106+
const [
107+
source_oid, staged_oid, workdir_oid
108+
] = await Promise.all([
109+
source_type === 'blob' ? source.oid() : undefined,
110+
staged_type === 'blob' ? staged.oid() : undefined,
111+
workdir_type === 'blob' ? workdir.oid() : undefined,
112+
]);
113+
const something_changed = (options.staged && staged_oid !== source_oid) || (options.worktree && workdir_oid !== source_oid);
114+
if (!something_changed)
115+
return null;
116+
117+
return Promise.all([
118+
// Update the index
119+
(async () => {
120+
if (!options.staged || staged_oid === source_oid)
121+
return;
122+
123+
await git.resetIndex({
124+
fs, dir, gitdir, cache,
125+
filepath,
126+
ref: source_ref,
127+
});
128+
})(),
129+
// Update the working tree
130+
(async () => {
131+
if (!options.worktree || workdir_oid === source_oid)
132+
return;
133+
134+
// If the file isn't in source, it needs to be deleted if it is tracked by git.
135+
// For now, I'll consider a file tracked if it exists in HEAD. This may not be correct though.
136+
// TODO: Add an isTracked(file) method to isomorphic-git
137+
if (!source && !head)
138+
return null;
139+
140+
if (source_oid) {
141+
// Write the file
142+
// Unfortunately, reading the source's file data is done differently depending on if it's the index or not.
143+
const source_content = source_ref === FROM_INDEX
144+
? (await git.readBlob({ fs, dir, gitdir, cache, oid: source_oid })).blob
145+
: await source.content();
146+
await fs.promises.writeFile(abs_filepath, source_content);
147+
} else if (!options.overlay) {
148+
// Delete the file
149+
await fs.promises.unlink(abs_filepath);
150+
}
151+
})(),
152+
]);
153+
},
154+
});
155+
await Promise.all(operations);
156+
}
157+
}

0 commit comments

Comments
 (0)