diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fe134..87372e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +- Fixed existing Gitea remote setup so `owner/name` connections can use the + machine's configured SSH key instead of always forcing HTTPS authentication. +- Added an explicit SSH/HTTPS clone protocol choice and optional + `GITEA_SSH_USER`/`GITEA_SSH_PORT` configuration. + All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. diff --git a/README.md b/README.md index 299915f..f36fd6a 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Supported variable names: - `GITEA_URL` or `GITEA_BASE_URL` - `GITEA_OWNER` - `GITEA_TOKEN` or `GITEA_ACCESS_TOKEN` +- `GITEA_SSH_USER` (defaults to `git`) +- `GITEA_SSH_PORT` (only needed for a non-default SSH port) Token handling behavior: @@ -76,6 +78,11 @@ Token handling behavior: - Otherwise token is requested in the TUI - Token is not persisted by `git-tui` +When connecting an existing Gitea repository by `owner/name`, choose SSH to +use an SSH key already configured on the machine, or HTTPS to use a configured +Git credential helper. You can also paste a complete clone URL to use it +unchanged. + ## Keybindings - Main menu: diff --git a/doc/architecture.md b/doc/architecture.md index 773ee2a..cbc5660 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -17,7 +17,7 @@ - Gitea via HTTPS API and token-authenticated initial push. - Executes provider-specific connection to existing remote repositories: - GitHub by resolving owner or URL input to `origin`. - - Gitea by resolving owner and base URL or URL input to `origin`. + - Gitea by resolving owner, base URL, and SSH/HTTPS transport or full URL input to `origin`. - Implements file selection for staged and unstaged operations. ## Public API and Contracts diff --git a/doc/usage.md b/doc/usage.md index 7546118..da99809 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -54,6 +54,7 @@ - Chooses provider: GitHub or Gitea. - GitHub accepts `owner/name` or full clone URL input. - Gitea accepts `owner/name` or full clone URL input, and uses `GITEA_URL` or `GITEA_BASE_URL` defaults when a base URL is needed. + - For Gitea `owner/name` input, chooses SSH (the default) or HTTPS transport. SSH uses `GITEA_SSH_USER` (default `git`) and optional `GITEA_SSH_PORT`; HTTPS uses the configured Git credential helper. - Configures remote `origin` to the selected repository URL. - Optionally pushes current branch with upstream tracking (`git push -u origin `). diff --git a/git-tui b/git-tui index f87d77e..f147597 100755 --- a/git-tui +++ b/git-tui @@ -457,9 +457,25 @@ class GitTuiApp: self.status_message = "Missing Gitea base URL." return "" + transport = self.select_option_menu( + "Gitea clone protocol", + "Use the protocol already configured for this Gitea instance", + [ + ("SSH (uses your configured SSH key)", "ssh"), + ("HTTPS (uses a Git credential helper)", "https"), + ("Cancel", "cancel"), + ], + ) + if transport is None or transport == "cancel": + self.status_message = "Remote connection canceled." + return "" + remote_url = self.resolve_gitea_remote_url( repo_input=normalized_repo_input, base_url=base_url, + transport=transport, + ssh_user=os.getenv("GITEA_SSH_USER", "git").strip() or "git", + ssh_port=os.getenv("GITEA_SSH_PORT", "").strip(), ) if not remote_url: self.show_output( @@ -471,15 +487,25 @@ class GitTuiApp: return "" return remote_url - def resolve_gitea_remote_url(self, repo_input: str, base_url: str) -> str: - """Builds a Gitea HTTPS clone URL from owner/name input. + def resolve_gitea_remote_url( + self, + repo_input: str, + base_url: str, + transport: str = "https", + ssh_user: str = "git", + ssh_port: str = "", + ) -> str: + """Builds a Gitea clone URL from owner/name input. Args: repo_input: Repository locator in owner/name format. base_url: Base URL of the Gitea instance. + transport: Clone protocol, either ``ssh`` or ``https``. + ssh_user: SSH username used for SSH clone URLs. + ssh_port: Optional SSH port used for SSH clone URLs. Returns: - The computed HTTPS clone URL, or an empty string when input is + The computed clone URL, or an empty string when input is invalid. """ @@ -493,7 +519,35 @@ class GitTuiApp: return "" if repo_name.endswith(".git"): repo_name = repo_name[:-4] - return f"{base_url}/{owner}/{repo_name}.git" + if not repo_name: + return "" + + if transport == "https": + return f"{base_url}/{owner}/{repo_name}.git" + if transport != "ssh": + return "" + + parsed_base_url = urllib_parse.urlparse(base_url) + ssh_host = parsed_base_url.hostname or "" + if not ssh_host or not ssh_user or any( + character in ssh_user for character in "@/: \t\r\n" + ): + return "" + if ssh_port: + try: + port_number = int(ssh_port) + except ValueError: + return "" + if port_number < 1 or port_number > 65535: + return "" + uri_host = f"[{ssh_host}]" if ":" in ssh_host else ssh_host + return ( + f"ssh://{ssh_user}@{uri_host}:{port_number}/" + f"{owner}/{repo_name}.git" + ) + if ":" in ssh_host: + return f"ssh://{ssh_user}@[{ssh_host}]/{owner}/{repo_name}.git" + return f"{ssh_user}@{ssh_host}:{owner}/{repo_name}.git" def normalize_remote_input(self, raw_value: str) -> str: """Normalizes user-provided repository input. diff --git a/tests/test_git_tui.py b/tests/test_git_tui.py new file mode 100644 index 0000000..0467bcb --- /dev/null +++ b/tests/test_git_tui.py @@ -0,0 +1,66 @@ +"""Focused tests for pure URL resolution behavior in the executable module.""" + +import importlib.machinery +import importlib.util +import pathlib +import sys +import unittest + + +SCRIPT_PATH = pathlib.Path(__file__).parents[1] / "git-tui" +LOADER = importlib.machinery.SourceFileLoader("git_tui", str(SCRIPT_PATH)) +SPEC = importlib.util.spec_from_loader(LOADER.name, LOADER) +assert SPEC is not None +git_tui = importlib.util.module_from_spec(SPEC) +sys.modules[LOADER.name] = git_tui +LOADER.exec_module(git_tui) + + +class GiteaRemoteUrlTests(unittest.TestCase): + def setUp(self) -> None: + self.app = git_tui.GitTuiApp.__new__(git_tui.GitTuiApp) + + def test_resolves_ssh_url_from_https_base_url(self) -> None: + remote_url = self.app.resolve_gitea_remote_url( + "owner/project", "https://git.example.com", transport="ssh" + ) + + self.assertEqual(remote_url, "git@git.example.com:owner/project.git") + + def test_resolves_custom_ssh_user_and_port(self) -> None: + remote_url = self.app.resolve_gitea_remote_url( + "owner/project", + "https://git.example.com/gitea", + transport="ssh", + ssh_user="forge", + ssh_port="2222", + ) + + self.assertEqual( + remote_url, + "ssh://forge@git.example.com:2222/owner/project.git", + ) + + def test_still_supports_https(self) -> None: + remote_url = self.app.resolve_gitea_remote_url( + "owner/project", "https://git.example.com", transport="https" + ) + + self.assertEqual( + remote_url, + "https://git.example.com/owner/project.git", + ) + + def test_rejects_invalid_ssh_port(self) -> None: + remote_url = self.app.resolve_gitea_remote_url( + "owner/project", + "https://git.example.com", + transport="ssh", + ssh_port="not-a-port", + ) + + self.assertEqual(remote_url, "") + + +if __name__ == "__main__": + unittest.main()