diff --git a/README.md b/README.md index 96e4040..52a817a 100644 --- a/README.md +++ b/README.md @@ -371,10 +371,12 @@ make web Одноразовое переключение живой установки (хаб назывался `jira-hub`) делает скрипт `scripts/cutover.sh` (обёртка над `scripts/cutover.py`, stdlib). Выполняет владелец; без `--apply` скрипт только проверяет и печатает шаги. -1. Слить PR переименования; переименовать репозиторий в Gitea `aep/jira-hub` -> `aep/sdlc-hub`. +1. Слить PR переименования; переименовать репозиторий в Gitea `aep/jira-hub` -> `aep/sdlc-hub`. Gitea не + перенаправляет git по SSH со старого имени: сразу сменить адрес `origin` (общий для checkout и всех worktree) — + `git -C ~/projects/jira-bridge/jira-hub remote set-url origin ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git`. 2. Закрыть или не трогать сессии Claude с MCP хаба (после переключения -- `/mcp` reconnect). 3. `git -C ~/projects/jira-bridge/jira-hub pull --ff-only`, сразу `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh` - -- сухой прогон, проверить вывод (шаг 1 скрипта: чистый checkout на `main` = `origin/main`, доступен `aep/sdlc-hub`, + -- сухой прогон, проверить вывод (шаг 1 скрипта: чистый checkout на `main` = `main` в `aep/sdlc-hub`, доступен `aep/sdlc-hub`, нет целевых каталогов, есть `uv`, `make`, `claude`, `launchctl`, `node`, `npm`; неотслеживаемая старая сборка `src/jira_hub/` не мешает, на шаге 4 она удаляется; адрес нового `origin` берётся из прежнего с заменой имени репозитория, протокол (ssh/https) сохраняется). Скрипт также предупреждает об открытых сессиях Claude со старым diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/sdlc-rename/design.md index 9019c1d..058452f 100644 --- a/openspec/changes/sdlc-rename/design.md +++ b/openspec/changes/sdlc-rename/design.md @@ -46,9 +46,9 @@ launchd, двух чужих репозиториев и user scope Claude; руками их легко сделать не в том порядке (I-2 wiki-module). Скрипт одноразовый, после переключения остаётся в истории как образец; тесты — на сухой прогон и проверки шага 1 (во временном `HOME`), без launchd. -6. **Переименование репозитория в Gitea — руками владельца** в интерфейсе (Settings → Repository name). Gitea оставляет - перенаправление со старого адреса; `tea` и `origin` после этого работают по новому адресу. Скрипт проверяет, что - переименование сделано, и меняет `origin`. +6. **Переименование репозитория в Gitea — руками владельца** в интерфейсе (Settings → Repository name). Перенаправления + по SSH Gitea не даёт (проверено 24.10.2026: «Cannot find repository: aep/jira-hub»), поэтому владелец сразу меняет + `origin` на новый адрес; скрипт сверяет `HEAD` с `main` нового репозитория и принимает уже сменённый `origin`. 7. **Checkout переезжает в `jira-bridge/sdlc-hub`**, workspace `jira-bridge` остаётся: от его пути зависят каталог памяти Claude, workspace `CLAUDE.md`, путь общей папки реле (`…/utm/rshb/jira-bridge`) и настройки агентов на виртуалке. Переименование workspace — отдельно, если понадобится. diff --git a/scripts/cutover.py b/scripts/cutover.py index 79c7d9f..97e5396 100644 --- a/scripts/cutover.py +++ b/scripts/cutover.py @@ -78,7 +78,9 @@ NEW_REPO = "/aep/sdlc-hub" def derive_remote(origin: str) -> str: - """Адрес нового репозитория из адреса origin: тот же протокол и суффикс, имя другое.""" + """Адрес нового репозитория из адреса origin: тот же протокол и суффикс, имя другое; уже новый — как есть.""" + if NEW_REPO in origin: + return origin if OLD_REPO not in origin: raise ValueError("origin %s не содержит %s" % (origin, OLD_REPO)) return origin.replace(OLD_REPO, NEW_REPO) @@ -125,17 +127,18 @@ def preflight(cfg: Config) -> List[str]: if br != "main": problems.append("checkout %s не на ветке main (сейчас: %s)" % (old, br or "?")) head = _git(old, "rev-parse", "HEAD").stdout.strip() - # ls-remote вместо fetch: проверка остаётся строго чтением (fetch пишет FETCH_HEAD) - lr = _git(old, "ls-remote", "origin", "refs/heads/main") - remote_head = lr.stdout.split()[0] if lr.returncode == 0 and lr.stdout.split() else "" - if not remote_head: - problems.append("не удалось получить origin/main из %s" % old) - elif remote_head != head: - problems.append("HEAD не совпадает с origin/main: сделайте git pull --ff-only в %s" % old) if not remote: remote, prob = target_remote(cfg) if prob: problems.append(prob) + # сверка с новым адресом: после переименования Gitea не перенаправляет git по SSH, старый origin недоступен; + # ls-remote вместо fetch: проверка остаётся строго чтением (fetch пишет FETCH_HEAD) + if remote: + lr = _git(old, "ls-remote", remote, "refs/heads/main") + remote_head = lr.stdout.split()[0] if lr.returncode == 0 and lr.stdout.split() else "" + if remote_head and remote_head != head: + problems.append("HEAD не совпадает с main в %s: сделайте git pull --ff-only в %s (origin — %s)" + % (remote, old, remote)) if remote: lr = subprocess.run(["git", "ls-remote", remote], capture_output=True, text=True, env=_git_env(), stdin=subprocess.DEVNULL) diff --git a/tests/test_cutover.py b/tests/test_cutover.py index 00c3092..87691ff 100644 --- a/tests/test_cutover.py +++ b/tests/test_cutover.py @@ -258,6 +258,7 @@ def test_preflight_tracked_or_other_untracked_is_dirty(env): @pytest.mark.parametrize("origin,expected", [ ("ssh://git@git.aepif.ru:2222/aep/jira-hub.git", "ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git"), ("https://git.aepif.ru/aep/jira-hub.git", "https://git.aepif.ru/aep/sdlc-hub.git"), + ("ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git", "ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git"), # origin уже сменён ]) def test_derive_remote(origin, expected): assert cutover.derive_remote(origin) == expected @@ -367,3 +368,17 @@ def test_step6_rewrites_consumers_with_backups(env): assert "sdlc-jira" in (proj / ".claude" / "settings.local.json").read_text() assert any("каталог jira-hub" in ln for ln in left) assert not (proj / "CLAUDE.md.pre-sdlc").exists() + + +def test_preflight_checks_head_against_new_remote(env): + """Gitea не перенаправляет git по SSH после переименования: старый origin недоступен, сверка — с новым адресом.""" + git("remote", "set-url", "origin", str(env["tmp"] / "gone.git"), cwd=env["old"]) + problems = cutover.preflight(cutover.Config.from_env()) + assert not any("origin/main" in s or "HEAD не совпадает" in s for s in problems), problems + other = env["tmp"] / "other" + git("clone", str(env["bare"]), str(other), cwd=env["tmp"]) + (other / "b.txt").write_text("y") + git("add", ".", cwd=other) + git("commit", "-m", "ahead", cwd=other) + git("push", "origin", "main", cwd=other) + assert any("HEAD не совпадает" in s for s in cutover.preflight(cutover.Config.from_env()))