mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
feat: CLI improvements (#6446)
* 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>
This commit is contained in:
@@ -3,11 +3,11 @@ 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 } from "../../core/conf.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 { GitSyncRepository, WriteMode } from "./types.ts";
|
||||
import { WriteMode } from "./types.ts";
|
||||
import { GitSyncSettingsConverter } from "./converter.ts";
|
||||
import { handleLegacyRepositoryMigration } from "./legacySettings.ts";
|
||||
import {
|
||||
@@ -132,11 +132,9 @@ export async function pullGitSyncSettings(
|
||||
const backendSyncOptions: SyncOptions = GitSyncSettingsConverter.fromBackendFormat(selectedRepo.settings);
|
||||
|
||||
// Check if wmill.yaml exists - create a default one if it doesn't exist
|
||||
let wmillYamlExists = true;
|
||||
try {
|
||||
await Deno.stat("wmill.yaml");
|
||||
} catch (error) {
|
||||
wmillYamlExists = false;
|
||||
const wmillYamlPath = getWmillYamlPath();
|
||||
const wmillYamlExists = wmillYamlPath !== null;
|
||||
if (!wmillYamlExists) {
|
||||
if (!opts.jsonOutput) {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
@@ -165,11 +163,11 @@ export async function pullGitSyncSettings(
|
||||
if (isGitRepository()) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (currentBranch) {
|
||||
if (!updatedConfig.git_branches) {
|
||||
updatedConfig.git_branches = {};
|
||||
if (!updatedConfig.gitBranches) {
|
||||
updatedConfig.gitBranches = {};
|
||||
}
|
||||
if (!updatedConfig.git_branches[currentBranch]) {
|
||||
updatedConfig.git_branches[currentBranch] = { overrides: {} };
|
||||
if (!updatedConfig.gitBranches[currentBranch]) {
|
||||
updatedConfig.gitBranches[currentBranch] = { overrides: {} };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,16 +356,16 @@ export async function pullGitSyncSettings(
|
||||
let needsBranchStructure = false;
|
||||
if (isGitRepository()) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (currentBranch && (!localConfig.git_branches || !localConfig.git_branches[currentBranch])) {
|
||||
if (currentBranch && (!localConfig.gitBranches || !localConfig.gitBranches[currentBranch])) {
|
||||
needsBranchStructure = true;
|
||||
|
||||
// Create empty branch structure
|
||||
const updatedConfig = { ...localConfig };
|
||||
if (!updatedConfig.git_branches) {
|
||||
updatedConfig.git_branches = {};
|
||||
if (!updatedConfig.gitBranches) {
|
||||
updatedConfig.gitBranches = {};
|
||||
}
|
||||
if (!updatedConfig.git_branches[currentBranch]) {
|
||||
updatedConfig.git_branches[currentBranch] = { overrides: {} };
|
||||
if (!updatedConfig.gitBranches[currentBranch]) {
|
||||
updatedConfig.gitBranches[currentBranch] = { overrides: {} };
|
||||
}
|
||||
|
||||
// Write updated configuration
|
||||
@@ -429,11 +427,11 @@ export async function pullGitSyncSettings(
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (currentBranch) {
|
||||
log.info(`Detected Git repository, adding empty branch structure for: ${currentBranch}`);
|
||||
if (!updatedConfig.git_branches) {
|
||||
updatedConfig.git_branches = {};
|
||||
if (!updatedConfig.gitBranches) {
|
||||
updatedConfig.gitBranches = {};
|
||||
}
|
||||
if (!updatedConfig.git_branches[currentBranch]) {
|
||||
updatedConfig.git_branches[currentBranch] = { overrides: {} };
|
||||
if (!updatedConfig.gitBranches[currentBranch]) {
|
||||
updatedConfig.gitBranches[currentBranch] = { overrides: {} };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, validateBranchConfiguration, getEffectiveSettings } from "../../core/conf.ts";
|
||||
import { SyncOptions, readConfigFile, validateBranchConfiguration, getEffectiveSettings, getWmillYamlPath } from "../../core/conf.ts";
|
||||
import { deepEqual } from "../../utils/utils.ts";
|
||||
|
||||
import { GitSyncRepository } from "./types.ts";
|
||||
@@ -44,9 +44,8 @@ export async function pushGitSyncSettings(
|
||||
|
||||
try {
|
||||
// Check if wmill.yaml exists - require it for git-sync settings commands
|
||||
try {
|
||||
await Deno.stat("wmill.yaml");
|
||||
} catch (error) {
|
||||
const wmillYamlPath = getWmillYamlPath();
|
||||
if (!wmillYamlPath) {
|
||||
log.error(
|
||||
colors.red(
|
||||
"No wmill.yaml file found. Please run 'wmill init' first to create the configuration file.",
|
||||
|
||||
@@ -30,12 +30,12 @@ export function getOrCreateBranchConfig(config: SyncOptions, branchName: string)
|
||||
config: SyncOptions;
|
||||
branchKey: string;
|
||||
} {
|
||||
if (!config.git_branches) {
|
||||
config.git_branches = {};
|
||||
if (!config.gitBranches) {
|
||||
config.gitBranches = {};
|
||||
}
|
||||
|
||||
if (!config.git_branches[branchName]) {
|
||||
config.git_branches[branchName] = {};
|
||||
if (!config.gitBranches[branchName]) {
|
||||
config.gitBranches[branchName] = {};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -53,12 +53,12 @@ export function applyBackendSettingsToBranch(
|
||||
const { config: updatedConfig } = getOrCreateBranchConfig(config, branchName);
|
||||
|
||||
// Get the base settings (top-level + defaults) to compare against
|
||||
const { git_branches, ...topLevelSettings } = config;
|
||||
const { gitBranches, ...topLevelSettings } = config;
|
||||
const baseSettings: Partial<SyncOptions> = { ...DEFAULT_SYNC_OPTIONS, ...topLevelSettings };
|
||||
|
||||
// Only store fields that differ from the base settings
|
||||
Object.keys(backendSettings).forEach(key => {
|
||||
if (key !== 'git_branches' && backendSettings[key as keyof SyncOptions] !== undefined) {
|
||||
if (key !== 'gitBranches' && backendSettings[key as keyof SyncOptions] !== undefined) {
|
||||
const backendValue = backendSettings[key as keyof SyncOptions];
|
||||
const baseValue = baseSettings[key as keyof SyncOptions];
|
||||
|
||||
@@ -66,10 +66,10 @@ export function applyBackendSettingsToBranch(
|
||||
const isDifferent = GitSyncSettingsConverter.isDifferent(backendValue, baseValue);
|
||||
|
||||
if (isDifferent) {
|
||||
if (!updatedConfig.git_branches![branchName].overrides) {
|
||||
updatedConfig.git_branches![branchName].overrides = {};
|
||||
if (!updatedConfig.gitBranches![branchName].overrides) {
|
||||
updatedConfig.gitBranches![branchName].overrides = {};
|
||||
}
|
||||
(updatedConfig.git_branches![branchName].overrides as any)[key] = backendValue;
|
||||
(updatedConfig.gitBranches![branchName].overrides as any)[key] = backendValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,14 +43,14 @@ async function initAction(opts: InitOptions) {
|
||||
if (isGitRepository()) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (currentBranch) {
|
||||
initialConfig.git_branches = {
|
||||
initialConfig.gitBranches = {
|
||||
[currentBranch]: { overrides: {} },
|
||||
};
|
||||
} else {
|
||||
initialConfig.git_branches = {};
|
||||
initialConfig.gitBranches = {};
|
||||
}
|
||||
} else {
|
||||
initialConfig.git_branches = {};
|
||||
initialConfig.gitBranches = {};
|
||||
}
|
||||
|
||||
await Deno.writeTextFile("wmill.yaml", yamlStringify(initialConfig));
|
||||
@@ -116,16 +116,16 @@ async function initAction(opts: InitOptions) {
|
||||
const currentConfig = await import("../../core/conf.ts").then((m) =>
|
||||
m.readConfigFile()
|
||||
);
|
||||
if (!currentConfig.git_branches) {
|
||||
currentConfig.git_branches = {};
|
||||
if (!currentConfig.gitBranches) {
|
||||
currentConfig.gitBranches = {};
|
||||
}
|
||||
if (!currentConfig.git_branches[currentBranch]) {
|
||||
currentConfig.git_branches[currentBranch] = { overrides: {} };
|
||||
if (!currentConfig.gitBranches[currentBranch]) {
|
||||
currentConfig.gitBranches[currentBranch] = { overrides: {} };
|
||||
}
|
||||
|
||||
currentConfig.git_branches[currentBranch].baseUrl =
|
||||
currentConfig.gitBranches[currentBranch].baseUrl =
|
||||
activeWorkspace.remote;
|
||||
currentConfig.git_branches[currentBranch].workspaceId =
|
||||
currentConfig.gitBranches[currentBranch].workspaceId =
|
||||
activeWorkspace.workspaceId;
|
||||
|
||||
await Deno.writeTextFile(
|
||||
|
||||
+165
-24
@@ -42,7 +42,19 @@ import {
|
||||
readConfigFile,
|
||||
getEffectiveSettings,
|
||||
validateBranchConfiguration,
|
||||
mergeConfigWithConfigFile,
|
||||
} from "../../core/conf.ts";
|
||||
import {
|
||||
SpecificItemsConfig,
|
||||
getSpecificItemsForCurrentBranch,
|
||||
isSpecificItem,
|
||||
getBranchSpecificPath,
|
||||
fromBranchSpecificPath,
|
||||
isCurrentBranchFile,
|
||||
toBranchSpecificPath,
|
||||
isBranchSpecificFile,
|
||||
} from "../../core/specific_items.ts";
|
||||
import { getCurrentGitBranch } from "../../utils/git.ts";
|
||||
import { Workspace } from "../workspace/workspace.ts";
|
||||
import { removePathPrefix } from "../../types.ts";
|
||||
import { SyncCodebase, listSyncCodebases } from "../../utils/codebase.ts";
|
||||
@@ -67,9 +79,9 @@ function mergeCliWithEffectiveOptions<
|
||||
// Resolve effective sync options using branch-based configuration
|
||||
async function resolveEffectiveSyncOptions(
|
||||
workspace: Workspace,
|
||||
localConfig: SyncOptions,
|
||||
promotion?: string
|
||||
): Promise<SyncOptions> {
|
||||
const localConfig = await readConfigFile();
|
||||
return await getEffectiveSettings(localConfig, promotion);
|
||||
}
|
||||
|
||||
@@ -631,9 +643,36 @@ export async function elementsToMap(
|
||||
els: DynFSElement,
|
||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||
json: boolean,
|
||||
skips: Skips
|
||||
skips: Skips,
|
||||
specificItems?: SpecificItemsConfig
|
||||
): Promise<{ [key: string]: string }> {
|
||||
const map: { [key: string]: string } = {};
|
||||
const processedBasePaths = new Set<string>();
|
||||
|
||||
// First pass: collect all file paths to identify branch-specific files
|
||||
const allPaths: string[] = [];
|
||||
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
|
||||
if (!entry.isDirectory && !entry.ignored) {
|
||||
allPaths.push(entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
const branchSpecificExists = new Set<string>();
|
||||
|
||||
if (specificItems) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (currentBranch) {
|
||||
for (const path of allPaths) {
|
||||
if (isCurrentBranchFile(path)) {
|
||||
const basePath = fromBranchSpecificPath(path, currentBranch);
|
||||
if (isSpecificItem(basePath, specificItems)) {
|
||||
branchSpecificExists.add(basePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
|
||||
if (entry.isDirectory || entry.ignored) continue;
|
||||
const path = entry.path;
|
||||
@@ -695,11 +734,21 @@ export async function elementsToMap(
|
||||
"nu",
|
||||
"java",
|
||||
"rb",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
].includes(path.split(".").pop() ?? "") &&
|
||||
!isFileResource(path)
|
||||
)
|
||||
continue;
|
||||
|
||||
// Handle branch-specific files - skip files for other branches
|
||||
if (specificItems && isBranchSpecificFile(path)) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (!currentBranch || !isCurrentBranchFile(path)) {
|
||||
// Skip branch-specific files for other branches
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const content = await entry.getContentText();
|
||||
|
||||
if (skips.skipSecrets && path.endsWith(".variable" + ext)) {
|
||||
@@ -727,7 +776,33 @@ export async function elementsToMap(
|
||||
log.warn(`Error reading variable ${path} to check for secrets`);
|
||||
}
|
||||
}
|
||||
map[entry.path] = content;
|
||||
|
||||
// Handle branch-specific path mapping after all filtering
|
||||
if (specificItems) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (currentBranch && isCurrentBranchFile(path)) {
|
||||
// This is a branch-specific file for current branch
|
||||
const basePath = fromBranchSpecificPath(path, currentBranch);
|
||||
if (isSpecificItem(basePath, specificItems)) {
|
||||
// Map to base path for push operations
|
||||
map[basePath] = content;
|
||||
processedBasePaths.add(basePath);
|
||||
} else {
|
||||
// Branch-specific file doesn't match pattern, skip it
|
||||
continue;
|
||||
}
|
||||
} else if (!isBranchSpecificFile(path)) {
|
||||
// This is a regular base file, check if we should skip it
|
||||
if (processedBasePaths.has(path)) {
|
||||
// Skip base file, we already processed branch-specific version
|
||||
continue;
|
||||
}
|
||||
map[path] = content;
|
||||
}
|
||||
} else {
|
||||
// No specific items configuration, use regular path
|
||||
map[entry.path] = content;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -758,14 +833,15 @@ async function compareDynFSElement(
|
||||
skips: Skips,
|
||||
ignoreMetadataDeletion: boolean,
|
||||
codebases: SyncCodebase[],
|
||||
ignoreCodebaseChanges: boolean
|
||||
ignoreCodebaseChanges: boolean,
|
||||
specificItems?: SpecificItemsConfig
|
||||
): Promise<Change[]> {
|
||||
const [m1, m2] = els2
|
||||
? await Promise.all([
|
||||
elementsToMap(els1, ignore, json, skips),
|
||||
elementsToMap(els2, ignore, json, skips),
|
||||
elementsToMap(els1, ignore, json, skips, specificItems),
|
||||
elementsToMap(els2, ignore, json, skips, specificItems),
|
||||
])
|
||||
: [await elementsToMap(els1, ignore, json, skips), {}];
|
||||
: [await elementsToMap(els1, ignore, json, skips, specificItems), {}];
|
||||
|
||||
const changes: Change[] = [];
|
||||
|
||||
@@ -1163,6 +1239,10 @@ export async function pull(
|
||||
opts: GlobalOptions &
|
||||
SyncOptions & { repository?: string; promotion?: string }
|
||||
) {
|
||||
|
||||
const originalCliOpts = { ...opts };
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
|
||||
// Validate branch configuration early
|
||||
try {
|
||||
await validateBranchConfiguration(false, opts.yes);
|
||||
@@ -1184,11 +1264,15 @@ export async function pull(
|
||||
// Resolve effective sync options with branch awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(
|
||||
workspace,
|
||||
opts,
|
||||
opts.promotion
|
||||
);
|
||||
|
||||
// Extract specific items configuration before merging overwrites gitBranches
|
||||
const specificItems = getSpecificItemsForCurrentBranch(opts);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts);
|
||||
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
|
||||
@@ -1238,7 +1322,8 @@ export async function pull(
|
||||
opts,
|
||||
false,
|
||||
codebases,
|
||||
true
|
||||
true,
|
||||
specificItems
|
||||
);
|
||||
|
||||
log.info(
|
||||
@@ -1255,6 +1340,12 @@ export async function pull(
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(specificItems && isSpecificItem(change.path, specificItems)
|
||||
? {
|
||||
branch_specific: true,
|
||||
branch_specific_path: getBranchSpecificPath(change.path, specificItems)
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
total: changes.length,
|
||||
};
|
||||
@@ -1264,7 +1355,7 @@ export async function pull(
|
||||
|
||||
if (changes.length > 0) {
|
||||
if (!opts.jsonOutput) {
|
||||
prettyChanges(changes);
|
||||
prettyChanges(changes, specificItems);
|
||||
}
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
@@ -1284,8 +1375,17 @@ export async function pull(
|
||||
|
||||
log.info(colors.gray(`Applying changes to files ...`));
|
||||
for await (const change of changes) {
|
||||
const target = path.join(Deno.cwd(), change.path);
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||
// Determine if this file should be written to a branch-specific path
|
||||
let targetPath = change.path;
|
||||
if (specificItems && isSpecificItem(change.path, specificItems)) {
|
||||
const branchSpecificPath = getBranchSpecificPath(change.path, specificItems);
|
||||
if (branchSpecificPath) {
|
||||
targetPath = branchSpecificPath;
|
||||
}
|
||||
}
|
||||
|
||||
const target = path.join(Deno.cwd(), targetPath);
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", targetPath);
|
||||
if (change.name === "edited") {
|
||||
if (opts.stateful) {
|
||||
try {
|
||||
@@ -1328,12 +1428,12 @@ export async function pull(
|
||||
}
|
||||
}
|
||||
if (exts.some((e) => change.path.endsWith(e))) {
|
||||
log.info(`Editing script content of ${change.path}`);
|
||||
log.info(`Editing script content of ${targetPath}${targetPath !== change.path ? colors.gray(` (branch-specific override for ${change.path})`) : ""}`);
|
||||
} else if (
|
||||
change.path.endsWith(".yaml") ||
|
||||
change.path.endsWith(".json")
|
||||
) {
|
||||
log.info(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
log.info(`Editing ${getTypeStrFromPath(change.path)} ${targetPath}${targetPath !== change.path ? colors.gray(` (branch-specific override for ${change.path})`) : ""}`);
|
||||
}
|
||||
await Deno.writeTextFile(target, change.after);
|
||||
|
||||
@@ -1345,10 +1445,10 @@ export async function pull(
|
||||
await ensureDir(path.dirname(target));
|
||||
if (opts.stateful) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
log.info(`Adding ${getTypeStrFromPath(change.path)} ${targetPath}${targetPath !== change.path ? colors.gray(` (branch-specific override for ${change.path})`) : ""}`);
|
||||
}
|
||||
await Deno.writeTextFile(target, change.content);
|
||||
log.info(`Writing ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
log.info(`Writing ${getTypeStrFromPath(change.path)} ${targetPath}${targetPath !== change.path ? colors.gray(` (branch-specific override for ${change.path})`) : ""}`);
|
||||
if (opts.stateful) {
|
||||
await Deno.copyFile(target, stateTarget);
|
||||
}
|
||||
@@ -1423,6 +1523,12 @@ export async function pull(
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(specificItems && isSpecificItem(change.path, specificItems)
|
||||
? {
|
||||
branch_specific: true,
|
||||
branch_specific_path: getBranchSpecificPath(change.path, specificItems)
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
total: changes.length,
|
||||
};
|
||||
@@ -1445,21 +1551,33 @@ export async function pull(
|
||||
}
|
||||
}
|
||||
|
||||
function prettyChanges(changes: Change[]) {
|
||||
function prettyChanges(changes: Change[], specificItems?: SpecificItemsConfig) {
|
||||
for (const change of changes) {
|
||||
let displayPath = change.path;
|
||||
let branchNote = "";
|
||||
|
||||
// Check if this will be written as a branch-specific file
|
||||
if (specificItems && isSpecificItem(change.path, specificItems)) {
|
||||
const branchSpecificPath = getBranchSpecificPath(change.path, specificItems);
|
||||
if (branchSpecificPath) {
|
||||
displayPath = branchSpecificPath;
|
||||
branchNote = " (branch-specific)";
|
||||
}
|
||||
}
|
||||
|
||||
if (change.name === "added") {
|
||||
log.info(
|
||||
colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||
colors.green(`+ ${getTypeStrFromPath(change.path)} ` + displayPath + colors.gray(branchNote))
|
||||
);
|
||||
} else if (change.name === "deleted") {
|
||||
log.info(
|
||||
colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||
colors.red(`- ${getTypeStrFromPath(change.path)} ` + displayPath + colors.gray(branchNote))
|
||||
);
|
||||
} else if (change.name === "edited") {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`~ ${getTypeStrFromPath(change.path)} ` +
|
||||
change.path +
|
||||
displayPath + colors.gray(branchNote) +
|
||||
(change.codebase ? ` (codebase changed)` : "")
|
||||
)
|
||||
);
|
||||
@@ -1499,6 +1617,12 @@ function removeSuffix(str: string, suffix: string) {
|
||||
export async function push(
|
||||
opts: GlobalOptions & SyncOptions & { repository?: string }
|
||||
) {
|
||||
// Save original CLI options before merging with config file
|
||||
const originalCliOpts = { ...opts };
|
||||
|
||||
// Load configuration from wmill.yaml and merge with CLI options
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
|
||||
// Validate branch configuration early
|
||||
try {
|
||||
await validateBranchConfiguration(false, opts.yes);
|
||||
@@ -1516,11 +1640,15 @@ export async function push(
|
||||
// Resolve effective sync options with branch awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(
|
||||
workspace,
|
||||
opts,
|
||||
opts.promotion
|
||||
);
|
||||
|
||||
// Extract specific items configuration BEFORE merging overwrites gitBranches
|
||||
const specificItems = getSpecificItemsForCurrentBranch(opts);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts);
|
||||
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
if (opts.raw) {
|
||||
@@ -1581,7 +1709,8 @@ export async function push(
|
||||
opts,
|
||||
true,
|
||||
codebases,
|
||||
false
|
||||
false,
|
||||
specificItems
|
||||
);
|
||||
|
||||
const globalDeps = await findGlobalDeps();
|
||||
@@ -1660,6 +1789,12 @@ export async function push(
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(specificItems && isSpecificItem(change.path, specificItems)
|
||||
? {
|
||||
branch_specific: true,
|
||||
branch_specific_path: getBranchSpecificPath(change.path, specificItems)
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
total: changes.length,
|
||||
};
|
||||
@@ -1669,7 +1804,7 @@ export async function push(
|
||||
|
||||
if (changes.length > 0) {
|
||||
if (!opts.jsonOutput) {
|
||||
prettyChanges(changes);
|
||||
prettyChanges(changes, specificItems);
|
||||
}
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
@@ -2041,6 +2176,12 @@ export async function push(
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(specificItems && isSpecificItem(change.path, specificItems)
|
||||
? {
|
||||
branch_specific: true,
|
||||
branch_specific_path: getBranchSpecificPath(change.path, specificItems)
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
total: changes.length,
|
||||
duration_ms: Math.round(performance.now() - start),
|
||||
|
||||
@@ -386,22 +386,22 @@ async function bind(
|
||||
}
|
||||
|
||||
// For unbind, check if branch exists
|
||||
if (!bindWorkspace && (!config.git_branches || !config.git_branches[branch])) {
|
||||
log.error(colors.red(`Branch '${branch}' not found in wmill.yaml git_branches`));
|
||||
if (!bindWorkspace && (!config.gitBranches || !config.gitBranches[branch])) {
|
||||
log.error(colors.red(`Branch '${branch}' not found in wmill.yaml gitBranches`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the branch configuration with workspace binding
|
||||
if (!config.git_branches) {
|
||||
config.git_branches = {};
|
||||
if (!config.gitBranches) {
|
||||
config.gitBranches = {};
|
||||
}
|
||||
if (!config.git_branches[branch]) {
|
||||
config.git_branches[branch] = { overrides: {} };
|
||||
if (!config.gitBranches[branch]) {
|
||||
config.gitBranches[branch] = { overrides: {} };
|
||||
}
|
||||
|
||||
if (bindWorkspace && activeWorkspace) {
|
||||
config.git_branches[branch].baseUrl = activeWorkspace.remote;
|
||||
config.git_branches[branch].workspaceId = activeWorkspace.workspaceId;
|
||||
config.gitBranches[branch].baseUrl = activeWorkspace.remote;
|
||||
config.gitBranches[branch].workspaceId = activeWorkspace.workspaceId;
|
||||
|
||||
log.info(colors.green(
|
||||
`✓ Bound branch '${branch}' to workspace '${activeWorkspace.name}'\n` +
|
||||
@@ -409,8 +409,8 @@ async function bind(
|
||||
));
|
||||
} else {
|
||||
// Unbind
|
||||
delete config.git_branches[branch].baseUrl;
|
||||
delete config.git_branches[branch].workspaceId;
|
||||
delete config.gitBranches[branch].baseUrl;
|
||||
delete config.gitBranches[branch].workspaceId;
|
||||
|
||||
log.info(colors.green(`✓ Removed workspace binding from branch '${branch}'`));
|
||||
}
|
||||
|
||||
+210
-35
@@ -1,5 +1,8 @@
|
||||
import { log, yamlParseFile, Confirm, yamlStringify } from "../../deps.ts";
|
||||
import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts";
|
||||
import { join, dirname, resolve, relative } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export let showDiffs = false;
|
||||
export function setShowDiffs(value: boolean) {
|
||||
@@ -37,13 +40,40 @@ export interface SyncOptions {
|
||||
codebases?: Codebase[];
|
||||
parallel?: number;
|
||||
jsonOutput?: boolean;
|
||||
git_branches?: {
|
||||
gitBranches?: {
|
||||
commonSpecificItems?: {
|
||||
variables?: string[];
|
||||
resources?: string[];
|
||||
};
|
||||
} & {
|
||||
[branchName: string]: SyncOptions & {
|
||||
overrides?: Partial<SyncOptions>;
|
||||
promotionOverrides?: Partial<SyncOptions>;
|
||||
baseUrl?: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
specificItems?: {
|
||||
variables?: string[];
|
||||
resources?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
// Legacy field - deprecated, use gitBranches instead
|
||||
git_branches?: {
|
||||
commonSpecificItems?: {
|
||||
variables?: string[];
|
||||
resources?: string[];
|
||||
};
|
||||
} & {
|
||||
[branchName: string]: SyncOptions & {
|
||||
overrides?: Partial<SyncOptions>;
|
||||
promotionOverrides?: Partial<SyncOptions>;
|
||||
baseUrl?: string;
|
||||
workspaceId?: string;
|
||||
specificItems?: {
|
||||
variables?: string[];
|
||||
resources?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
promotion?: string;
|
||||
}
|
||||
@@ -62,9 +92,90 @@ export interface Codebase {
|
||||
inject?: string[];
|
||||
}
|
||||
|
||||
function getGitRepoRoot(): string | null {
|
||||
try {
|
||||
const result = execSync("git rev-parse --show-toplevel", {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe"
|
||||
});
|
||||
return result.trim();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findWmillYaml(): string | null {
|
||||
const startDir = resolve(Deno.cwd());
|
||||
const isInGitRepo = isGitRepository();
|
||||
|
||||
// If not in git repo, only check current directory
|
||||
if (!isInGitRepo) {
|
||||
const wmillYamlPath = join(startDir, "wmill.yaml");
|
||||
return existsSync(wmillYamlPath) ? wmillYamlPath : null;
|
||||
}
|
||||
|
||||
// If in git repo, search up to git repository root
|
||||
const gitRoot = getGitRepoRoot();
|
||||
let currentDir = startDir;
|
||||
let foundPath: string | null = null;
|
||||
|
||||
while (true) {
|
||||
const wmillYamlPath = join(currentDir, "wmill.yaml");
|
||||
|
||||
if (existsSync(wmillYamlPath)) {
|
||||
foundPath = wmillYamlPath;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we've reached the git repository root
|
||||
if (gitRoot && resolve(currentDir) === resolve(gitRoot)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we've reached the filesystem root
|
||||
const parentDir = dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
// If wmill.yaml was found in a parent directory, warn the user and change working directory
|
||||
if (foundPath && resolve(dirname(foundPath)) !== resolve(startDir)) {
|
||||
const configDir = dirname(foundPath);
|
||||
const relativePath = relative(startDir, foundPath);
|
||||
log.warn(`⚠️ wmill.yaml found in parent directory: ${relativePath}`);
|
||||
|
||||
// Change working directory to where wmill.yaml was found
|
||||
Deno.chdir(configDir);
|
||||
log.info(`📁 Changed working directory to: ${configDir}`);
|
||||
}
|
||||
|
||||
return foundPath;
|
||||
}
|
||||
|
||||
export function getWmillYamlPath(): string | null {
|
||||
return findWmillYaml();
|
||||
}
|
||||
|
||||
export async function readConfigFile(): Promise<SyncOptions> {
|
||||
try {
|
||||
const conf = (await yamlParseFile("wmill.yaml")) as SyncOptions;
|
||||
// First, try to find wmill.yaml recursively
|
||||
const wmillYamlPath = findWmillYaml();
|
||||
|
||||
if (!wmillYamlPath) {
|
||||
log.warn(
|
||||
"No wmill.yaml found. Use 'wmill init' to bootstrap it. Using 'bun' as default typescript runtime."
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
const conf = (await yamlParseFile(wmillYamlPath)) as SyncOptions;
|
||||
|
||||
// Handle legacy format migrations (combine overrides and git_branches)
|
||||
let needsConfigWrite = false;
|
||||
const migrationMessages: string[] = [];
|
||||
|
||||
// Handle obsolete overrides format
|
||||
if (conf && 'overrides' in conf) {
|
||||
@@ -78,18 +189,54 @@ export async function readConfigFile(): Promise<SyncOptions> {
|
||||
" Please delete your wmill.yaml and run 'wmill init' to recreate it with the new format."
|
||||
);
|
||||
} else {
|
||||
// Remove empty overrides with a note
|
||||
log.info("ℹ️ Removing empty 'overrides: {}' from wmill.yaml (migrated to git_branches format)");
|
||||
// Remove empty overrides
|
||||
delete conf.overrides;
|
||||
// Write the updated config back to file
|
||||
try {
|
||||
await Deno.writeTextFile("wmill.yaml", yamlStringify(conf));
|
||||
} catch (error) {
|
||||
log.warn(`Could not update wmill.yaml to remove empty overrides: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
needsConfigWrite = true;
|
||||
migrationMessages.push("ℹ️ Removing empty 'overrides: {}' from wmill.yaml (migrated to gitBranches format)");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle git_branches to gitBranches migration
|
||||
if (conf && 'git_branches' in conf) {
|
||||
if (!conf.gitBranches) {
|
||||
// Deep copy git_branches to gitBranches (even if empty)
|
||||
conf.gitBranches = JSON.parse(JSON.stringify(conf.git_branches));
|
||||
needsConfigWrite = true;
|
||||
migrationMessages.push("⚠️ Migrating 'git_branches' to 'gitBranches' (camelCase). The snake_case format is deprecated.");
|
||||
migrationMessages.push("✅ Successfully migrated 'git_branches' to 'gitBranches' in wmill.yaml");
|
||||
} else {
|
||||
migrationMessages.push("⚠️ Both 'git_branches' and 'gitBranches' found in wmill.yaml. Using 'gitBranches' and ignoring 'git_branches'.");
|
||||
}
|
||||
// Always remove the old field from config object (both file and memory)
|
||||
delete conf.git_branches;
|
||||
}
|
||||
|
||||
// Perform single atomic write if any migrations are needed
|
||||
if (needsConfigWrite) {
|
||||
try {
|
||||
await Deno.writeTextFile(wmillYamlPath, yamlStringify(conf));
|
||||
// Log all migration messages after successful write
|
||||
migrationMessages.forEach(msg => {
|
||||
if (msg.startsWith('⚠️')) {
|
||||
log.warn(msg);
|
||||
} else {
|
||||
log.info(msg);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn(`Could not update wmill.yaml to apply migrations: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
} else if (migrationMessages.length > 0) {
|
||||
// Log messages for non-write cases (like "both found")
|
||||
migrationMessages.forEach(msg => {
|
||||
if (msg.startsWith('⚠️')) {
|
||||
log.warn(msg);
|
||||
} else {
|
||||
log.info(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (conf?.defaultTs == undefined) {
|
||||
log.warn(
|
||||
"No defaultTs defined in your wmill.yaml. Using 'bun' as default."
|
||||
@@ -100,10 +247,23 @@ export async function readConfigFile(): Promise<SyncOptions> {
|
||||
if (e instanceof Error && (e.message.includes("overrides") || e.message.includes("Obsolete configuration format"))) {
|
||||
throw e; // Re-throw the specific obsolete format error
|
||||
}
|
||||
log.warn(
|
||||
"No wmill.yaml found. Use 'wmill init' to bootstrap it. Using 'bun' as default typescript runtime."
|
||||
);
|
||||
return {};
|
||||
|
||||
// Since we already found the file path, this is likely a parsing or access error
|
||||
if (e instanceof Error && e.message.includes("Error parsing yaml")) {
|
||||
const yamlError = e.cause instanceof Error ? e.cause.message : String(e.cause);
|
||||
throw new Error(
|
||||
"❌ YAML syntax error in wmill.yaml:\n" +
|
||||
" " + yamlError + "\n" +
|
||||
" Please fix the YAML syntax in wmill.yaml or delete the file to start fresh."
|
||||
);
|
||||
} else {
|
||||
// File exists but has other issues (permissions, etc.)
|
||||
throw new Error(
|
||||
"❌ Failed to read wmill.yaml:\n" +
|
||||
" " + (e instanceof Error ? e.message : String(e)) + "\n" +
|
||||
" Please check file permissions or fix the syntax."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,26 +308,26 @@ export async function validateBranchConfiguration(skipValidation?: boolean, auto
|
||||
}
|
||||
|
||||
const config = await readConfigFile();
|
||||
const { git_branches } = config;
|
||||
const { gitBranches } = config;
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
|
||||
// In a git repository, git_branches section is recommended
|
||||
if (!git_branches || Object.keys(git_branches).length === 0) {
|
||||
// In a git repository, gitBranches section is recommended
|
||||
if (!gitBranches || Object.keys(gitBranches).length === 0) {
|
||||
log.warn(
|
||||
"⚠️ WARNING: In a Git repository, the 'git_branches' section is recommended in wmill.yaml.\n" +
|
||||
" Consider adding a git_branches section with configuration for your Git branches.\n" +
|
||||
"⚠️ WARNING: In a Git repository, the 'gitBranches' section is recommended in wmill.yaml.\n" +
|
||||
" Consider adding a gitBranches section with configuration for your Git branches.\n" +
|
||||
" Run 'wmill init' to recreate the configuration file with proper branch setup."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Current branch must be defined in git_branches config
|
||||
if (currentBranch && !git_branches[currentBranch]) {
|
||||
// Current branch must be defined in gitBranches config
|
||||
if (currentBranch && !gitBranches[currentBranch]) {
|
||||
// In interactive mode, offer to create the branch
|
||||
if (Deno.stdin.isTerminal()) {
|
||||
const availableBranches = Object.keys(git_branches).join(', ');
|
||||
const availableBranches = Object.keys(gitBranches).join(', ');
|
||||
log.info(
|
||||
`Current Git branch '${currentBranch}' is not defined in the git_branches configuration.\n` +
|
||||
`Current Git branch '${currentBranch}' is not defined in the gitBranches configuration.\n` +
|
||||
`Available branches: ${availableBranches}`
|
||||
);
|
||||
|
||||
@@ -177,13 +337,21 @@ export async function validateBranchConfiguration(skipValidation?: boolean, auto
|
||||
});
|
||||
|
||||
if (shouldCreate) {
|
||||
// Warn if branch name contains filesystem-unsafe characters
|
||||
if (/[\/\\:*?"<>|.]/.test(currentBranch)) {
|
||||
const sanitizedBranchName = currentBranch.replace(/[\/\\:*?"<>|.]/g, '_');
|
||||
log.warn(`⚠️ WARNING: Branch name "${currentBranch}" contains filesystem-unsafe characters (/ \\ : * ? " < > | .).`);
|
||||
log.warn(` Branch-specific files will be saved with sanitized name: "${sanitizedBranchName}"`);
|
||||
log.warn(` Example: "file.variable.yaml" → "file.${sanitizedBranchName}.variable.yaml"`);
|
||||
}
|
||||
|
||||
// Read current config, add branch, and write it back
|
||||
const currentConfig = await readConfigFile();
|
||||
|
||||
if (!currentConfig.git_branches) {
|
||||
currentConfig.git_branches = {};
|
||||
if (!currentConfig.gitBranches) {
|
||||
currentConfig.gitBranches = {};
|
||||
}
|
||||
currentConfig.git_branches[currentBranch] = { overrides: {} };
|
||||
currentConfig.gitBranches[currentBranch] = { overrides: {} };
|
||||
|
||||
await Deno.writeTextFile("wmill.yaml", yamlStringify(currentConfig));
|
||||
|
||||
@@ -193,10 +361,17 @@ export async function validateBranchConfiguration(skipValidation?: boolean, auto
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Warn about filesystem-unsafe characters in branch name
|
||||
if (/[\/\\:*?"<>|.]/.test(currentBranch)) {
|
||||
const sanitizedBranchName = currentBranch.replace(/[\/\\:*?"<>|.]/g, '_');
|
||||
log.warn(`⚠️ WARNING: Branch name "${currentBranch}" contains filesystem-unsafe characters (/ \\ : * ? " < > | .).`);
|
||||
log.warn(` Branch-specific files will use sanitized name: "${sanitizedBranchName}"`);
|
||||
}
|
||||
|
||||
log.warn(
|
||||
`⚠️ WARNING: Current Git branch '${currentBranch}' is not defined in the git_branches configuration.\n` +
|
||||
` Consider adding configuration for branch '${currentBranch}' in the git_branches section of wmill.yaml.\n` +
|
||||
` Available branches: ${Object.keys(git_branches).join(', ')}`
|
||||
`⚠️ WARNING: Current Git branch '${currentBranch}' is not defined in the gitBranches configuration.\n` +
|
||||
` Consider adding configuration for branch '${currentBranch}' in the gitBranches section of wmill.yaml.\n` +
|
||||
` Available branches: ${Object.keys(gitBranches).join(', ')}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -206,15 +381,15 @@ export async function validateBranchConfiguration(skipValidation?: boolean, auto
|
||||
// Get effective settings by merging top-level settings with branch-specific overrides
|
||||
export async function getEffectiveSettings(config: SyncOptions, promotion?: string, skipBranchValidation?: boolean, suppressLogs?: boolean): Promise<SyncOptions> {
|
||||
// Start with top-level settings from config
|
||||
const { git_branches, ...topLevelSettings } = config;
|
||||
const { gitBranches, ...topLevelSettings } = config;
|
||||
let effective = { ...topLevelSettings };
|
||||
|
||||
if (isGitRepository()) {
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
|
||||
// If promotion is specified, use that branch's promotionOverrides or overrides
|
||||
if (promotion && git_branches && git_branches[promotion]) {
|
||||
const targetBranch = git_branches[promotion];
|
||||
if (promotion && gitBranches && gitBranches[promotion]) {
|
||||
const targetBranch = gitBranches[promotion];
|
||||
|
||||
// First try promotionOverrides, then fall back to overrides
|
||||
if (targetBranch.promotionOverrides) {
|
||||
@@ -232,8 +407,8 @@ export async function getEffectiveSettings(config: SyncOptions, promotion?: stri
|
||||
}
|
||||
}
|
||||
// Otherwise use current branch overrides (existing behavior)
|
||||
else if (currentBranch && git_branches && git_branches[currentBranch] && git_branches[currentBranch].overrides) {
|
||||
Object.assign(effective, git_branches[currentBranch].overrides);
|
||||
else if (currentBranch && gitBranches && gitBranches[currentBranch] && gitBranches[currentBranch].overrides) {
|
||||
Object.assign(effective, gitBranches[currentBranch].overrides);
|
||||
if (!suppressLogs) {
|
||||
log.info(`Applied settings for Git branch: ${currentBranch}`);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ async function tryResolveBranchWorkspace(
|
||||
|
||||
// Read wmill.yaml to check for branch workspace configuration
|
||||
const config = await readConfigFile();
|
||||
const branchConfig = config.git_branches?.[currentBranch];
|
||||
const branchConfig = config.gitBranches?.[currentBranch];
|
||||
|
||||
// Check if branch has workspace configuration
|
||||
if (!branchConfig?.baseUrl || !branchConfig?.workspaceId) {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { minimatch } from "../../deps.ts";
|
||||
import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts";
|
||||
import { SyncOptions } from "./conf.ts";
|
||||
|
||||
export interface SpecificItemsConfig {
|
||||
variables?: string[];
|
||||
resources?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specific items configuration for the current git branch
|
||||
* Merges commonSpecificItems with branch-specific specificItems
|
||||
*/
|
||||
export function getSpecificItemsForCurrentBranch(config: SyncOptions): SpecificItemsConfig | undefined {
|
||||
if (!isGitRepository() || !config.gitBranches) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (!currentBranch) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const commonItems = config.gitBranches.commonSpecificItems;
|
||||
const branchItems = config.gitBranches[currentBranch]?.specificItems;
|
||||
|
||||
// If neither common nor branch-specific items exist, return undefined
|
||||
if (!commonItems && !branchItems) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Merge common and branch-specific items
|
||||
const merged: SpecificItemsConfig = {};
|
||||
|
||||
// Add common items
|
||||
if (commonItems?.variables) {
|
||||
merged.variables = [...commonItems.variables];
|
||||
}
|
||||
if (commonItems?.resources) {
|
||||
merged.resources = [...commonItems.resources];
|
||||
}
|
||||
|
||||
// Add branch-specific items (extending common items)
|
||||
if (branchItems?.variables) {
|
||||
merged.variables = [...(merged.variables || []), ...branchItems.variables];
|
||||
}
|
||||
if (branchItems?.resources) {
|
||||
merged.resources = [...(merged.resources || []), ...branchItems.resources];
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path matches any of the patterns in the given list
|
||||
*/
|
||||
function matchesPatterns(path: string, patterns: string[]): boolean {
|
||||
return patterns.some(pattern => minimatch(path, pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path should be treated as branch-specific
|
||||
*/
|
||||
export function isSpecificItem(path: string, specificItems: SpecificItemsConfig | undefined): boolean {
|
||||
if (!specificItems) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the item type from the file path
|
||||
if (path.endsWith('.variable.yaml')) {
|
||||
return specificItems.variables ? matchesPatterns(path, specificItems.variables) : false;
|
||||
}
|
||||
|
||||
if (path.endsWith('.resource.yaml')) {
|
||||
return specificItems.resources ? matchesPatterns(path, specificItems.resources) : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a base path to a branch-specific path
|
||||
*/
|
||||
export function toBranchSpecificPath(basePath: string, branchName: string): string {
|
||||
// Extract the extension (e.g., ".variable.yaml" or ".resource.yaml")
|
||||
const extensionMatch = basePath.match(/(\.(variable|resource)\.yaml)$/);
|
||||
if (!extensionMatch) {
|
||||
return basePath; // Return unchanged if no recognized extension
|
||||
}
|
||||
|
||||
const extension = extensionMatch[1];
|
||||
const pathWithoutExtension = basePath.substring(0, basePath.length - extension.length);
|
||||
|
||||
// Sanitize branch name to be filesystem-safe
|
||||
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
|
||||
|
||||
// Warn about potential collisions if sanitization occurred
|
||||
if (sanitizedBranchName !== branchName) {
|
||||
console.warn(`Warning: Branch name "${branchName}" contains filesystem-unsafe characters (/ \\ : * ? " < > | .) and was sanitized to "${sanitizedBranchName}". This may cause collisions with other similarly named branches.`);
|
||||
}
|
||||
|
||||
return `${pathWithoutExtension}.${sanitizedBranchName}${extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a branch-specific path back to a base path
|
||||
*/
|
||||
export function fromBranchSpecificPath(branchSpecificPath: string, branchName: string): string {
|
||||
// Sanitize branch name the same way as in toBranchSpecificPath
|
||||
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
|
||||
|
||||
// Pattern: path.sanitizedBranchName.extension
|
||||
const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`\\.${escapedBranchName}(\\.(variable|resource)\\.yaml)$`);
|
||||
const match = branchSpecificPath.match(pattern);
|
||||
|
||||
if (!match) {
|
||||
return branchSpecificPath; // Return unchanged if not a branch-specific path
|
||||
}
|
||||
|
||||
const extension = match[1];
|
||||
const pathWithoutBranchAndExtension = branchSpecificPath.substring(
|
||||
0,
|
||||
branchSpecificPath.length - `.${sanitizedBranchName}${extension}`.length
|
||||
);
|
||||
|
||||
return `${pathWithoutBranchAndExtension}${extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branch-specific path for the current branch if the item should be branch-specific
|
||||
*/
|
||||
export function getBranchSpecificPath(
|
||||
basePath: string,
|
||||
specificItems: SpecificItemsConfig | undefined
|
||||
): string | undefined {
|
||||
if (!isGitRepository() || !specificItems) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (!currentBranch) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isSpecificItem(basePath, specificItems)) {
|
||||
return toBranchSpecificPath(basePath, currentBranch);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Cache for compiled regex patterns to avoid recompilation
|
||||
const branchPatternCache = new Map<string, RegExp>();
|
||||
|
||||
/**
|
||||
* Check if a path is a branch-specific file for the current branch
|
||||
*/
|
||||
export function isCurrentBranchFile(path: string): boolean {
|
||||
if (!isGitRepository()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentBranch = getCurrentGitBranch();
|
||||
if (!currentBranch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached pattern or create and cache new one
|
||||
let pattern = branchPatternCache.get(currentBranch);
|
||||
if (!pattern) {
|
||||
pattern = new RegExp(`\\.${currentBranch}\\.(variable|resource)\\.yaml$`);
|
||||
branchPatternCache.set(currentBranch, pattern);
|
||||
}
|
||||
|
||||
return pattern.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is a branch-specific file for ANY branch (not necessarily current)
|
||||
* Used to identify and skip files from other branches during sync operations
|
||||
*/
|
||||
export function isBranchSpecificFile(path: string): boolean {
|
||||
// Pattern: *.branchName.variable.yaml or *.branchName.resource.yaml
|
||||
return /\.[^.]+\.(variable|resource)\.yaml$/.test(path);
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
"gitSync_9": "hub/19738/sync-script-to-git-repo-windmill",
|
||||
"gitSync_10": "hub/19785/sync-script-to-git-repo-windmill",
|
||||
"gitSync_11": "hub/19789/sync-script-to-git-repo-windmill",
|
||||
"gitSync": "hub/19798/sync-script-to-git-repo-windmill",
|
||||
"gitSync_12": "hub/19798/sync-script-to-git-repo-windmill",
|
||||
"gitSync": "hub/19801/sync-script-to-git-repo-windmill",
|
||||
"gitSyncTest_0": "hub/9073/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_1": "hub/11499/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_2": "hub/11667/git-repo-test-read-write-windmill",
|
||||
|
||||
Reference in New Issue
Block a user