diff --git a/docs/changelog.rst b/docs/changelog.rst
index fde2ef9..a4dafdd 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -16,6 +16,10 @@ Changelog
 
     - Password will default to ``TWINE_PASSWORD``
 
+  * :feature:`166` Allow the Repository URL to be provided on the command-line
+    (``--repository-url``) or via an environment variable
+    (``TWINE_REPOSITORY_URL``).
+
 * :release:`1.7.4 <2016-07-09>`
 
   * Correct a packaging error.
diff --git a/twine/commands/register.py b/twine/commands/register.py
index ea6732e..29744d1 100644
--- a/twine/commands/register.py
+++ b/twine/commands/register.py
@@ -24,8 +24,12 @@ from twine import utils
 
 
 def register(package, repository, username, password, comment, config_file,
-             cert, client_cert):
-    config = utils.get_repository_from_config(config_file, repository)
+             cert, client_cert, repository_url):
+    config = utils.get_repository_from_config(
+        config_file,
+        repository,
+        repository_url,
+    )
     config["repository"] = utils.normalize_repository_url(
         config["repository"]
     )
@@ -63,15 +67,26 @@ def main(args):
     parser.add_argument(
         "-r", "--repository",
         action=utils.EnvironmentDefault,
-        env='TWINE_REPOSITORY',
+        env="TWINE_REPOSITORY",
         default="pypi",
-        help="The repository to register the package to (default: "
+        help="The repository to register the package to. Can be a section in "
+             "the config file or a full URL to the repository (default: "
              "%(default)s)",
     )
+    parser.add_argument(
+        "--repository-url",
+        action=utils.EnvironmentDefault,
+        env="TWINE_REPOSITORY_URL",
+        default=None,
+        required=False,
+        help="The repository URL to upload the package to. This can be "
+             "specified with --repository because it will be used if there is "
+             "no configuration for the value passed to --repository."
+    )
     parser.add_argument(
         "-u", "--username",
         action=utils.EnvironmentDefault,
-        env='TWINE_USERNAME',
+        env="TWINE_USERNAME",
         required=False, help="The username to authenticate to the repository "
                              "as (can also be set via %(env)s environment "
                              "variable)",
@@ -79,7 +94,7 @@ def main(args):
     parser.add_argument(
         "-p", "--password",
         action=utils.EnvironmentDefault,
-        env='TWINE_PASSWORD',
+        env="TWINE_PASSWORD",
         required=False, help="The password to authenticate to the repository "
                              "with (can also be set via %(env)s environment "
                              "variable)",
diff --git a/twine/commands/upload.py b/twine/commands/upload.py
index 7f7335e..e78f4af 100644
--- a/twine/commands/upload.py
+++ b/twine/commands/upload.py
@@ -72,7 +72,8 @@ def skip_upload(response, skip_existing, package):
 
 
 def upload(dists, repository, sign, identity, username, password, comment,
-           sign_with, config_file, skip_existing, cert, client_cert):
+           sign_with, config_file, skip_existing, cert, client_cert,
+           repository_url):
     # Check that a nonsensical option wasn't given
     if not sign and identity:
         raise ValueError("sign must be given along with identity")
@@ -85,7 +86,11 @@ def upload(dists, repository, sign, identity, username, password, comment,
     )
     uploads = [i for i in dists if not i.endswith(".asc")]
 
-    config = utils.get_repository_from_config(config_file, repository)
+    config = utils.get_repository_from_config(
+        config_file,
+        repository,
+        repository_url,
+    )
 
     config["repository"] = utils.normalize_repository_url(
         config["repository"]
@@ -152,7 +157,19 @@ def main(args):
         action=utils.EnvironmentDefault,
         env="TWINE_REPOSITORY",
         default="pypi",
-        help="The repository to upload the files to (default: %(default)s)",
+        help="The repository to register the package to. Can be a section in "
+             "the config file or a full URL to the repository (default: "
+             "%(default)s)",
+    )
+    parser.add_argument(
+        "--repository-url",
+        action=utils.EnvironmentDefault,
+        env="TWINE_REPOSITORY_URL",
+        default=None,
+        required=False,
+        help="The repository URL to upload the package to. This can be "
+             "specified with --repository because it will be used if there is "
+             "no configuration for the value passed to --repository."
     )
     parser.add_argument(
         "-s", "--sign",
diff --git a/twine/utils.py b/twine/utils.py
index 3e21fde..9b2e8d6 100644
--- a/twine/utils.py
+++ b/twine/utils.py
@@ -40,6 +40,7 @@ else:
 
 
 DEFAULT_REPOSITORY = "https://upload.pypi.org/legacy/"
+TEST_REPOSITORY = "https://test.pypi.org/legacy/"
 
 
 def get_config(path="~/.pypirc"):
@@ -50,7 +51,11 @@ def get_config(path="~/.pypirc"):
         return {"pypi": {"repository": DEFAULT_REPOSITORY,
                          "username": None,
                          "password": None
-                         }
+                         },
+                "pypitest": {"repository": TEST_REPOSITORY,
+                             "username": None,
+                             "password": None
+                             },
                 }
 
     # Parse the rc file
@@ -94,13 +99,22 @@ def get_config(path="~/.pypirc"):
     return config
 
 
-def get_repository_from_config(config_file, repository):
+def get_repository_from_config(config_file, repository, repository_url=None):
     # Get our config from the .pypirc file
     try:
         return get_config(config_file)[repository]
     except KeyError:
+        if repository_url and "://" in repository_url:
+            # assume that the repsoitory is actually an URL and just sent
+            # them a dummy with the repo set
+            return {
+                "repository": repository_url,
+                "username": None,
+                "password": None,
+            }
         msg = (
-            "Missing '{repo}' section from the configuration file.\n"
+            "Missing '{repo}' section from the configuration file\n"
+            "or not a complete URL in --repository.\n"
             "Maybe you have a out-dated '{cfg}' format?\n"
             "more info: "
             "https://docs.python.org/distutils/packageindex.html#pypirc\n"

Make twine work without a config file
I wanted to add twine to a CI script but it wasn't trivial to find the pypirc file, so I had to regenerate via echo "..." calls (windows). 

It would be nice of twine could work with https://testpypi.python.org/pypi without first adding a config file .
diff --git a/tests/test_upload.py b/tests/test_upload.py
index 2649fe4..e134f82 100644
--- a/tests/test_upload.py
+++ b/tests/test_upload.py
@@ -82,10 +82,13 @@ def test_get_config_old_format(tmpdir):
         upload.upload(dists=dists, repository="pypi", sign=None, identity=None,
                       username=None, password=None, comment=None,
                       cert=None, client_cert=None,
-                      sign_with=None, config_file=pypirc, skip_existing=False)
+                      sign_with=None, config_file=pypirc, skip_existing=False,
+                      repository_url=None,
+                      )
     except KeyError as err:
         assert err.args[0] == (
-            "Missing 'pypi' section from the configuration file.\n"
+            "Missing 'pypi' section from the configuration file\n"
+            "or not a complete URL in --repository.\n"
             "Maybe you have a out-dated '{0}' format?\n"
             "more info: "
             "https://docs.python.org/distutils/packageindex.html#pypirc\n"
@@ -130,7 +133,9 @@ def test_skip_upload_respects_skip_existing(monkeypatch):
 
 
 def test_password_and_username_from_env(monkeypatch):
-    def none_upload(*args, **kwargs): pass
+    def none_upload(*args, **kwargs):
+        pass
+
     replaced_upload = pretend.call_recorder(none_upload)
     monkeypatch.setattr(twine.commands.upload, "upload", replaced_upload)
     testenv = {"TWINE_USERNAME": "pypiuser",
diff --git a/tests/test_utils.py b/tests/test_utils.py
index cb763b7..4c4d116 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -19,7 +19,6 @@ import textwrap
 
 import pytest
 
-from twine.utils import DEFAULT_REPOSITORY, get_config, get_userpass_value
 from twine import utils
 
 import helpers
@@ -38,9 +37,9 @@ def test_get_config(tmpdir):
             password = testpassword
         """))
 
-    assert get_config(pypirc) == {
+    assert utils.get_config(pypirc) == {
         "pypi": {
-            "repository": DEFAULT_REPOSITORY,
+            "repository": utils.DEFAULT_REPOSITORY,
             "username": "testuser",
             "password": "testpassword",
         },
@@ -57,9 +56,9 @@ def test_get_config_no_distutils(tmpdir):
             password = testpassword
         """))
 
-    assert get_config(pypirc) == {
+    assert utils.get_config(pypirc) == {
         "pypi": {
-            "repository": DEFAULT_REPOSITORY,
+            "repository": utils.DEFAULT_REPOSITORY,
             "username": "testuser",
             "password": "testpassword",
         },
@@ -79,9 +78,9 @@ def test_get_config_no_section(tmpdir):
             password = testpassword
         """))
 
-    assert get_config(pypirc) == {
+    assert utils.get_config(pypirc) == {
         "pypi": {
-            "repository": DEFAULT_REPOSITORY,
+            "repository": utils.DEFAULT_REPOSITORY,
             "username": "testuser",
             "password": "testpassword",
         },
@@ -91,23 +90,47 @@ def test_get_config_no_section(tmpdir):
 def test_get_config_missing(tmpdir):
     pypirc = os.path.join(str(tmpdir), ".pypirc")
 
-    assert get_config(pypirc) == {
+    assert utils.get_config(pypirc) == {
         "pypi": {
-            "repository": DEFAULT_REPOSITORY,
+            "repository": utils.DEFAULT_REPOSITORY,
             "username": None,
             "password": None,
         },
+        "pypitest": {
+            "repository": utils.TEST_REPOSITORY,
+            "username": None,
+            "password": None
+        },
     }
 
 
+def test_get_repository_config_missing(tmpdir):
+    pypirc = os.path.join(str(tmpdir), ".pypirc")
+
+    repository_url = "https://notexisting.python.org/pypi"
+    exp = {
+        "repository": repository_url,
+        "username": None,
+        "password": None,
+    }
+    assert (utils.get_repository_from_config(pypirc, 'foo', repository_url) ==
+            exp)
+    exp = {
+            "repository": utils.DEFAULT_REPOSITORY,
+            "username": None,
+            "password": None,
+        }
+    assert utils.get_repository_from_config(pypirc, "pypi") == exp
+
+
 def test_get_config_deprecated_pypirc():
     tests_dir = os.path.dirname(os.path.abspath(__file__))
     deprecated_pypirc_path = os.path.join(tests_dir, 'fixtures',
                                           'deprecated-pypirc')
 
-    assert get_config(deprecated_pypirc_path) == {
+    assert utils.get_config(deprecated_pypirc_path) == {
         "pypi": {
-            "repository": DEFAULT_REPOSITORY,
+            "repository": utils.DEFAULT_REPOSITORY,
             "username": 'testusername',
             "password": 'testpassword',
         },
@@ -123,7 +146,7 @@ def test_get_config_deprecated_pypirc():
     ),
 )
 def test_get_userpass_value(cli_value, config, key, strategy, expected):
-    ret = get_userpass_value(cli_value, config, key, strategy)
+    ret = utils.get_userpass_value(cli_value, config, key, strategy)
     assert ret == expected
 
 

pip install -e .
parse_log_pytest
pytest
coverage pretend pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
args==0.1.0
certifi==2025.1.31
charset-normalizer==3.4.1
clint==0.5.1
coverage==7.8.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pkginfo==1.12.1.2
pluggy @ file:///croot/pluggy_1733169602837/work
pretend==1.0.9
pytest @ file:///croot/pytest_1738938843180/work
requests==2.32.3
requests-toolbelt==1.0.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
-e git+https://github.com/pypa/twine.git@26a7c446888e50712168a395cd91360666264390#egg=twine
urllib3==2.3.0

name: twine
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - args==0.1.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - clint==0.5.1
      - coverage==7.8.0
      - idna==3.10
      - pkginfo==1.12.1.2
      - pretend==1.0.9
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - urllib3==2.3.0
prefix: /opt/conda/envs/twine

tests/test_upload.py::test_get_config_old_format tests/test_utils.py::test_get_config_missing tests/test_utils.py::test_get_repository_config_missing
tests/test_upload.py::test_ensure_wheel_files_uploaded_first tests/test_upload.py::test_ensure_if_no_wheel_files tests/test_upload.py::test_find_dists_expands_globs tests/test_upload.py::test_find_dists_errors_on_invalid_globs tests/test_upload.py::test_find_dists_handles_real_files tests/test_upload.py::test_skip_existing_skips_files_already_on_PyPI tests/test_upload.py::test_skip_existing_skips_files_already_on_pypiserver tests/test_upload.py::test_skip_upload_respects_skip_existing tests/test_upload.py::test_password_and_username_from_env tests/test_utils.py::test_get_config tests/test_utils.py::test_get_config_no_distutils tests/test_utils.py::test_get_config_no_section tests/test_utils.py::test_get_config_deprecated_pypirc tests/test_utils.py::test_get_userpass_value[cli-config0-key-<lambda>-cli] tests/test_utils.py::test_get_userpass_value[None-config1-key-<lambda>-value] tests/test_utils.py::test_get_userpass_value[None-config2-key-<lambda>-fallback] tests/test_utils.py::test_default_to_environment_action[MY_PASSWORD-None-environ0-None] tests/test_utils.py::test_default_to_environment_action[MY_PASSWORD-None-environ1-foo] tests/test_utils.py::test_default_to_environment_action[URL-https://example.org-environ2-https://example.org] tests/test_utils.py::test_default_to_environment_action[URL-https://example.org-environ3-https://pypi.org]
twine/commands/register.py docs/changelog.rst twine/commands/upload.py twine/utils.py
twine/commands/register.py docs/changelog.rst twine/commands/upload.py twine/utils.py