mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
* feat: branch specific items for cli * error on wmill.yaml parsing errors * also search for wmill.yaml in parent dirs when git * git_branches -> gitBranches * Update cli/src/core/specific_items.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * sanitize branch name (regex + fs path) * improve sanitatino * robust relative paths * hubpath --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
491 lines
16 KiB
TypeScript
491 lines
16 KiB
TypeScript
import { colors, log, yamlStringify } from "../../../deps.ts";
|
|
import { GlobalOptions } from "../../types.ts";
|
|
import { requireLogin } from "../../core/auth.ts";
|
|
import { resolveWorkspace } from "../../core/context.ts";
|
|
import * as wmill from "../../../gen/services.gen.ts";
|
|
import { SyncOptions, readConfigFile, getEffectiveSettings, DEFAULT_SYNC_OPTIONS, getWmillYamlPath } from "../../core/conf.ts";
|
|
import { deepEqual } from "../../utils/utils.ts";
|
|
import { getCurrentGitBranch, isGitRepository } from "../../utils/git.ts";
|
|
|
|
import { WriteMode } from "./types.ts";
|
|
import { GitSyncSettingsConverter } from "./converter.ts";
|
|
import { handleLegacyRepositoryMigration } from "./legacySettings.ts";
|
|
import {
|
|
selectAndLogRepository,
|
|
generateStructuredDiff,
|
|
generateChanges,
|
|
displayChanges,
|
|
applyBackendSettingsToBranch,
|
|
normalizeRepoPath,
|
|
outputResult
|
|
} from "./utils.ts";
|
|
|
|
export async function pullGitSyncSettings(
|
|
opts: GlobalOptions & {
|
|
repository?: string;
|
|
workspaceLevel?: boolean;
|
|
default?: boolean;
|
|
diff?: boolean;
|
|
jsonOutput?: boolean;
|
|
replace?: boolean;
|
|
override?: boolean;
|
|
withBackendSettings?: string;
|
|
yes?: boolean;
|
|
promotion?: string;
|
|
},
|
|
) {
|
|
const workspace = await resolveWorkspace(opts);
|
|
await requireLogin(opts);
|
|
|
|
try {
|
|
// Parse and validate --with-backend-settings if provided
|
|
let settings: any;
|
|
if (opts.withBackendSettings) {
|
|
try {
|
|
const parsedSettings = JSON.parse(opts.withBackendSettings);
|
|
|
|
// Validate the structure matches expected format (raw settings object)
|
|
if (
|
|
!parsedSettings.include_path ||
|
|
!Array.isArray(parsedSettings.include_path)
|
|
) {
|
|
throw new Error(
|
|
"Invalid settings format. Expected include_path array",
|
|
);
|
|
}
|
|
if (
|
|
!parsedSettings.include_type ||
|
|
!Array.isArray(parsedSettings.include_type)
|
|
) {
|
|
throw new Error(
|
|
"Invalid settings format. Expected include_type array",
|
|
);
|
|
}
|
|
|
|
// Create mock backend response with single repository using provided settings
|
|
const mockRepositoryPath = opts.repository || "u/mock/repo";
|
|
settings = {
|
|
git_sync: {
|
|
repositories: [{
|
|
git_repo_resource_path: mockRepositoryPath,
|
|
settings: {
|
|
include_path: parsedSettings.include_path,
|
|
include_type: parsedSettings.include_type,
|
|
exclude_path: parsedSettings.exclude_path || [],
|
|
extra_include_path: parsedSettings.extra_include_path || [],
|
|
},
|
|
}],
|
|
},
|
|
};
|
|
} catch (parseError) {
|
|
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
outputResult(opts, {
|
|
success: false,
|
|
error: `Failed to parse --with-backend-settings JSON: ${errorMessage}`,
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
// Fetch workspace settings to get git-sync configuration
|
|
try {
|
|
settings = await wmill.getSettings({
|
|
workspace: workspace.workspaceId,
|
|
});
|
|
} catch (apiError) {
|
|
const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
|
|
outputResult(opts, {
|
|
success: false,
|
|
error: `Failed to fetch workspace settings: ${errorMessage}`,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (
|
|
!settings.git_sync?.repositories ||
|
|
settings.git_sync.repositories.length === 0
|
|
) {
|
|
outputResult(opts, {
|
|
success: false,
|
|
error: "No git-sync repositories configured",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Find the repository to work with
|
|
let selectedRepo = await selectAndLogRepository(
|
|
settings.git_sync.repositories,
|
|
opts.repository,
|
|
opts.jsonOutput,
|
|
);
|
|
|
|
// Check if the selected repository needs migration and handle it
|
|
selectedRepo = await handleLegacyRepositoryMigration(
|
|
selectedRepo,
|
|
settings.git_sync,
|
|
workspace,
|
|
opts,
|
|
"pull"
|
|
);
|
|
|
|
// Convert backend settings to SyncOptions format
|
|
const backendSyncOptions: SyncOptions = GitSyncSettingsConverter.fromBackendFormat(selectedRepo.settings);
|
|
|
|
// Check if wmill.yaml exists - create a default one if it doesn't exist
|
|
const wmillYamlPath = getWmillYamlPath();
|
|
const wmillYamlExists = wmillYamlPath !== null;
|
|
if (!wmillYamlExists) {
|
|
if (!opts.jsonOutput) {
|
|
log.info(
|
|
colors.yellow(
|
|
"No wmill.yaml file found. Will create one with backend git-sync settings.",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
let localConfig: SyncOptions;
|
|
if (wmillYamlExists) {
|
|
localConfig = await readConfigFile();
|
|
} else {
|
|
localConfig = { ...DEFAULT_SYNC_OPTIONS };
|
|
}
|
|
|
|
// Handle the case where wmill.yaml doesn't exist - just create it with backend settings
|
|
if (!wmillYamlExists) {
|
|
// When creating from scratch, just apply backend settings directly
|
|
let updatedConfig = { ...localConfig };
|
|
|
|
// Apply backend settings to the default config
|
|
Object.assign(updatedConfig, backendSyncOptions);
|
|
|
|
// Add git branch structure if in a git repository
|
|
if (isGitRepository()) {
|
|
const currentBranch = getCurrentGitBranch();
|
|
if (currentBranch) {
|
|
if (!updatedConfig.gitBranches) {
|
|
updatedConfig.gitBranches = {};
|
|
}
|
|
if (!updatedConfig.gitBranches[currentBranch]) {
|
|
updatedConfig.gitBranches[currentBranch] = { overrides: {} };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write the new configuration
|
|
await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
|
|
|
|
if (opts.jsonOutput) {
|
|
console.log(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: "wmill.yaml created with backend git-sync settings",
|
|
repository: selectedRepo.git_repo_resource_path,
|
|
}),
|
|
);
|
|
} else {
|
|
log.info(
|
|
colors.green(
|
|
`wmill.yaml created with git-sync settings from ${selectedRepo.git_repo_resource_path}`,
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For existing wmill.yaml files, continue with the normal flow
|
|
// Calculate current settings once and reuse throughout
|
|
const currentSettings = await getEffectiveSettings(localConfig, opts.promotion, true, opts.jsonOutput);
|
|
|
|
// Determine write mode for branch-based configuration
|
|
let writeMode: WriteMode = "replace";
|
|
|
|
// Determine write mode based on flags
|
|
if (opts.default || opts.replace) {
|
|
writeMode = "replace";
|
|
} else if (opts.override) {
|
|
writeMode = "override";
|
|
} else {
|
|
// Default behavior for existing files with no explicit flags
|
|
// Use same logic as diff to determine if there's a real conflict
|
|
const gitSyncBackend = GitSyncSettingsConverter.extractGitSyncFields(
|
|
GitSyncSettingsConverter.normalize(backendSyncOptions),
|
|
);
|
|
const gitSyncCurrent = GitSyncSettingsConverter.extractGitSyncFields(
|
|
GitSyncSettingsConverter.normalize(currentSettings),
|
|
);
|
|
const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
|
|
if (hasConflict) {
|
|
// For diff mode, show what override would look like
|
|
writeMode = "override";
|
|
} else {
|
|
writeMode = "replace";
|
|
}
|
|
}
|
|
|
|
if (opts.diff) {
|
|
// Show differences between local and backend
|
|
const normalizedCurrent = GitSyncSettingsConverter.normalize(currentSettings);
|
|
const normalizedBackend = GitSyncSettingsConverter.normalize(backendSyncOptions);
|
|
const gitSyncCurrent = GitSyncSettingsConverter.extractGitSyncFields(normalizedCurrent);
|
|
const gitSyncBackend = GitSyncSettingsConverter.extractGitSyncFields(normalizedBackend);
|
|
const hasChanges = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
|
|
if (opts.jsonOutput) {
|
|
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
|
|
|
// Generate structured diff using the same normalized objects
|
|
const structuredDiff = hasChanges
|
|
? generateStructuredDiff(gitSyncCurrent, gitSyncBackend)
|
|
: {};
|
|
|
|
console.log(
|
|
JSON.stringify({
|
|
success: true,
|
|
hasChanges,
|
|
local: GitSyncSettingsConverter.toBackendFormat(normalizedCurrent),
|
|
backend: GitSyncSettingsConverter.toBackendFormat(normalizedBackend),
|
|
repository: selectedRepo.git_repo_resource_path,
|
|
diff: structuredDiff,
|
|
}),
|
|
);
|
|
} else {
|
|
if (hasChanges) {
|
|
log.info("Changes that would be applied locally:");
|
|
const changes = generateChanges(currentSettings, backendSyncOptions);
|
|
|
|
if (Object.keys(changes).length === 0) {
|
|
log.info(colors.green("No differences found"));
|
|
} else {
|
|
displayChanges(changes);
|
|
}
|
|
} else {
|
|
log.info(colors.green("No differences found"));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For non-diff mode, handle interactive logic if needed
|
|
// Only show interactive prompts for existing files with conflicts
|
|
if (
|
|
!opts.diff &&
|
|
!opts.default &&
|
|
!opts.replace &&
|
|
!opts.override
|
|
) {
|
|
// Recalculate current settings with promotion if needed
|
|
const effectiveCurrentSettings = await getEffectiveSettings(localConfig, opts.promotion, true, opts.jsonOutput);
|
|
// Use the same logic as diff to determine current settings
|
|
const gitSyncBackend = GitSyncSettingsConverter.extractGitSyncFields(
|
|
GitSyncSettingsConverter.normalize(backendSyncOptions),
|
|
);
|
|
const gitSyncCurrent = GitSyncSettingsConverter.extractGitSyncFields(
|
|
GitSyncSettingsConverter.normalize(effectiveCurrentSettings),
|
|
);
|
|
const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
|
|
if (hasConflict && !opts.yes && Deno.stdin.isTerminal()) {
|
|
// Show the diff first
|
|
log.info("Changes that would be applied locally:");
|
|
const changes = generateChanges(effectiveCurrentSettings, backendSyncOptions);
|
|
if (Object.keys(changes).length > 0) {
|
|
displayChanges(changes);
|
|
}
|
|
|
|
// Interactive mode - ask user
|
|
const { Select } = await import("../../../deps.ts");
|
|
const choice = await Select.prompt({
|
|
message: "Settings conflict detected. How would you like to proceed?",
|
|
options: [
|
|
{
|
|
name: "Replace existing settings",
|
|
value: "replace",
|
|
},
|
|
{
|
|
name: "Add branch-specific override",
|
|
value: "override",
|
|
},
|
|
{ name: "Cancel", value: "cancel" },
|
|
],
|
|
});
|
|
|
|
if (choice === "cancel") {
|
|
log.info("Operation cancelled");
|
|
return;
|
|
}
|
|
|
|
writeMode = choice as WriteMode;
|
|
} else if (hasConflict && opts.yes) {
|
|
// --yes flag: default to override behavior for conflicts
|
|
writeMode = "override";
|
|
log.info(colors.yellow("Settings conflict detected. Using --override behavior (default for --yes)."));
|
|
} else if (hasConflict) {
|
|
// Non-interactive mode with conflicts - show message and exit
|
|
if (opts.jsonOutput) {
|
|
console.log(
|
|
JSON.stringify({
|
|
success: false,
|
|
error:
|
|
"Settings conflict detected. Use --replace or --override flags to resolve.",
|
|
hasConflict: true,
|
|
}),
|
|
);
|
|
} else {
|
|
log.error(colors.red("Settings conflict detected."));
|
|
log.info(
|
|
"Use --replace to overwrite existing settings or --override to add branch-specific override.",
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if there are actually any changes before writing
|
|
const gitSyncBackend = GitSyncSettingsConverter.extractGitSyncFields(
|
|
GitSyncSettingsConverter.normalize(backendSyncOptions),
|
|
);
|
|
const gitSyncCurrent = GitSyncSettingsConverter.extractGitSyncFields(
|
|
GitSyncSettingsConverter.normalize(currentSettings),
|
|
);
|
|
const hasActualChanges = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
|
|
if (!hasActualChanges && wmillYamlExists) {
|
|
// Even if no changes, check if we need to create empty branch structure
|
|
let needsBranchStructure = false;
|
|
if (isGitRepository()) {
|
|
const currentBranch = getCurrentGitBranch();
|
|
if (currentBranch && (!localConfig.gitBranches || !localConfig.gitBranches[currentBranch])) {
|
|
needsBranchStructure = true;
|
|
|
|
// Create empty branch structure
|
|
const updatedConfig = { ...localConfig };
|
|
if (!updatedConfig.gitBranches) {
|
|
updatedConfig.gitBranches = {};
|
|
}
|
|
if (!updatedConfig.gitBranches[currentBranch]) {
|
|
updatedConfig.gitBranches[currentBranch] = { overrides: {} };
|
|
}
|
|
|
|
// Write updated configuration
|
|
await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
|
|
|
|
if (opts.jsonOutput) {
|
|
console.log(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: `Created empty branch structure for: ${currentBranch}`,
|
|
repository: selectedRepo.git_repo_resource_path,
|
|
}),
|
|
);
|
|
} else {
|
|
log.info(
|
|
colors.green(
|
|
`Created empty branch structure for: ${currentBranch}`,
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (opts.jsonOutput) {
|
|
console.log(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: "No changes needed",
|
|
repository: selectedRepo.git_repo_resource_path,
|
|
}),
|
|
);
|
|
} else {
|
|
log.info(
|
|
colors.green(
|
|
"No changes needed - settings are already up to date",
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Apply the settings based on write mode
|
|
let updatedConfig: SyncOptions;
|
|
|
|
|
|
if (writeMode === "replace") {
|
|
// Replace top-level settings
|
|
updatedConfig = { ...localConfig };
|
|
// Update with backend git-sync settings
|
|
Object.assign(updatedConfig, backendSyncOptions);
|
|
} else {
|
|
// Override mode - will be handled by branch logic below
|
|
updatedConfig = { ...localConfig };
|
|
}
|
|
|
|
// For replace mode, add empty branch structure if in Git repo
|
|
if (writeMode === "replace" && isGitRepository()) {
|
|
const currentBranch = getCurrentGitBranch();
|
|
if (currentBranch) {
|
|
log.info(`Detected Git repository, adding empty branch structure for: ${currentBranch}`);
|
|
if (!updatedConfig.gitBranches) {
|
|
updatedConfig.gitBranches = {};
|
|
}
|
|
if (!updatedConfig.gitBranches[currentBranch]) {
|
|
updatedConfig.gitBranches[currentBranch] = { overrides: {} };
|
|
}
|
|
}
|
|
}
|
|
|
|
// For override mode, write to branch overrides
|
|
if (writeMode === "override" && isGitRepository()) {
|
|
const currentBranch = getCurrentGitBranch();
|
|
if (currentBranch) {
|
|
log.info(`Detected Git repository, writing settings to branch: ${currentBranch}`);
|
|
updatedConfig = applyBackendSettingsToBranch(localConfig, currentBranch, backendSyncOptions);
|
|
}
|
|
}
|
|
|
|
// Write updated configuration
|
|
await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
|
|
|
|
if (opts.jsonOutput) {
|
|
console.log(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: `Git-sync settings pulled successfully`,
|
|
repository: selectedRepo.git_repo_resource_path,
|
|
}),
|
|
);
|
|
} else {
|
|
log.info(
|
|
colors.green(
|
|
`Git-sync settings pulled successfully from ${selectedRepo.git_repo_resource_path}`,
|
|
),
|
|
);
|
|
if (writeMode === "override") {
|
|
const currentBranch = getCurrentGitBranch();
|
|
if (currentBranch) {
|
|
log.info(
|
|
colors.gray(`Settings written to branch '${currentBranch}' overrides`),
|
|
);
|
|
}
|
|
} else if (writeMode === "replace") {
|
|
log.info(colors.gray(`Settings written to top-level configuration`));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
if (opts.jsonOutput) {
|
|
console.log(
|
|
JSON.stringify({ success: false, error: errorMessage }),
|
|
);
|
|
} else {
|
|
log.error(
|
|
colors.red(
|
|
`Failed to pull git-sync settings: ${errorMessage}`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|