Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/dev-call/pull-git-subtree/cmd.ts
6451 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2025 Posit Software, PBC
5
*/
6
7
import { Command } from "cliffy/command/mod.ts";
8
import { gitCmdOutput, gitCmds } from "../../../core/git.ts";
9
import { debug, error, info } from "../../../deno_ral/log.ts";
10
import { logLevel } from "../../../core/log.ts";
11
12
interface SubtreeConfig {
13
name: string;
14
prefix: string;
15
remoteUrl: string;
16
remoteBranch: string;
17
}
18
19
// Subtree configurations - update these with actual repositories
20
const SUBTREES: SubtreeConfig[] = [
21
{
22
name: "julia-engine",
23
prefix: "src/resources/extension-subtrees/julia-engine",
24
remoteUrl: "https://github.com/gordonwoodhull/quarto-julia-engine.git",
25
remoteBranch: "main",
26
},
27
{
28
name: "orange-book",
29
prefix: "src/resources/extension-subtrees/orange-book",
30
remoteUrl: "https://github.com/quarto-ext/orange-book.git",
31
remoteBranch: "main",
32
},
33
];
34
35
async function findLastSplit(
36
quartoRoot: string,
37
prefix: string,
38
): Promise<string | null> {
39
try {
40
debug(
41
`Searching for last split with grep pattern: git-subtree-dir: ${prefix}$`,
42
);
43
const log = await gitCmdOutput(quartoRoot, [
44
"log",
45
`--grep=git-subtree-dir: ${prefix}$`,
46
"-1",
47
"--pretty=%b",
48
]);
49
50
debug(`Git log output: ${log}`);
51
const splitLine = log.split("\n").find((line) =>
52
line.startsWith("git-subtree-split:")
53
);
54
if (!splitLine) {
55
debug("No split line found in log output");
56
return null;
57
}
58
59
const splitCommit = splitLine.split(/\s+/)[1];
60
debug(`Found last split commit: ${splitCommit}`);
61
return splitCommit;
62
} catch (e) {
63
debug(`Error finding last split: ${e}`);
64
return null;
65
}
66
}
67
68
async function pullSubtree(
69
quartoRoot: string,
70
config: SubtreeConfig,
71
): Promise<void> {
72
info(`\n=== Pulling subtree: ${config.name} ===`);
73
info(`Prefix: ${config.prefix}`);
74
info(`Remote: ${config.remoteUrl}`);
75
info(`Branch: ${config.remoteBranch}`);
76
77
// Fetch from remote
78
info("Fetching...");
79
await gitCmds(quartoRoot, [
80
["fetch", config.remoteUrl, config.remoteBranch],
81
]);
82
83
// Check what FETCH_HEAD points to
84
const fetchHead = await gitCmdOutput(quartoRoot, ["rev-parse", "FETCH_HEAD"]);
85
debug(`FETCH_HEAD resolves to: ${fetchHead.trim()}`);
86
87
// Check if prefix directory exists
88
const prefixPath = `${quartoRoot}/${config.prefix}`;
89
let prefixExists = false;
90
try {
91
const stat = await Deno.stat(prefixPath);
92
prefixExists = stat.isDirectory;
93
debug(`Prefix directory exists: ${prefixExists} (${prefixPath})`);
94
} catch {
95
debug(`Prefix directory does not exist: ${prefixPath}`);
96
}
97
98
// Find last split point
99
let lastSplit = await findLastSplit(quartoRoot, config.prefix);
100
101
if (!lastSplit) {
102
info("No previous subtree found - using 'git subtree add'");
103
await gitCmds(quartoRoot, [
104
[
105
"subtree",
106
"add",
107
"--squash",
108
`--prefix=${config.prefix}`,
109
config.remoteUrl,
110
config.remoteBranch,
111
],
112
]);
113
lastSplit = await gitCmdOutput(quartoRoot, ["rev-parse", "FETCH_HEAD^"]);
114
debug(`After subtree add, lastSplit set to: ${lastSplit.trim()}`);
115
}
116
117
// Check for new commits
118
const commitRange = `${lastSplit}..FETCH_HEAD`;
119
debug(`Checking commit range: ${commitRange}`);
120
121
const hasNewCommits = await gitCmdOutput(quartoRoot, [
122
"log",
123
"--oneline",
124
commitRange,
125
"-1",
126
]);
127
128
if (!hasNewCommits.trim()) {
129
info("No new commits to merge");
130
debug(`Commit range ${commitRange} has no commits`);
131
if (!prefixExists) {
132
info("WARNING: Prefix directory doesn't exist but no new commits found!");
133
debug("This may indicate lastSplit was found on a different branch");
134
}
135
return;
136
}
137
138
debug(`Found new commits in range ${commitRange}`);
139
140
// Do the subtree pull
141
info("Running git subtree pull --squash...");
142
await gitCmds(quartoRoot, [
143
[
144
"subtree",
145
"pull",
146
"--squash",
147
`--prefix=${config.prefix}`,
148
config.remoteUrl,
149
config.remoteBranch,
150
],
151
]);
152
153
info("✓ Done!");
154
}
155
156
export const pullGitSubtreeCommand = new Command()
157
.name("pull-git-subtree")
158
.hidden()
159
.arguments("[name:string]")
160
.description(
161
"Pull configured git subtrees.\n\n" +
162
"This command pulls from configured subtree repositories " +
163
"using --squash, which creates two commits: a squash commit " +
164
"containing the subtree changes and a merge commit that " +
165
"integrates it into your branch.\n\n" +
166
"Arguments:\n" +
167
" [name] Name of subtree to pull (use 'all' or omit to pull all)",
168
)
169
.action(async (_options: unknown, nameArg?: string) => {
170
// Get quarto root directory
171
const quartoRoot = Deno.env.get("QUARTO_ROOT");
172
if (!quartoRoot) {
173
error(
174
"QUARTO_ROOT environment variable not set. This command requires a development version of Quarto.",
175
);
176
Deno.exit(1);
177
}
178
179
// Show current branch for debugging (only if debug logging enabled)
180
if (logLevel() === "DEBUG") {
181
try {
182
const currentBranch = await gitCmdOutput(quartoRoot, [
183
"branch",
184
"--show-current",
185
]);
186
debug(`Current branch: ${currentBranch.trim()}`);
187
} catch (e) {
188
debug(`Unable to determine current branch: ${e}`);
189
}
190
}
191
192
// Determine which subtrees to pull
193
let subtreesToPull: SubtreeConfig[];
194
195
if (!nameArg || nameArg === "all") {
196
// Pull all subtrees
197
subtreesToPull = SUBTREES;
198
info(`Quarto root: ${quartoRoot}`);
199
info(`Processing all ${SUBTREES.length} subtree(s)...`);
200
} else {
201
// Pull specific subtree by name
202
const config = SUBTREES.find((s) => s.name === nameArg);
203
if (!config) {
204
error(`Unknown subtree name: ${nameArg}`);
205
error(`Available subtrees: ${SUBTREES.map((s) => s.name).join(", ")}`);
206
Deno.exit(1);
207
}
208
subtreesToPull = [config];
209
info(`Quarto root: ${quartoRoot}`);
210
info(`Processing subtree: ${nameArg}`);
211
}
212
213
let successCount = 0;
214
let errorCount = 0;
215
216
for (const config of subtreesToPull) {
217
try {
218
await pullSubtree(quartoRoot, config);
219
successCount++;
220
} catch (err) {
221
const message = err instanceof Error ? err.message : String(err);
222
error(`Failed to pull subtree ${config.name}: ${message}`);
223
errorCount++;
224
}
225
}
226
227
info(`\n=== Summary ===`);
228
info(`Success: ${successCount}`);
229
info(`Failed: ${errorCount}`);
230
231
if (errorCount > 0) {
232
Deno.exit(1);
233
}
234
});
235
236