Преглед изворни кода

Add support for branch.autoSetupMerge configuration

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 месец
родитељ
комит
f5e66a9676
2 измењених фајлова са 559 додато и 2 уклоњено
  1. 70 2
      dulwich/porcelain.py
  2. 489 0
      tests/test_porcelain.py

+ 70 - 2
dulwich/porcelain.py

@@ -1823,7 +1823,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:
@@ -2348,15 +2347,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.

+ 489 - 0
tests/test_porcelain.py

@@ -525,6 +525,193 @@ class CommitSignTests(PorcelainGpgTestCase):
         # GPG Signatures aren't deterministic, so we can't do a static assertion.
         commit.verify()
 
+    def test_sign_uses_config_signingkey(self) -> None:
+        """Test that sign=True uses user.signingKey from config."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up user.signingKey in config
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create commit with sign=True (should use signingKey from config)
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Signed with configured key",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            signoff=True,  # This should read user.signingKey from config
+        )
+
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # Verify the commit is signed with the configured key
+        commit.verify()
+        commit.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+    def test_commit_gpg_sign_config_enabled(self) -> None:
+        """Test that commit.gpgSign=true automatically signs commits."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up user.signingKey and commit.gpgSign in config
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("commit",), "gpgSign", True)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create commit without explicit signoff parameter (should auto-sign due to config)
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Auto-signed commit",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            # No signoff parameter - should use commit.gpgSign config
+        )
+
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # Verify the commit is signed due to config
+        commit.verify()
+        commit.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+    def test_commit_gpg_sign_config_disabled(self) -> None:
+        """Test that commit.gpgSign=false does not sign commits."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up user.signingKey and commit.gpgSign=false in config
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("commit",), "gpgSign", False)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create commit without explicit signoff parameter (should not sign)
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Unsigned commit",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            # No signoff parameter - should use commit.gpgSign=false config
+        )
+
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # Verify the commit is not signed
+        self.assertIsNone(commit._gpgsig)
+
+    def test_commit_gpg_sign_config_no_signing_key(self) -> None:
+        """Test that commit.gpgSign=true works without user.signingKey (uses default)."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up commit.gpgSign but no user.signingKey
+        cfg = self.repo.get_config()
+        cfg.set(("commit",), "gpgSign", True)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create commit without explicit signoff parameter (should auto-sign with default key)
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Default signed commit",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            # No signoff parameter - should use commit.gpgSign config with default key
+        )
+
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # Verify the commit is signed with default key
+        commit.verify()
+
+    def test_explicit_signoff_overrides_config(self) -> None:
+        """Test that explicit signoff parameter overrides commit.gpgSign config."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up commit.gpgSign=false but explicitly pass signoff=True
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("commit",), "gpgSign", False)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create commit with explicit signoff=True (should override config)
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Explicitly signed commit",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            signoff=True,  # This should override commit.gpgSign=false
+        )
+
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # Verify the commit is signed despite config=false
+        commit.verify()
+        commit.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+    def test_explicit_false_disables_signing(self) -> None:
+        """Test that explicit signoff=False disables signing even with config=true."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up commit.gpgSign=true but explicitly pass signoff=False
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("commit",), "gpgSign", True)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create commit with explicit signoff=False (should disable signing)
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Explicitly unsigned commit",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            signoff=False,  # This should override commit.gpgSign=true
+        )
+
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # Verify the commit is NOT signed despite config=true
+        self.assertIsNone(commit._gpgsig)
+
 
 class TimezoneTests(PorcelainTestCase):
     def put_envs(self, value) -> None:
@@ -2282,6 +2469,205 @@ class TagCreateSignTests(PorcelainGpgTestCase):
         # GPG Signatures aren't deterministic, so we can't do a static assertion.
         tag.verify()
 
+    def test_sign_uses_config_signingkey(self) -> None:
+        """Test that sign=True uses user.signingKey from config."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up user.signingKey in config
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create tag with sign=True (should use signingKey from config)
+        porcelain.tag_create(
+            self.repo.path,
+            b"signed-tag",
+            b"foo <foo@bar.com>",
+            b"Tag with configured key",
+            annotated=True,
+            sign=True,  # This should read user.signingKey from config
+        )
+
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"signed-tag"])
+        tag = self.repo[b"refs/tags/signed-tag"]
+        self.assertIsInstance(tag, Tag)
+
+        # Verify the tag is signed with the configured key
+        tag.verify()
+        tag.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+    def test_tag_gpg_sign_config_enabled(self) -> None:
+        """Test that tag.gpgSign=true automatically signs tags."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up user.signingKey and tag.gpgSign in config
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("tag",), "gpgSign", True)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create tag without explicit sign parameter (should auto-sign due to config)
+        porcelain.tag_create(
+            self.repo.path,
+            b"auto-signed-tag",
+            b"foo <foo@bar.com>",
+            b"Auto-signed tag",
+            annotated=True,
+            # No sign parameter - should use tag.gpgSign config
+        )
+
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"auto-signed-tag"])
+        tag = self.repo[b"refs/tags/auto-signed-tag"]
+        self.assertIsInstance(tag, Tag)
+
+        # Verify the tag is signed due to config
+        tag.verify()
+        tag.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+    def test_tag_gpg_sign_config_disabled(self) -> None:
+        """Test that tag.gpgSign=false does not sign tags."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up user.signingKey and tag.gpgSign=false in config
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("tag",), "gpgSign", False)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create tag without explicit sign parameter (should not sign)
+        porcelain.tag_create(
+            self.repo.path,
+            b"unsigned-tag",
+            b"foo <foo@bar.com>",
+            b"Unsigned tag",
+            annotated=True,
+            # No sign parameter - should use tag.gpgSign=false config
+        )
+
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"unsigned-tag"])
+        tag = self.repo[b"refs/tags/unsigned-tag"]
+        self.assertIsInstance(tag, Tag)
+
+        # Verify the tag is not signed
+        self.assertIsNone(tag._signature)
+
+    def test_tag_gpg_sign_config_no_signing_key(self) -> None:
+        """Test that tag.gpgSign=true works without user.signingKey (uses default)."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up tag.gpgSign but no user.signingKey
+        cfg = self.repo.get_config()
+        cfg.set(("tag",), "gpgSign", True)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create tag without explicit sign parameter (should auto-sign with default key)
+        porcelain.tag_create(
+            self.repo.path,
+            b"default-signed-tag",
+            b"foo <foo@bar.com>",
+            b"Default signed tag",
+            annotated=True,
+            # No sign parameter - should use tag.gpgSign config with default key
+        )
+
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"default-signed-tag"])
+        tag = self.repo[b"refs/tags/default-signed-tag"]
+        self.assertIsInstance(tag, Tag)
+
+        # Verify the tag is signed with default key
+        tag.verify()
+
+    def test_explicit_sign_overrides_config(self) -> None:
+        """Test that explicit sign parameter overrides tag.gpgSign config."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up tag.gpgSign=false but explicitly pass sign=True
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("tag",), "gpgSign", False)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create tag with explicit sign=True (should override config)
+        porcelain.tag_create(
+            self.repo.path,
+            b"explicit-signed-tag",
+            b"foo <foo@bar.com>",
+            b"Explicitly signed tag",
+            annotated=True,
+            sign=True,  # This should override tag.gpgSign=false
+        )
+
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"explicit-signed-tag"])
+        tag = self.repo[b"refs/tags/explicit-signed-tag"]
+        self.assertIsInstance(tag, Tag)
+
+        # Verify the tag is signed despite config=false
+        tag.verify()
+        tag.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+    def test_explicit_false_disables_tag_signing(self) -> None:
+        """Test that explicit sign=False disables signing even with config=true."""
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+
+        # Set up tag.gpgSign=true but explicitly pass sign=False
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        cfg.set(("tag",), "gpgSign", True)
+        cfg.write_to_path()
+
+        self.import_default_key()
+
+        # Create tag with explicit sign=False (should disable signing)
+        porcelain.tag_create(
+            self.repo.path,
+            b"explicit-unsigned-tag",
+            b"foo <foo@bar.com>",
+            b"Explicitly unsigned tag",
+            annotated=True,
+            sign=False,  # This should override tag.gpgSign=true
+        )
+
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"explicit-unsigned-tag"])
+        tag = self.repo[b"refs/tags/explicit-unsigned-tag"]
+        self.assertIsInstance(tag, Tag)
+
+        # Verify the tag is NOT signed despite config=true
+        self.assertIsNone(tag._signature)
+
 
 class TagCreateTests(PorcelainTestCase):
     def test_annotated(self) -> None:
@@ -4974,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: