@echo off setlocal enabledelayedexpansion rem Take specific commits from wj to main, from Windows. rem rem tools\promote-to-main.cmd [ ...] rem tools\promote-to-main.cmd --dry-run ... rem rem Commits are applied in the order given, so give them oldest first. rem rem WHY CHERRY-PICK AND NOT A MERGE rem rem wj holds this site's work. Some of it is product - the rest of the world rem should have it - and some belongs to this site alone. Merging the branch rem takes both; that is how a site branch quietly becomes the product. Naming rem the commits keeps the decision explicit and per change. rem rem It does NOT push. Read what it prints, then run tools\git-push.cmd main. rem RUN FROM A COPY OF OURSELF. rem rem This checks out main. The tooling may live only on the site branch, in which rem case that checkout DELETES this file while cmd.exe is still reading it - and rem cmd.exe reads a batch file line by line, by file offset, re-seeking after rem every command. It would carry on reading whatever bytes are at that offset rem afterwards, while manipulating branches. The bash version has the same rem hazard and the same fix. if not "%PROMOTE_SELFCOPY%"=="1" ( rem The copy runs from %TEMP%, where %~dp0 is the temp directory and not the rem checkout, so the repository root has to travel with it. set "PROMOTE_REPO=%~dp0.." set "PROMOTE_SELFCOPY=1" copy /y "%~f0" "%TEMP%\promote-to-main-copy.cmd" >nul call "%TEMP%\promote-to-main-copy.cmd" %* rem Delayed expansion: a plain %errorlevel% here is expanded when the block is rem parsed, which is BEFORE the call above has run, so it would always report rem the code from whatever ran previously. set "EXITCODE=!errorlevel!" del /q "%TEMP%\promote-to-main-copy.cmd" >nul 2>&1 exit /b !EXITCODE! ) cd /d "%PROMOTE_REPO%" set "DRYRUN=0" if "%~1"=="--dry-run" ( set "DRYRUN=1" shift ) if "%~1"=="" ( echo usage: tools\promote-to-main.cmd [--dry-run] ^ [^ ...] echo. echo Give commits oldest first. Find them with: echo git log --oneline main..wj exit /b 1 ) rem The ADR-015 site-literal check is a shell script, and it is the one gate rem that makes a site branch survivable: it catches a hostname or share path rem riding along in a commit that looked generic. Running the promotion without rem it would defeat the point, so this refuses rather than skipping it quietly. where bash >nul 2>&1 if errorlevel 1 ( echo ERROR: bash was not found on PATH. echo. echo The naming/style gate ^(scripts\check-naming-and-style.sh^) needs it, and echo that gate is what stops a site-specific literal reaching main. Promoting echo without it is not safe. echo. echo Git for Windows ships bash. Add its usr\bin to PATH, or run this from echo Git Bash using tools/promote-to-main.sh instead. exit /b 1 ) for /f "delims=" %%b in ('git rev-parse --abbrev-ref HEAD') do set "STARTING_BRANCH=%%b" for /f "delims=" %%s in ('git status --porcelain') do ( echo Working tree is not clean: git status --short echo. echo Cherry-picking onto main from here would mix uncommitted work into the echo promotion, or fail halfway and leave you on main with a dirty tree. exit /b 1 ) rem Resolve and describe first, so a typo is caught before anything moves. echo About to promote onto main, in this order: for %%r in (%*) do ( git rev-parse --verify --quiet "%%r^^{commit}" >nul if errorlevel 1 ( echo ERROR: not a commit: %%r exit /b 1 ) for /f "delims=" %%l in ('git log --oneline -1 %%r') do echo %%l ) echo. if "%DRYRUN%"=="1" ( echo --dry-run: files each commit touches, so you can see what reaches main. for %%r in (%*) do ( echo. for /f "delims=" %%l in ('git log --oneline -1 %%r') do echo --- %%l --- git show --stat --oneline %%r ) echo. echo Nothing was changed. exit /b 0 ) git checkout main if errorlevel 1 goto :restore for /f "delims=" %%h in ('git rev-parse HEAD') do set "MAIN_BEFORE=%%h" for %%r in (%*) do ( echo. for /f "delims=" %%l in ('git log --oneline -1 %%r') do echo ==^> picking %%l git cherry-pick -x %%r if errorlevel 1 ( echo. echo That commit does not apply cleanly to main. echo Resolve it by hand if it is worth having: echo git status echo git cherry-pick --abort goto :restore ) ) echo. echo ==^> main gates echo --- naming, style, site literals ^(ADR-015^) --- bash scripts/check-naming-and-style.sh if errorlevel 1 goto :restore echo --- generated project map --- venv\Scripts\python scripts\gen_project_map.py --check >nul 2>&1 if errorlevel 1 ( echo Project map is stale; regenerating. venv\Scripts\python scripts\gen_project_map.py git add docs/PROJECT-MAP.md git commit -q -m "Regenerate the project map after promotion" ) echo --- python suite --- venv\Scripts\python -m pytest -q if errorlevel 1 goto :restore echo --- frontend --- pushd frontend call npm test --silent set "FRONTEND_STATUS=!errorlevel!" popd if not "!FRONTEND_STATUS!"=="0" goto :restore echo. echo main is now: git log --oneline %MAIN_BEFORE%..HEAD echo. echo Gates passed. NOT pushed - review the above, then: echo tools\git-push.cmd main echo. echo You are on main. Return to your branch with: echo git checkout %STARTING_BRANCH% exit /b 0 :restore echo. echo Promotion did NOT complete. git rev-parse --verify --quiet CHERRY_PICK_HEAD >nul 2>&1 if not errorlevel 1 ( echo Aborting the in-progress cherry-pick ... git cherry-pick --abort ) git checkout "%STARTING_BRANCH%" >nul 2>&1 echo Back on %STARTING_BRANCH%. main is unchanged and nothing was pushed. exit /b 1