Compare commits

...

1 Commits
v1 ... v1.5.0

Author SHA1 Message Date
5e9b134168 Add --force flag to reverse sync
Bypasses the PR step and force-pushes the subrepo's state directly onto
the monorepo branch. Also skips the skip-dirty guard. Only valid with
--reverse. Returns status "forced".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:43:47 +03:00
6 changed files with 74 additions and 20 deletions

View File

@@ -1,5 +1,17 @@
# Changelog # Changelog
## 1.5.0
### Breaking Changes
_None._
### Features
- **`--force` flag for reverse sync**: `josh-sync sync --reverse --force` bypasses
the PR step and force-pushes the subrepo's state directly onto the monorepo branch.
Also skips the `skip-dirty` guard. Only valid with `--reverse`. Returns status `forced`.
## 1.4.0 ## 1.4.0
### Breaking Changes ### Breaking Changes

View File

@@ -78,7 +78,7 @@ CI workflows pin to a floating major tag (e.g. `@v1`). A breaking change bumps t
## CLI ## CLI
``` ```
josh-sync sync [--forward|--reverse] [--target NAME[,NAME]] [--branch BRANCH] josh-sync sync [--forward|--reverse] [--force] [--target NAME[,NAME]] [--branch BRANCH]
josh-sync preflight josh-sync preflight
josh-sync import <target> josh-sync import <target>
josh-sync reset <target> josh-sync reset <target>
@@ -92,7 +92,7 @@ josh-sync state reset <target> [branch]
## How It Works ## How It Works
- **Forward sync** (mono → subrepo): pushes directly if clean, creates conflict PR if not. Uses `--force-with-lease` for safety. - **Forward sync** (mono → subrepo): pushes directly if clean, creates conflict PR if not. Uses `--force-with-lease` for safety.
- **Reverse sync** (subrepo → mono): always creates a PR, never pushes directly. - **Reverse sync** (subrepo → mono): creates a PR by default. Add `--force` to bypass the PR and push directly to the mono branch (destructive).
- **File exclusion**: `exclude` patterns are embedded inline in the josh-proxy URL. Excluded files exist only in the monorepo. - **File exclusion**: `exclude` patterns are embedded inline in the josh-proxy URL. Excluded files exist only in the monorepo.
- **Filter reconciliation**: Changing the exclude list auto-creates a merge commit that connects old and new histories — no force-push needed. - **Filter reconciliation**: Changing the exclude list auto-creates a merge commit that connects old and new histories — no force-push needed.
- **Loop prevention**: `Josh-Sync-Origin:` git trailer filters out bot commits. - **Loop prevention**: `Josh-Sync-Origin:` git trailer filters out bot commits.

View File

@@ -1 +1 @@
1.4.0 1.5.0

View File

@@ -88,6 +88,7 @@ Global flags:
Sync flags: Sync flags:
--forward Forward only (mono → subrepo) --forward Forward only (mono → subrepo)
--reverse Reverse only (subrepo → mono) --reverse Reverse only (subrepo → mono)
--force Force-push subrepo state to mono branch (--reverse only, skips PR)
--target NAME Filter to target(s) — comma-separated for multiple (env: JOSH_SYNC_TARGET) --target NAME Filter to target(s) — comma-separated for multiple (env: JOSH_SYNC_TARGET)
--branch BRANCH Filter to one branch --branch BRANCH Filter to one branch
@@ -108,6 +109,7 @@ cmd_sync() {
local filter_target="${JOSH_SYNC_TARGET:-}" local filter_target="${JOSH_SYNC_TARGET:-}"
local filter_branch="" local filter_branch=""
local config_file=".josh-sync.yml" local config_file=".josh-sync.yml"
local force_flag=false
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
@@ -117,10 +119,18 @@ cmd_sync() {
--branch) filter_branch="$2"; shift 2 ;; --branch) filter_branch="$2"; shift 2 ;;
--config) config_file="$2"; shift 2 ;; --config) config_file="$2"; shift 2 ;;
--debug) export JOSH_SYNC_DEBUG=1; shift ;; --debug) export JOSH_SYNC_DEBUG=1; shift ;;
--force) force_flag=true; shift ;;
*) die "Unknown flag: $1" ;; *) die "Unknown flag: $1" ;;
esac esac
done done
if [ "$force_flag" = true ] && [ "$direction" != "reverse" ]; then
die "--force requires --reverse"
fi
if [ "$force_flag" = true ]; then
export SYNC_REVERSE_FORCE=1
fi
parse_config "$config_file" parse_config "$config_file"
# Forward sync # Forward sync
@@ -263,6 +273,9 @@ _sync_direction() {
log "WARN" "Trees differ but no human commits — skipping state update (will retry)" log "WARN" "Trees differ but no human commits — skipping state update (will retry)"
continue continue
fi fi
if [ "$result" = "forced" ]; then
log "WARN" "Target ${target_name}, branch ${branch}: force-pushed subrepo directly to mono — DESTRUCTIVE"
fi
# Update state (only on success) # Update state (only on success)
local new_state local new_state

View File

@@ -28,6 +28,16 @@ subrepo_auth_url() {
fi fi
} }
# ─── Monorepo Auth URL ─────────────────────────────────────────────
# Derives a push-capable HTTPS URL from MONOREPO_API.
# MONOREPO_API shape: https://<host>/api/v1/repos/<org/repo>
mono_auth_url() {
local api_host_path
api_host_path=$(echo "$MONOREPO_API" | sed 's|https://||; s|/api/v1/repos/|/|')
echo "https://${BOT_USER}:${GITEA_TOKEN}@${api_host_path}.git"
}
# ─── Remote Queries ───────────────────────────────────────────────── # ─── Remote Queries ─────────────────────────────────────────────────
subrepo_ls_remote() { subrepo_ls_remote() {

View File

@@ -240,6 +240,8 @@ reverse_sync() {
log "INFO" "=== Reverse sync: subrepo/${subrepo_branch} → mono/${mono_branch} ===" log "INFO" "=== Reverse sync: subrepo/${subrepo_branch} → mono/${mono_branch} ==="
local force_mode="${SYNC_REVERSE_FORCE:-0}"
# 1. Clone subrepo # 1. Clone subrepo
git clone "$(subrepo_auth_url)" \ git clone "$(subrepo_auth_url)" \
--branch "$subrepo_branch" --single-branch \ --branch "$subrepo_branch" --single-branch \
@@ -286,10 +288,14 @@ reverse_sync() {
fi fi
if [ -z "$human_commits" ]; then if [ -z "$human_commits" ]; then
if [ "$force_mode" = "1" ]; then
log "WARN" "No human commits found — force mode: continuing anyway"
else
log "INFO" "No new human commits in subrepo — nothing to sync" log "INFO" "No new human commits in subrepo — nothing to sync"
echo "skip-dirty" echo "skip-dirty"
return return
fi fi
fi
log "INFO" "New human commits to sync:" log "INFO" "New human commits to sync:"
echo "$human_commits" >&2 echo "$human_commits" >&2
@@ -400,7 +406,19 @@ reverse_sync() {
log "INFO" "Pushed to staging branch via josh: ${staging_branch}${linearized:+ (linearized)}" log "INFO" "Pushed to staging branch via josh: ${staging_branch}${linearized:+ (linearized)}"
# 6. Create PR on monorepo (NEVER direct push) # 6. Publish: force-push directly (--force) or create PR (default)
if [ "$force_mode" = "1" ]; then
log "WARN" "Force mode: pushing ${staging_branch} directly to mono/${mono_branch} — bypassing PR"
local mono_dir="${work_dir}/monorepo"
git clone "$(mono_auth_url)" \
--branch "$staging_branch" --single-branch \
"$mono_dir" || die "Failed to clone monorepo staging branch for force-push"
git -C "$mono_dir" push --force "$(mono_auth_url)" \
"HEAD:refs/heads/${mono_branch}" \
|| die "Failed to force-push to mono/${mono_branch}"
log "INFO" "Force-pushed mono/${staging_branch} → mono/${mono_branch}"
echo "forced"
else
local pr_body local pr_body
local linearize_note="" local linearize_note=""
if [ "$linearized" = true ]; then if [ "$linearized" = true ]; then
@@ -430,6 +448,7 @@ ${linearize_note}
log "INFO" "Reverse sync PR created on monorepo" log "INFO" "Reverse sync PR created on monorepo"
echo "pr-created" echo "pr-created"
fi
} }
# ─── Initial Import: Subrepo → Monorepo (first time) ─────────────── # ─── Initial Import: Subrepo → Monorepo (first time) ───────────────