Repository: pypa/twine

Issue:
Repository port lost in get_repository_from_config when username is provided
### Is there an existing issue for this?

- [X] I have searched the existing issues (open and closed), and could not find an existing issue

### What keywords did you use to search existing issues?

get_repository_from_config, port

### What operating system are you using?

Linux

### If you selected 'Other', describe your Operating System here

_No response_

### What version of Python are you running?

```console
Python 3.8.10
Python 3.11.9
```


### How did you install twine? Did you use your operating system's package manager or pip or something else?

```console
$ pip install twine
```


### What version of twine do you have installed (include the complete output)

```console
twine version 5.1.0 (importlib-metadata: 7.1.0, keyring: 25.2.1, pkginfo: 1.10.0, requests: 2.31.0, requests-toolbelt: 1.0.0, urllib3: 2.2.1)
```


### Which package repository are you using?

A private one hosted with https://pypi.org/project/pypiserver/, behind an apache server enforcing basicauth.

### Please describe the issue that you are experiencing

There is a bug that must have been introduced in 5.1.0 in the repository url parsing.
Concretely, the function `get_repository_from_config` looses the port information when a username is provided.

The expected behaviour:
```python
>> get_repository_from_config(..., ..., 'https://user:pass@some.domain.com:12345')
{'repository': 'https://some.domain.com:12345', 'username': 'user', 'password': 'pass'}
```

The current behaviour:
```python
>> get_repository_from_config(..., ..., 'https://user:pass@some.domain.com:12345')
{'repository': 'https://some.domain.com', 'username': 'user', 'password': 'pass'}
```


### Please list the steps required to reproduce this behaviour

Just test the given examples

### Anything else you'd like to mention?

_No response_


Discussion:

Patch:
diff --git a/changelog/fix-repo-urls-with-auth-and-port.bugfix b/changelog/fix-repo-urls-with-auth-and-port.bugfix
new file mode 100644
index 00000000..8841e505
--- /dev/null
+++ b/changelog/fix-repo-urls-with-auth-and-port.bugfix
@@ -0,0 +1,5 @@
+Fix bug for Repository URLs with auth where the port was lost. When attempting
+to prevent printing authentication credentials in URLs provided with username
+and password, we did not properly handle the case where the URL also contains
+a port (when reconstructing the URL). This is now handled and tested to ensure
+no regressions.
diff --git a/tox.ini b/tox.ini
index 9dd7b847..9681ed3a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -95,11 +95,22 @@ commands =
 
 [testenv:changelog]
 basepython = python3
+skip_install = True
 deps =
     towncrier
 commands =
     towncrier build {posargs}
 
+
+# Usage:
+#  tox -e create-changelog-item -- [additional arguments] {filename}.{bugfix,feature,doc,removal,misc}
+[testenv:create-changelog-item]
+basepython = python3
+skip_install = True
+deps = towncrier
+commands =
+    towncrier create --config pyproject.toml {posargs}
+
 [testenv:release]
 # specify Python 3 to use platform's default Python 3
 basepython = python3
diff --git a/twine/utils.py b/twine/utils.py
index 05739bc4..10e374e7 100644
--- a/twine/utils.py
+++ b/twine/utils.py
@@ -174,7 +174,9 @@ def _config_from_repository_url(url: str) -> RepositoryConfig:
     if parsed.username:
         config["username"] = parsed.username
         config["password"] = parsed.password
-        config["repository"] = urlunparse((parsed.scheme, parsed.hostname) + parsed[2:])
+        config["repository"] = cast(
+            str, rfc3986.urlparse(url).copy_with(userinfo=None).unsplit()
+        )
     config["repository"] = normalize_repository_url(cast(str, config["repository"]))
     return config
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 6ec178b9..714c0621 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -150,15 +150,38 @@ def test_get_repository_config_missing(config_file):
     assert utils.get_repository_from_config(config_file, "pypi") == exp
 
 
-def test_get_repository_config_url_with_auth(config_file):
-    repository_url = "https://user:pass@notexisting.python.org/pypi"
-    exp = {
-        "repository": "https://notexisting.python.org/pypi",
-        "username": "user",
-        "password": "pass",
-    }
-    assert utils.get_repository_from_config(config_file, "foo", repository_url) == exp
-    assert utils.get_repository_from_config(config_file, "pypi", repository_url) == exp
+@pytest.mark.parametrize(
+    "repository_url, expected_config",
+    [
+        (
+            "https://user:pass@notexisting.python.org/pypi",
+            {
+                "repository": "https://notexisting.python.org/pypi",
+                "username": "user",
+                "password": "pass",
+            },
+        ),
+        (
+            "https://auser:pass@pypi.proxy.local.repo.net:8443",
+            {
+                "repository": "https://pypi.proxy.local.repo.net:8443",
+                "username": "auser",
+                "password": "pass",
+            },
+        ),
+    ],
+)
+def test_get_repository_config_url_with_auth(
+    config_file, repository_url, expected_config
+):
+    assert (
+        utils.get_repository_from_config(config_file, "foo", repository_url)
+        == expected_config
+    )
+    assert (
+        utils.get_repository_from_config(config_file, "pypi", repository_url)
+        == expected_config
+    )
 
 
 @pytest.mark.parametrize(