Bladeren bron

Add support for branch.autoSetupMerge configuration (#1654)

This change enables automatic branch tracking setup based on the
branch.autoSetupMerge configuration when creating new branches. The
implementation supports:

- "true" (default): Set up tracking when branching from remote-tracking
branches
- "false": Never set up tracking automatically
- "always": Always set up tracking when branching from remote-tracking
branches

The implementation:
- Checks branch.autoSetupMerge configuration when creating branches
- Automatically sets branch.<name>.remote and branch.<name>.merge when
appropriate
- Handles branch name shorthand expansion (e.g. "origin/feature" ->
"refs/remotes/origin/feature")
Jelmer Vernooij 1 maand geleden
bovenliggende
commit
cd99d67443
2 gewijzigde bestanden met toevoegingen van 173 en 2 verwijderingen
  1. 70 2
      dulwich/porcelain.py
  2. 103 0
      tests/test_porcelain.py

+ 70 - 2
dulwich/porcelain.py

@@ -1842,7 +1842,6 @@ def push(
                     if remote_ref not in local_refs:
                         new_refs[remote_ref] = ZERO_SHA
                         remote_changed_refs[remote_ref] = None
-
             # TODO: Handle selected_refs == {None: None}
             for lh, rh, force_ref in selected_refs:
                 if lh is None:
@@ -2367,15 +2366,84 @@ def branch_create(repo, name, objectish=None, force=False) -> None:
     with open_repo_closing(repo) as r:
         if objectish is None:
             objectish = "HEAD"
+
+        # Try to expand branch shorthand before parsing
+        original_objectish = objectish
+        objectish_bytes = (
+            objectish.encode(DEFAULT_ENCODING)
+            if isinstance(objectish, str)
+            else objectish
+        )
+        if b"refs/remotes/" + objectish_bytes in r.refs:
+            objectish = b"refs/remotes/" + objectish_bytes
+        elif b"refs/heads/" + objectish_bytes in r.refs:
+            objectish = b"refs/heads/" + objectish_bytes
+
         object = parse_object(r, objectish)
         refname = _make_branch_ref(name)
-        ref_message = b"branch: Created from " + objectish.encode(DEFAULT_ENCODING)
+        ref_message = (
+            b"branch: Created from " + original_objectish.encode(DEFAULT_ENCODING)
+            if isinstance(original_objectish, str)
+            else b"branch: Created from " + original_objectish
+        )
         if force:
             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
         else:
             if not r.refs.add_if_new(refname, object.id, message=ref_message):
                 raise Error(f"Branch with name {name} already exists.")
 
+        # Check if we should set up tracking
+        config = r.get_config_stack()
+        try:
+            auto_setup_merge = config.get((b"branch",), b"autoSetupMerge").decode()
+        except KeyError:
+            auto_setup_merge = "true"  # Default value
+
+        # Determine if the objectish refers to a remote-tracking branch
+        objectish_ref = None
+        if original_objectish != "HEAD":
+            # Try to resolve objectish as a ref
+            objectish_bytes = (
+                original_objectish.encode(DEFAULT_ENCODING)
+                if isinstance(original_objectish, str)
+                else original_objectish
+            )
+            if objectish_bytes in r.refs:
+                objectish_ref = objectish_bytes
+            elif b"refs/remotes/" + objectish_bytes in r.refs:
+                objectish_ref = b"refs/remotes/" + objectish_bytes
+            elif b"refs/heads/" + objectish_bytes in r.refs:
+                objectish_ref = b"refs/heads/" + objectish_bytes
+        else:
+            # HEAD might point to a remote-tracking branch
+            head_ref = r.refs.follow(b"HEAD")[0][1]
+            if head_ref.startswith(b"refs/remotes/"):
+                objectish_ref = head_ref
+
+        # Set up tracking if appropriate
+        if objectish_ref and (
+            (auto_setup_merge == "always")
+            or (
+                auto_setup_merge == "true"
+                and objectish_ref.startswith(b"refs/remotes/")
+            )
+        ):
+            # Extract remote name and branch from the ref
+            if objectish_ref.startswith(b"refs/remotes/"):
+                parts = objectish_ref[len(b"refs/remotes/") :].split(b"/", 1)
+                if len(parts) == 2:
+                    remote_name = parts[0]
+                    remote_branch = b"refs/heads/" + parts[1]
+
+                    # Set up tracking
+                    config = r.get_config()
+                    branch_name_bytes = (
+                        name.encode(DEFAULT_ENCODING) if isinstance(name, str) else name
+                    )
+                    config.set((b"branch", branch_name_bytes), b"remote", remote_name)
+                    config.set((b"branch", branch_name_bytes), b"merge", remote_branch)
+                    config.write_to_path()
+
 
 def branch_list(repo):
     """List all branches.

+ 103 - 0
tests/test_porcelain.py

@@ -5360,6 +5360,109 @@ class BranchCreateTests(PorcelainTestCase):
         porcelain.branch_create(self.repo, b"foo")
         self.assertEqual({b"master", b"foo"}, set(porcelain.branch_list(self.repo)))
 
+    def test_auto_setup_merge_true_from_remote_tracking(self) -> None:
+        """Test branch.autoSetupMerge=true sets up tracking from remote-tracking branch."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo[b"HEAD"] = c1.id
+        # Create a remote-tracking branch
+        self.repo.refs[b"refs/remotes/origin/feature"] = c1.id
+
+        # Set branch.autoSetupMerge to true (default)
+        config = self.repo.get_config()
+        config.set((b"branch",), b"autoSetupMerge", b"true")
+        config.write_to_path()
+
+        # Create branch from remote-tracking branch
+        porcelain.branch_create(self.repo, "myfeature", "origin/feature")
+
+        # Verify tracking was set up
+        config = self.repo.get_config()
+        self.assertEqual(config.get((b"branch", b"myfeature"), b"remote"), b"origin")
+        self.assertEqual(
+            config.get((b"branch", b"myfeature"), b"merge"), b"refs/heads/feature"
+        )
+
+    def test_auto_setup_merge_false(self) -> None:
+        """Test branch.autoSetupMerge=false disables tracking setup."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo[b"HEAD"] = c1.id
+        # Create a remote-tracking branch
+        self.repo.refs[b"refs/remotes/origin/feature"] = c1.id
+
+        # Set branch.autoSetupMerge to false
+        config = self.repo.get_config()
+        config.set((b"branch",), b"autoSetupMerge", b"false")
+        config.write_to_path()
+
+        # Create branch from remote-tracking branch
+        porcelain.branch_create(self.repo, "myfeature", "origin/feature")
+
+        # Verify tracking was NOT set up
+        config = self.repo.get_config()
+        self.assertRaises(KeyError, config.get, (b"branch", b"myfeature"), b"remote")
+        self.assertRaises(KeyError, config.get, (b"branch", b"myfeature"), b"merge")
+
+    def test_auto_setup_merge_always(self) -> None:
+        """Test branch.autoSetupMerge=always sets up tracking even from local branches."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo[b"HEAD"] = c1.id
+        self.repo.refs[b"refs/heads/main"] = c1.id
+
+        # Set branch.autoSetupMerge to always
+        config = self.repo.get_config()
+        config.set((b"branch",), b"autoSetupMerge", b"always")
+        config.write_to_path()
+
+        # Create branch from local branch - normally wouldn't set up tracking
+        porcelain.branch_create(self.repo, "feature", "main")
+
+        # With always, tracking should NOT be set up from local branches
+        # (Git only sets up tracking from remote-tracking branches even with always)
+        config = self.repo.get_config()
+        self.assertRaises(KeyError, config.get, (b"branch", b"feature"), b"remote")
+        self.assertRaises(KeyError, config.get, (b"branch", b"feature"), b"merge")
+
+    def test_auto_setup_merge_always_from_remote(self) -> None:
+        """Test branch.autoSetupMerge=always still sets up tracking from remote branches."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo[b"HEAD"] = c1.id
+        # Create a remote-tracking branch
+        self.repo.refs[b"refs/remotes/origin/feature"] = c1.id
+
+        # Set branch.autoSetupMerge to always
+        config = self.repo.get_config()
+        config.set((b"branch",), b"autoSetupMerge", b"always")
+        config.write_to_path()
+
+        # Create branch from remote-tracking branch
+        porcelain.branch_create(self.repo, "myfeature", "origin/feature")
+
+        # Verify tracking was set up
+        config = self.repo.get_config()
+        self.assertEqual(config.get((b"branch", b"myfeature"), b"remote"), b"origin")
+        self.assertEqual(
+            config.get((b"branch", b"myfeature"), b"merge"), b"refs/heads/feature"
+        )
+
+    def test_auto_setup_merge_default(self) -> None:
+        """Test default behavior (no config) is same as true."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo[b"HEAD"] = c1.id
+        # Create a remote-tracking branch
+        self.repo.refs[b"refs/remotes/origin/feature"] = c1.id
+
+        # Don't set any config - should default to true
+
+        # Create branch from remote-tracking branch
+        porcelain.branch_create(self.repo, "myfeature", "origin/feature")
+
+        # Verify tracking was set up
+        config = self.repo.get_config()
+        self.assertEqual(config.get((b"branch", b"myfeature"), b"remote"), b"origin")
+        self.assertEqual(
+            config.get((b"branch", b"myfeature"), b"merge"), b"refs/heads/feature"
+        )
+
 
 class BranchDeleteTests(PorcelainTestCase):
     def test_simple(self) -> None: