Path: blob/main/src/command/dev-call/pull-git-subtree/cmd.ts
6451 views
/*1* cmd.ts2*3* Copyright (C) 2025 Posit Software, PBC4*/56import { Command } from "cliffy/command/mod.ts";7import { gitCmdOutput, gitCmds } from "../../../core/git.ts";8import { debug, error, info } from "../../../deno_ral/log.ts";9import { logLevel } from "../../../core/log.ts";1011interface SubtreeConfig {12name: string;13prefix: string;14remoteUrl: string;15remoteBranch: string;16}1718// Subtree configurations - update these with actual repositories19const SUBTREES: SubtreeConfig[] = [20{21name: "julia-engine",22prefix: "src/resources/extension-subtrees/julia-engine",23remoteUrl: "https://github.com/gordonwoodhull/quarto-julia-engine.git",24remoteBranch: "main",25},26{27name: "orange-book",28prefix: "src/resources/extension-subtrees/orange-book",29remoteUrl: "https://github.com/quarto-ext/orange-book.git",30remoteBranch: "main",31},32];3334async function findLastSplit(35quartoRoot: string,36prefix: string,37): Promise<string | null> {38try {39debug(40`Searching for last split with grep pattern: git-subtree-dir: ${prefix}$`,41);42const log = await gitCmdOutput(quartoRoot, [43"log",44`--grep=git-subtree-dir: ${prefix}$`,45"-1",46"--pretty=%b",47]);4849debug(`Git log output: ${log}`);50const splitLine = log.split("\n").find((line) =>51line.startsWith("git-subtree-split:")52);53if (!splitLine) {54debug("No split line found in log output");55return null;56}5758const splitCommit = splitLine.split(/\s+/)[1];59debug(`Found last split commit: ${splitCommit}`);60return splitCommit;61} catch (e) {62debug(`Error finding last split: ${e}`);63return null;64}65}6667async function pullSubtree(68quartoRoot: string,69config: SubtreeConfig,70): Promise<void> {71info(`\n=== Pulling subtree: ${config.name} ===`);72info(`Prefix: ${config.prefix}`);73info(`Remote: ${config.remoteUrl}`);74info(`Branch: ${config.remoteBranch}`);7576// Fetch from remote77info("Fetching...");78await gitCmds(quartoRoot, [79["fetch", config.remoteUrl, config.remoteBranch],80]);8182// Check what FETCH_HEAD points to83const fetchHead = await gitCmdOutput(quartoRoot, ["rev-parse", "FETCH_HEAD"]);84debug(`FETCH_HEAD resolves to: ${fetchHead.trim()}`);8586// Check if prefix directory exists87const prefixPath = `${quartoRoot}/${config.prefix}`;88let prefixExists = false;89try {90const stat = await Deno.stat(prefixPath);91prefixExists = stat.isDirectory;92debug(`Prefix directory exists: ${prefixExists} (${prefixPath})`);93} catch {94debug(`Prefix directory does not exist: ${prefixPath}`);95}9697// Find last split point98let lastSplit = await findLastSplit(quartoRoot, config.prefix);99100if (!lastSplit) {101info("No previous subtree found - using 'git subtree add'");102await gitCmds(quartoRoot, [103[104"subtree",105"add",106"--squash",107`--prefix=${config.prefix}`,108config.remoteUrl,109config.remoteBranch,110],111]);112lastSplit = await gitCmdOutput(quartoRoot, ["rev-parse", "FETCH_HEAD^"]);113debug(`After subtree add, lastSplit set to: ${lastSplit.trim()}`);114}115116// Check for new commits117const commitRange = `${lastSplit}..FETCH_HEAD`;118debug(`Checking commit range: ${commitRange}`);119120const hasNewCommits = await gitCmdOutput(quartoRoot, [121"log",122"--oneline",123commitRange,124"-1",125]);126127if (!hasNewCommits.trim()) {128info("No new commits to merge");129debug(`Commit range ${commitRange} has no commits`);130if (!prefixExists) {131info("WARNING: Prefix directory doesn't exist but no new commits found!");132debug("This may indicate lastSplit was found on a different branch");133}134return;135}136137debug(`Found new commits in range ${commitRange}`);138139// Do the subtree pull140info("Running git subtree pull --squash...");141await gitCmds(quartoRoot, [142[143"subtree",144"pull",145"--squash",146`--prefix=${config.prefix}`,147config.remoteUrl,148config.remoteBranch,149],150]);151152info("✓ Done!");153}154155export const pullGitSubtreeCommand = new Command()156.name("pull-git-subtree")157.hidden()158.arguments("[name:string]")159.description(160"Pull configured git subtrees.\n\n" +161"This command pulls from configured subtree repositories " +162"using --squash, which creates two commits: a squash commit " +163"containing the subtree changes and a merge commit that " +164"integrates it into your branch.\n\n" +165"Arguments:\n" +166" [name] Name of subtree to pull (use 'all' or omit to pull all)",167)168.action(async (_options: unknown, nameArg?: string) => {169// Get quarto root directory170const quartoRoot = Deno.env.get("QUARTO_ROOT");171if (!quartoRoot) {172error(173"QUARTO_ROOT environment variable not set. This command requires a development version of Quarto.",174);175Deno.exit(1);176}177178// Show current branch for debugging (only if debug logging enabled)179if (logLevel() === "DEBUG") {180try {181const currentBranch = await gitCmdOutput(quartoRoot, [182"branch",183"--show-current",184]);185debug(`Current branch: ${currentBranch.trim()}`);186} catch (e) {187debug(`Unable to determine current branch: ${e}`);188}189}190191// Determine which subtrees to pull192let subtreesToPull: SubtreeConfig[];193194if (!nameArg || nameArg === "all") {195// Pull all subtrees196subtreesToPull = SUBTREES;197info(`Quarto root: ${quartoRoot}`);198info(`Processing all ${SUBTREES.length} subtree(s)...`);199} else {200// Pull specific subtree by name201const config = SUBTREES.find((s) => s.name === nameArg);202if (!config) {203error(`Unknown subtree name: ${nameArg}`);204error(`Available subtrees: ${SUBTREES.map((s) => s.name).join(", ")}`);205Deno.exit(1);206}207subtreesToPull = [config];208info(`Quarto root: ${quartoRoot}`);209info(`Processing subtree: ${nameArg}`);210}211212let successCount = 0;213let errorCount = 0;214215for (const config of subtreesToPull) {216try {217await pullSubtree(quartoRoot, config);218successCount++;219} catch (err) {220const message = err instanceof Error ? err.message : String(err);221error(`Failed to pull subtree ${config.name}: ${message}`);222errorCount++;223}224}225226info(`\n=== Summary ===`);227info(`Success: ${successCount}`);228info(`Failed: ${errorCount}`);229230if (errorCount > 0) {231Deno.exit(1);232}233});234235236