diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bdf31..80ace72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # 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 ### Breaking Changes diff --git a/README.md b/README.md index 0f8466c..bd5c0dc 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ CI workflows pin to a floating major tag (e.g. `@v1`). A breaking change bumps t ## 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 import josh-sync reset @@ -92,7 +92,7 @@ josh-sync state reset [branch] ## How It Works - **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. - **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. diff --git a/VERSION b/VERSION index 88c5fb8..bc80560 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.5.0 diff --git a/bin/josh-sync b/bin/josh-sync index 54f3906..2d089b3 100755 --- a/bin/josh-sync +++ b/bin/josh-sync @@ -88,6 +88,7 @@ Global flags: Sync flags: --forward Forward only (mono → subrepo) --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) --branch BRANCH Filter to one branch @@ -108,6 +109,7 @@ cmd_sync() { local filter_target="${JOSH_SYNC_TARGET:-}" local filter_branch="" local config_file=".josh-sync.yml" + local force_flag=false while [ $# -gt 0 ]; do case "$1" in @@ -117,10 +119,18 @@ cmd_sync() { --branch) filter_branch="$2"; shift 2 ;; --config) config_file="$2"; shift 2 ;; --debug) export JOSH_SYNC_DEBUG=1; shift ;; + --force) force_flag=true; shift ;; *) die "Unknown flag: $1" ;; esac 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" # Forward sync @@ -263,6 +273,9 @@ _sync_direction() { log "WARN" "Trees differ but no human commits — skipping state update (will retry)" continue 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) local new_state diff --git a/lib/auth.sh b/lib/auth.sh index 851f940..fd432d4 100644 --- a/lib/auth.sh +++ b/lib/auth.sh @@ -28,6 +28,16 @@ subrepo_auth_url() { fi } +# ─── Monorepo Auth URL ───────────────────────────────────────────── +# Derives a push-capable HTTPS URL from MONOREPO_API. +# MONOREPO_API shape: https:///api/v1/repos/ + +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 ───────────────────────────────────────────────── subrepo_ls_remote() { diff --git a/lib/sync.sh b/lib/sync.sh index 4fceae3..bd6ce33 100644 --- a/lib/sync.sh +++ b/lib/sync.sh @@ -240,6 +240,8 @@ reverse_sync() { log "INFO" "=== Reverse sync: subrepo/${subrepo_branch} → mono/${mono_branch} ===" + local force_mode="${SYNC_REVERSE_FORCE:-0}" + # 1. Clone subrepo git clone "$(subrepo_auth_url)" \ --branch "$subrepo_branch" --single-branch \ @@ -286,9 +288,13 @@ reverse_sync() { fi if [ -z "$human_commits" ]; then - log "INFO" "No new human commits in subrepo — nothing to sync" - echo "skip-dirty" - return + 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" + echo "skip-dirty" + return + fi fi log "INFO" "New human commits to sync:" @@ -400,16 +406,28 @@ reverse_sync() { log "INFO" "Pushed to staging branch via josh: ${staging_branch}${linearized:+ (linearized)}" - # 6. Create PR on monorepo (NEVER direct push) - local pr_body - local linearize_note="" - if [ "$linearized" = true ]; then - linearize_note=" + # 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 linearize_note="" + if [ "$linearized" = true ]; then + linearize_note=" > **Note:** Merge commits were squashed during sync because the subrepo > history contained merges that josh-proxy cannot map (see ADR-011). > Regular commits are preserved individually." - fi - pr_body="## Subrepo changes + fi + pr_body="## Subrepo changes New commits from subrepo \`${subrepo_branch}\`: @@ -422,14 +440,15 @@ ${linearize_note} - [ ] No leaked credentials or environment-specific config - [ ] CI passes" - create_pr "${MONOREPO_API}" "${GITEA_TOKEN}" \ - "$mono_branch" "$staging_branch" \ - "[Subrepo Sync] ${subrepo_branch} → ${mono_branch}" \ - "$pr_body" \ - || die "Failed to create PR on monorepo (check GITEA_TOKEN)" + create_pr "${MONOREPO_API}" "${GITEA_TOKEN}" \ + "$mono_branch" "$staging_branch" \ + "[Subrepo Sync] ${subrepo_branch} → ${mono_branch}" \ + "$pr_body" \ + || die "Failed to create PR on monorepo (check GITEA_TOKEN)" - log "INFO" "Reverse sync PR created on monorepo" - echo "pr-created" + log "INFO" "Reverse sync PR created on monorepo" + echo "pr-created" + fi } # ─── Initial Import: Subrepo → Monorepo (first time) ───────────────