From 490dc10a64f316a325c9b3dcc05a69a29d259601 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 29 Oct 2026 13:15:21 -0700 Subject: [PATCH 1/5] Default --pr-remote to git remote.pushDefault Currently --pr-remote defaults to origin but for cpython many repos need at least two remotes. Remembering to provide --pr-remote every time is hard, so make a way of providing a value. Chose to reuse git's existing config option to minimize cherry_picker specific infrastructure and documentation. --- README.md | 14 +++++++++----- cherry_picker/cherry_picker.py | 15 ++++++++++++--- cherry_picker/test_cherry_picker.py | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 06988df..bc9b215 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ options: -h, --help show this help message and exit -V, --version show program's version number and exit --dry-run Prints out the commands, but not executed - --pr-remote REMOTE Git remote to use for PR branches + --pr-remote REMOTE Git remote to use for PR branches (default: + remote.pushDefault or origin) --upstream-remote REMOTE Git remote to use for upstream branches --abort Abort current cherry-pick and clean up branch @@ -89,9 +90,11 @@ $ git remote add upstream https://github.com/python/cpython.git ``` By default, the PR branches used to submit pull requests back to the main -repository are pushed to `origin`. If this is incorrect, then the correct -remote will need be specified using the `--pr-remote` option (e.g. -`--pr-remote pr` to use a remote named `pr`). +repository are pushed to Git's `remote.pushDefault`, or `origin` if that is +not set. To set it for a group of checkouts, use an +[`includeIf`](https://git-scm.com/docs/git-config#_conditional_includes) +in your global Git config. To use a remote named `pr`, either pass +`--pr-remote pr` or set `remote.pushDefault` to `pr`. ## Cherry-picking 🐍🍒⛏️ @@ -122,7 +125,8 @@ Use the full commit hash for `cherry_picker.py`. ``` --dry-run Dry Run Mode. Prints out the commands, but not executed. ---pr-remote REMOTE Specify the git remote to push into. Default is 'origin'. +--pr-remote REMOTE Specify the git remote to push into. Default is + remote.pushDefault or 'origin'. --upstream-remote REMOTE Specify the git remote to use for upstream branches. Default is 'upstream' or 'origin' if the former doesn't exist. --status Do `git status` in cpython directory. diff --git a/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker.py index a71a1ac..97b46e5 100755 --- a/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker.py @@ -139,7 +139,13 @@ def __init__( if dry_run: print("Dry run requested, listing expected command sequence") - self.pr_remote = pr_remote + cmd = "git", "config", "--get", "remote.pushDefault" + self.pr_remote = pr_remote or ( + subprocess.run( + cmd, capture_output=True, text=True, check=False + ).stdout.strip() + or "origin" + ) self.upstream_remote = upstream_remote self.commit_sha1 = commit_sha1 self.branches = branches @@ -751,8 +757,11 @@ def get_parser(): parser.add_argument( "--pr-remote", metavar="REMOTE", - default="origin", - help="Git remote to use for PR branches", + default=None, + help=( + "Git remote to use for PR branches " + "(default: remote.pushDefault or origin)" + ), ) parser.add_argument( "--upstream-remote", diff --git a/cherry_picker/test_cherry_picker.py b/cherry_picker/test_cherry_picker.py index 35e8e14..525d6e7 100644 --- a/cherry_picker/test_cherry_picker.py +++ b/cherry_picker/test_cherry_picker.py @@ -317,6 +317,26 @@ def test_upstream_name( assert cp.upstream == remote_name +@pytest.mark.parametrize( + "push_default,expected", ((None, "origin"), ("personal", "personal")) +) +def test_pr_remote_name( + push_default, expected, config, tmp_git_repo_dir, git_config, monkeypatch +): + monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) + if push_default is not None: + git_config("--local", "remote.pushDefault", push_default) + + with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True): + cp = CherryPicker( + None, + "22a594a0047d7706537ff2ac676cdc0f1dcb329c", + ["3.6"], + config=config, + ) + assert cp.pr_remote == expected + + @pytest.mark.parametrize( "remote_to_add,remote_name,upstream_remote", ( From 1e3a2a420f91ad707738b718a739b1feff7697b8 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 29 Oct 2026 17:08:43 -0700 Subject: [PATCH 2/5] Move git config reading to a helper function --- cherry_picker/cherry_picker.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker.py index 97b46e5..f02caa7 100755 --- a/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker.py @@ -139,13 +139,7 @@ def __init__( if dry_run: print("Dry run requested, listing expected command sequence") - cmd = "git", "config", "--get", "remote.pushDefault" - self.pr_remote = pr_remote or ( - subprocess.run( - cmd, capture_output=True, text=True, check=False - ).stdout.strip() - or "origin" - ) + self.pr_remote = pr_remote or get_git_config_val("remote.pushDefault", "origin") self.upstream_remote = upstream_remote self.commit_sha1 = commit_sha1 self.branches = branches @@ -1115,6 +1109,20 @@ def load_val_from_git_cfg(cfg_key_suffix): return None +def get_git_config_val(key, default=None): + """Retrieve one option from Git config, or default if unset or empty.""" + cmd = "git", "config", "--get", key + try: + value = ( + subprocess.check_output(cmd, stderr=subprocess.DEVNULL) + .strip() + .decode("utf-8") + ) + except subprocess.CalledProcessError: + value = None + return value or default + + def from_git_rev_read(path): """Retrieve given file path contents of certain Git revision.""" if ":" not in path: From 59fd086c3038308d6fe13eb1e130b3b4307b30b0 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 29 Oct 2026 17:25:44 -0700 Subject: [PATCH 3/5] Simplify test to just exercise helper --- cherry_picker/test_cherry_picker.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/cherry_picker/test_cherry_picker.py b/cherry_picker/test_cherry_picker.py index 525d6e7..af0932e 100644 --- a/cherry_picker/test_cherry_picker.py +++ b/cherry_picker/test_cherry_picker.py @@ -25,6 +25,7 @@ get_commits_from_backport_branch, get_current_branch, get_full_sha_from_short, + get_git_config_val, get_sha1_from, get_state, load_config, @@ -318,23 +319,12 @@ def test_upstream_name( @pytest.mark.parametrize( - "push_default,expected", ((None, "origin"), ("personal", "personal")) + "value,expected", ((None, "default"), ("", "default"), ("set", "set")) ) -def test_pr_remote_name( - push_default, expected, config, tmp_git_repo_dir, git_config, monkeypatch -): - monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) - if push_default is not None: - git_config("--local", "remote.pushDefault", push_default) - - with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True): - cp = CherryPicker( - None, - "22a594a0047d7706537ff2ac676cdc0f1dcb329c", - ["3.6"], - config=config, - ) - assert cp.pr_remote == expected +def test_get_git_config_val(value, expected, tmp_git_repo_dir, git_config): + if value is not None: + git_config("--local", "cherry-picker.test-key", value) + assert get_git_config_val("cherry-picker.test-key", "default") == expected @pytest.mark.parametrize( From a94f9749ef2dcf0f70ab91f74eb49f8b7b264e73 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 2 Nov 2026 12:56:13 -0800 Subject: [PATCH 4/5] Reuse existing git config fetching code --- cherry_picker/cherry_picker.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker.py index f02caa7..c8b511e 100755 --- a/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker.py @@ -727,12 +727,8 @@ class state: def is_mirror(self) -> bool: """Return True if the current repository was created with --mirror.""" - cmd = ["git", "config", "--local", "--get", "remote.origin.mirror"] - try: - out = self.run_cmd(cmd, required_real_result=True) - except subprocess.CalledProcessError: - return False - return out.startswith("true") + mirror = get_git_config_val("remote.origin.mirror", "", local=True) + return mirror.startswith("true") def get_parser(): @@ -1098,20 +1094,13 @@ def wipe_cfg_vals_from_git_cfg(*cfg_opts): def load_val_from_git_cfg(cfg_key_suffix): """Retrieve one option from Git config.""" cfg_key = f'cherry-picker.{cfg_key_suffix.replace("_", "-")}' - cmd = "git", "config", "--local", "--get", cfg_key - try: - return ( - subprocess.check_output(cmd, stderr=subprocess.DEVNULL) - .strip() - .decode("utf-8") - ) - except subprocess.CalledProcessError: - return None + return get_git_config_val(cfg_key, local=True) -def get_git_config_val(key, default=None): +def get_git_config_val(key, default=None, *, local=False): """Retrieve one option from Git config, or default if unset or empty.""" - cmd = "git", "config", "--get", key + scope = ("--local",) if local else () + cmd = "git", "config", *scope, "--get", key try: value = ( subprocess.check_output(cmd, stderr=subprocess.DEVNULL) From 78df2667883ad1bc2c6f1b64035210991f4f841d Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 2 Nov 2026 17:05:29 -0800 Subject: [PATCH 5/5] Remove default arg that added more complexity than value --- cherry_picker/cherry_picker.py | 21 +++++++++++++-------- cherry_picker/test_cherry_picker.py | 6 ++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker.py index c8b511e..71d71c3 100755 --- a/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker.py @@ -139,7 +139,9 @@ def __init__( if dry_run: print("Dry run requested, listing expected command sequence") - self.pr_remote = pr_remote or get_git_config_val("remote.pushDefault", "origin") + self.pr_remote = ( + pr_remote or get_git_config_val("remote.pushDefault") or "origin" + ) self.upstream_remote = upstream_remote self.commit_sha1 = commit_sha1 self.branches = branches @@ -727,8 +729,12 @@ class state: def is_mirror(self) -> bool: """Return True if the current repository was created with --mirror.""" - mirror = get_git_config_val("remote.origin.mirror", "", local=True) - return mirror.startswith("true") + cmd = ["git", "config", "--local", "--get", "remote.origin.mirror"] + try: + out = self.run_cmd(cmd, required_real_result=True) + except subprocess.CalledProcessError: + return False + return out.startswith("true") def get_parser(): @@ -1097,19 +1103,18 @@ def load_val_from_git_cfg(cfg_key_suffix): return get_git_config_val(cfg_key, local=True) -def get_git_config_val(key, default=None, *, local=False): - """Retrieve one option from Git config, or default if unset or empty.""" +def get_git_config_val(key, *, local=False): + """Retrieve one option from Git config, or None if unset.""" scope = ("--local",) if local else () cmd = "git", "config", *scope, "--get", key try: - value = ( + return ( subprocess.check_output(cmd, stderr=subprocess.DEVNULL) .strip() .decode("utf-8") ) except subprocess.CalledProcessError: - value = None - return value or default + return None def from_git_rev_read(path): diff --git a/cherry_picker/test_cherry_picker.py b/cherry_picker/test_cherry_picker.py index af0932e..da762c8 100644 --- a/cherry_picker/test_cherry_picker.py +++ b/cherry_picker/test_cherry_picker.py @@ -318,13 +318,11 @@ def test_upstream_name( assert cp.upstream == remote_name -@pytest.mark.parametrize( - "value,expected", ((None, "default"), ("", "default"), ("set", "set")) -) +@pytest.mark.parametrize("value,expected", ((None, None), ("", ""), ("set", "set"))) def test_get_git_config_val(value, expected, tmp_git_repo_dir, git_config): if value is not None: git_config("--local", "cherry-picker.test-key", value) - assert get_git_config_val("cherry-picker.test-key", "default") == expected + assert get_git_config_val("cherry-picker.test-key") == expected @pytest.mark.parametrize(