feat(cli): add --branch option to sync pull/push commands

Add a --branch argument that allows overriding the current git branch
for sync operations. This enables:
- Using branch-specific settings even when not in a git repository
- Overriding the detected git branch when needed

The branch override is applied to:
- getEffectiveSettings() for branch-specific config overrides
- getSpecificItemsForCurrentBranch() for branch-specific items

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-01-16 18:57:36 +00:00
parent 7e1f116c1f
commit fac390a990
3 changed files with 77 additions and 51 deletions

View File

@@ -109,8 +109,9 @@ async function resolveEffectiveSyncOptions(
workspace: Workspace,
localConfig: SyncOptions,
promotion?: string,
branchOverride?: string,
): Promise<SyncOptions> {
return await getEffectiveSettings(localConfig, promotion);
return await getEffectiveSettings(localConfig, promotion, false, false, branchOverride);
}
type DynFSElement = {
@@ -1858,7 +1859,7 @@ async function buildTracker(changes: Change[]) {
export async function pull(
opts: GlobalOptions &
SyncOptions & { repository?: string; promotion?: string },
SyncOptions & { repository?: string; promotion?: string; branch?: string },
) {
const originalCliOpts = { ...opts };
opts = await mergeConfigWithConfigFile(opts);
@@ -1886,10 +1887,11 @@ export async function pull(
workspace,
opts,
opts.promotion,
opts.branch,
);
// Extract specific items configuration before merging overwrites gitBranches
const specificItems = getSpecificItemsForCurrentBranch(opts);
const specificItems = getSpecificItemsForCurrentBranch(opts, opts.branch);
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts);
@@ -2334,7 +2336,7 @@ function removeSuffix(str: string, suffix: string) {
}
export async function push(
opts: GlobalOptions & SyncOptions & { repository?: string },
opts: GlobalOptions & SyncOptions & { repository?: string; branch?: string },
) {
// Save original CLI options before merging with config file
const originalCliOpts = { ...opts };
@@ -2361,10 +2363,11 @@ export async function push(
workspace,
opts,
opts.promotion,
opts.branch,
);
// Extract specific items configuration BEFORE merging overwrites gitBranches
const specificItems = getSpecificItemsForCurrentBranch(opts);
const specificItems = getSpecificItemsForCurrentBranch(opts, opts.branch);
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts);
@@ -3118,6 +3121,10 @@ const command = new Command()
"--promotion <branch:string>",
"Use promotionOverrides from the specified branch instead of regular overrides",
)
.option(
"--branch <branch:string>",
"Override the current git branch (works even outside a git repository)",
)
// deno-lint-ignore no-explicit-any
.action(pull as any)
.command("push")
@@ -3171,6 +3178,10 @@ const command = new Command()
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo) when multiple repositories exist",
)
.option(
"--branch <branch:string>",
"Override the current git branch (works even outside a git repository)",
)
// deno-lint-ignore no-explicit-any
.action(push as any);

View File

@@ -487,18 +487,26 @@ export async function getEffectiveSettings(
config: SyncOptions,
promotion?: string,
skipBranchValidation?: boolean,
suppressLogs?: boolean
suppressLogs?: boolean,
branchOverride?: string
): Promise<SyncOptions> {
// Start with top-level settings from config
const { gitBranches, ...topLevelSettings } = config;
const effective = { ...topLevelSettings };
if (isGitRepository()) {
// Determine the branch to use: branchOverride takes precedence, then git detection
let currentBranch: string | null = null;
let originalBranchIfForked: string | null = null;
if (branchOverride) {
currentBranch = branchOverride;
if (!suppressLogs) {
log.info(`Using branch override: ${branchOverride}`);
}
} else if (isGitRepository()) {
const branch = getCurrentGitBranch();
originalBranchIfForked = getOriginalBranchForWorkspaceForks(branch);
const originalBranchIfForked = getOriginalBranchForWorkspaceForks(branch);
let currentBranch: string | null;
if (originalBranchIfForked) {
log.info(
`Using overrides from original branch \`${originalBranchIfForked}\``
@@ -507,53 +515,53 @@ export async function getEffectiveSettings(
} else {
currentBranch = branch;
}
} else {
log.debug("Not in a Git repository and no branch override provided, using top-level settings");
}
// If promotion is specified, use that branch's promotionOverrides or overrides
if (promotion && gitBranches && gitBranches[promotion]) {
const targetBranch = gitBranches[promotion];
// If promotion is specified, use that branch's promotionOverrides or overrides
if (promotion && gitBranches && gitBranches[promotion]) {
const targetBranch = gitBranches[promotion];
// First try promotionOverrides, then fall back to overrides
if (targetBranch.promotionOverrides) {
Object.assign(effective, targetBranch.promotionOverrides);
if (!suppressLogs) {
log.info(`Applied promotion settings from branch: ${promotion}`);
}
} else if (targetBranch.overrides) {
Object.assign(effective, targetBranch.overrides);
if (!suppressLogs) {
log.info(
`Applied settings from branch: ${promotion} (no promotionOverrides found)`
);
}
} else {
log.debug(
`No promotion or regular overrides found for branch '${promotion}', using top-level settings`
);
}
}
// Otherwise use current branch overrides (existing behavior)
else if (
currentBranch &&
gitBranches &&
gitBranches[currentBranch] &&
gitBranches[currentBranch].overrides
) {
Object.assign(effective, gitBranches[currentBranch].overrides);
// First try promotionOverrides, then fall back to overrides
if (targetBranch.promotionOverrides) {
Object.assign(effective, targetBranch.promotionOverrides);
if (!suppressLogs) {
log.info(`Applied promotion settings from branch: ${promotion}`);
}
} else if (targetBranch.overrides) {
Object.assign(effective, targetBranch.overrides);
if (!suppressLogs) {
const extraLog = originalBranchIfForked
? ` (because it is the origin of the workspace fork branch \`${branch}\`)`
: "";
log.info(
`Applied settings for Git branch: ${currentBranch}${extraLog}`
`Applied settings from branch: ${promotion} (no promotionOverrides found)`
);
}
} else if (currentBranch) {
} else {
log.debug(
`No branch-specific overrides found for '${currentBranch}', using top-level settings`
`No promotion or regular overrides found for branch '${promotion}', using top-level settings`
);
}
} else {
log.debug("Not in a Git repository, using top-level settings");
}
// Otherwise use current branch overrides (existing behavior)
else if (
currentBranch &&
gitBranches &&
gitBranches[currentBranch] &&
gitBranches[currentBranch].overrides
) {
Object.assign(effective, gitBranches[currentBranch].overrides);
if (!suppressLogs) {
const extraLog = originalBranchIfForked
? ` (because it is the origin of the workspace fork branch \`${currentBranch}\`)`
: "";
log.info(
`Applied settings for Git branch: ${currentBranch}${extraLog}`
);
}
} else if (currentBranch) {
log.debug(
`No branch-specific overrides found for '${currentBranch}', using top-level settings`
);
}
return effective;

View File

@@ -60,12 +60,19 @@ function buildYamlTypePattern(): 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) {
export function getSpecificItemsForCurrentBranch(config: SyncOptions, branchOverride?: string): SpecificItemsConfig | undefined {
if (!config.gitBranches) {
return undefined;
}
const currentBranch = getCurrentGitBranch();
// Use branch override if provided, otherwise detect from git
let currentBranch: string | null = null;
if (branchOverride) {
currentBranch = branchOverride;
} else if (isGitRepository()) {
currentBranch = getCurrentGitBranch();
}
if (!currentBranch) {
return undefined;
}