feat(setup-remotes): pin branch.main.pushRemote to origin
Direct pushes on the canonical branch now target upstream instead of the personal fork whose branch-protection rules can diverge. Feature branches still fall back to the fork via remote.pushDefault. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## [1.1.0] - 2026-04-24
|
||||
|
||||
* `git-setup-remotes` now pins `branch.main.pushRemote` to `origin` so direct pushes on the canonical branch never target the personal fork.
|
||||
|
||||
## [1.0.0] - 2026-04-24
|
||||
|
||||
* Official Release🚀
|
||||
|
||||
@@ -18,6 +18,7 @@ Configures a clone for a fork-based workflow and is idempotent.
|
||||
- `fork` points at the maintainer's personal fork.
|
||||
- `main` tracks `origin/main`.
|
||||
- `remote.pushDefault` = `fork`, `push.default` = `current` so every `git push` and every `git push -u` for a new branch lands on the fork, not on the canonical repo.
|
||||
- `branch.main.pushRemote` = `origin` so a direct push on the canonical branch targets upstream, not the personal fork (whose branch-protection rules can diverge).
|
||||
|
||||
Usage:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "git-maintainer-tools"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
description = "Small CLIs (git-setup-remotes, git-sign-push) for fork-based OSS maintainer workflows."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -8,6 +8,10 @@ Target state after a successful run:
|
||||
* `remote.pushDefault` = `fork`, `push.default` = `current` so every
|
||||
`git push` without args (and new-branch pushes via sign-push) lands
|
||||
on the fork, not on the canonical repo.
|
||||
* `branch.main.pushRemote` = `origin` so a direct `git push` on the
|
||||
canonical branch never accidentally targets the personal fork (whose
|
||||
branch-protection rules diverge from upstream). Feature branches
|
||||
still fall back to the fork via `remote.pushDefault`.
|
||||
|
||||
The tool is idempotent: re-running on a correctly configured repo is a
|
||||
no-op. URLs are taken from CLI args or environment variables; there is
|
||||
@@ -120,17 +124,26 @@ def wire_main_tracking() -> None:
|
||||
def apply_push_config() -> None:
|
||||
g.run_git("config", "remote.pushDefault", "fork", capture=False)
|
||||
g.run_git("config", "push.default", "current", capture=False)
|
||||
# Override the global push default for `main` only: direct pushes on
|
||||
# the canonical branch go to `origin`, not the fork. Feature branches
|
||||
# stay on `fork` via `remote.pushDefault`.
|
||||
g.run_git("config", "branch.main.pushRemote", "origin", capture=False)
|
||||
|
||||
|
||||
def report_state() -> None:
|
||||
print("Remotes:")
|
||||
print(g.git_out("remote", "-v"))
|
||||
print()
|
||||
print(f"remote.pushDefault = {g.git_out('config', '--get', 'remote.pushDefault')}")
|
||||
print(f"push.default = {g.git_out('config', '--get', 'push.default')}")
|
||||
print(f"remote.pushDefault = {g.git_out('config', '--get', 'remote.pushDefault')}")
|
||||
print(f"push.default = {g.git_out('config', '--get', 'push.default')}")
|
||||
main_push = g.run_git(
|
||||
"config", "--get", "branch.main.pushRemote", check=False
|
||||
)
|
||||
if main_push.returncode == 0:
|
||||
print(f"branch.main.pushRemote = {(main_push.stdout or '').strip()}")
|
||||
main_u = g.abbrev_ref("main@{u}")
|
||||
if main_u:
|
||||
print(f"main tracks = {main_u}")
|
||||
print(f"main tracks = {main_u}")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
@@ -6,6 +6,12 @@ sandbox) ever signs shipped commits. For a branch with no upstream, the
|
||||
push target is resolved from `remote.pushDefault` (falling back to
|
||||
`origin`), so the fork-based workflow from `git-setup-remotes` routes
|
||||
new branches to the personal fork rather than the canonical repo.
|
||||
|
||||
For a branch that already has an upstream, this tool runs
|
||||
`git push --force-with-lease` without an explicit remote. Git then
|
||||
honours `branch.<name>.pushRemote` over `remote.pushDefault`, which is
|
||||
how `git-setup-remotes` pins `main` to `origin` while other branches
|
||||
still land on the fork.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
37
tests/test_apply_push_config.py
Normal file
37
tests/test_apply_push_config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Verify `apply_push_config` writes the expected git-config keys."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from git_maintainer_tools import setup_remotes
|
||||
|
||||
|
||||
def test_apply_push_config_writes_all_three_keys(monkeypatch):
|
||||
calls: list[tuple[str, ...]] = []
|
||||
|
||||
def fake_run_git(*args, **kwargs):
|
||||
calls.append(args)
|
||||
|
||||
monkeypatch.setattr(setup_remotes.g, "run_git", fake_run_git)
|
||||
setup_remotes.apply_push_config()
|
||||
|
||||
config_sets = [a for a in calls if a[:1] == ("config",)]
|
||||
assert ("config", "remote.pushDefault", "fork") in config_sets
|
||||
assert ("config", "push.default", "current") in config_sets
|
||||
assert ("config", "branch.main.pushRemote", "origin") in config_sets
|
||||
|
||||
|
||||
def test_main_push_override_is_origin_not_fork(monkeypatch):
|
||||
"""Regression: main must pin to origin so pushes never hit the fork."""
|
||||
calls: list[tuple[str, ...]] = []
|
||||
|
||||
def fake_run_git(*args, **kwargs):
|
||||
calls.append(args)
|
||||
|
||||
monkeypatch.setattr(setup_remotes.g, "run_git", fake_run_git)
|
||||
setup_remotes.apply_push_config()
|
||||
|
||||
main_overrides = [
|
||||
a for a in calls
|
||||
if a[:2] == ("config", "branch.main.pushRemote")
|
||||
]
|
||||
assert main_overrides == [("config", "branch.main.pushRemote", "origin")]
|
||||
Reference in New Issue
Block a user