Stop hardcoding port
This commit is contained in:
@@ -23,6 +23,10 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
|
|||||||
machine's configured SSH key instead of always forcing HTTPS authentication.
|
machine's configured SSH key instead of always forcing HTTPS authentication.
|
||||||
- Fixed synchronization of unborn repositories, which previously attempted to
|
- Fixed synchronization of unborn repositories, which previously attempted to
|
||||||
push a nonexistent `main` ref.
|
push a nonexistent `main` ref.
|
||||||
|
- Fixed custom Gitea SSH endpoints by preserving or retrieving the advertised
|
||||||
|
clone URL instead of guessing `git@<web-host>` on port 22.
|
||||||
|
- Fixed network authentication by allowing Git and SSH to use the real terminal
|
||||||
|
instead of disabling prompts globally.
|
||||||
|
|
||||||
## [0.2.0] - 2026-02-15
|
## [0.2.0] - 2026-02-15
|
||||||
|
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ Supported variable names:
|
|||||||
- `GITEA_URL` or `GITEA_BASE_URL`
|
- `GITEA_URL` or `GITEA_BASE_URL`
|
||||||
- `GITEA_OWNER`
|
- `GITEA_OWNER`
|
||||||
- `GITEA_TOKEN` or `GITEA_ACCESS_TOKEN`
|
- `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:
|
Token handling behavior:
|
||||||
|
|
||||||
@@ -84,10 +82,11 @@ Token handling behavior:
|
|||||||
- Otherwise token is requested in the TUI
|
- Otherwise token is requested in the TUI
|
||||||
- Token is not persisted by `git-tui`
|
- Token is not persisted by `git-tui`
|
||||||
|
|
||||||
When connecting an existing Gitea repository by `owner/name`, choose SSH to
|
When connecting an existing Gitea repository, paste the complete clone URL or
|
||||||
use an SSH key already configured on the machine, or HTTPS to use a configured
|
provide `owner/name` and ask the Gitea API to return its exact clone endpoints.
|
||||||
Git credential helper. You can also paste a complete clone URL to use it
|
The app does not derive an SSH endpoint from the web URL because the SSH user,
|
||||||
unchanged.
|
host, and port can differ. Private API lookups require a token; pasting the
|
||||||
|
clone URL does not.
|
||||||
|
|
||||||
Creating a Gitea repository still uses an API token, because Git/SSH cannot
|
Creating a Gitea repository still uses an API token, because Git/SSH cannot
|
||||||
create repositories. The Git remote transport is selected separately; choosing
|
create repositories. The Git remote transport is selected separately; choosing
|
||||||
@@ -99,6 +98,11 @@ offers to merge, replace local history after creating a backup branch, or
|
|||||||
force-push local history with `--force-with-lease`. Uncommitted changes must be
|
force-push local history with `--force-with-lease`. Uncommitted changes must be
|
||||||
committed or stashed first and are never silently discarded.
|
committed or stashed first and are never silently discarded.
|
||||||
|
|
||||||
|
Network Git commands temporarily leave the curses screen and use the real
|
||||||
|
terminal. SSH passphrase prompts, HTTPS username/token prompts, and interactive
|
||||||
|
credential helpers therefore behave as they do for Git commands run directly
|
||||||
|
from the shell.
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
- Main menu:
|
- Main menu:
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@
|
|||||||
- Gitea via HTTPS API, followed by an SSH or token-authenticated HTTPS push.
|
- Gitea via HTTPS API, followed by an SSH or token-authenticated HTTPS push.
|
||||||
- Executes provider-specific connection to existing remote repositories:
|
- Executes provider-specific connection to existing remote repositories:
|
||||||
- GitHub by resolving owner or URL input to `origin`.
|
- GitHub by resolving owner or URL input to `origin`.
|
||||||
- Gitea by resolving owner, base URL, and SSH/HTTPS transport or full URL input to `origin`.
|
- Gitea by preserving a full clone URL or retrieving exact SSH/HTTPS clone endpoints through the repository API.
|
||||||
- Synchronizes Gitea connections by inspecting ancestry, fast-forwarding the
|
- Synchronizes Gitea connections by inspecting ancestry, fast-forwarding the
|
||||||
side that is behind, or asking the user how to resolve divergent histories.
|
side that is behind, or asking the user how to resolve divergent histories.
|
||||||
- Preserves local history on a backup branch before a user-confirmed local
|
- Preserves local history on a backup branch before a user-confirmed local
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
- Requires `git` to be installed and available in `PATH`.
|
- Requires `git` to be installed and available in `PATH`.
|
||||||
- Requires `gh` in `PATH` only for GitHub publication.
|
- Requires `gh` in `PATH` only for GitHub publication.
|
||||||
- Requires Gitea API token for Gitea publication.
|
- Requires Gitea API token for Gitea publication.
|
||||||
- Runs Git subprocesses in non-interactive mode to avoid terminal credential prompt deadlocks.
|
- Temporarily suspends curses for network Git subprocesses so terminal authentication and credential helpers remain available.
|
||||||
|
|
||||||
## Data Model
|
## Data Model
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -41,7 +41,7 @@
|
|||||||
- `GITEA_URL` or `GITEA_BASE_URL` for instance URL default.
|
- `GITEA_URL` or `GITEA_BASE_URL` for instance URL default.
|
||||||
- `GITEA_OWNER` for default owner or organization.
|
- `GITEA_OWNER` for default owner or organization.
|
||||||
- `GITEA_TOKEN` or `GITEA_ACCESS_TOKEN` for API token default.
|
- `GITEA_TOKEN` or `GITEA_ACCESS_TOKEN` for API token default.
|
||||||
- Git credential prompts are disabled inside `git-tui`; authentication issues are reported in the UI instead of opening an interactive terminal prompt.
|
- Network Git commands temporarily suspend curses and use the real terminal, allowing normal SSH passphrase, HTTPS credential, and credential-helper prompts.
|
||||||
|
|
||||||
## Existing Repository Without Remote Flow
|
## Existing Repository Without Remote Flow
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
- Continue without remote configuration.
|
- Continue without remote configuration.
|
||||||
- Quit.
|
- Quit.
|
||||||
- Connect existing remote flow:
|
- Connect existing remote flow:
|
||||||
- Gitea accepts `owner/name` or full clone URL input, and uses `GITEA_URL` or `GITEA_BASE_URL` defaults when a base URL is needed.
|
- A full clone URL is used unchanged, preserving custom SSH users, aliases, hosts, and ports.
|
||||||
- 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.
|
- For `owner/name`, uses the Gitea API to obtain the instance's exact `ssh_url` or `clone_url`; private repository lookup requires an API token.
|
||||||
- Configures remote `origin` to the selected repository URL.
|
- Configures remote `origin` to the selected repository URL.
|
||||||
- Inspects both histories and synchronizes automatically when one side can be fast-forwarded.
|
- Inspects both histories and synchronizes automatically when one side can be fast-forwarded.
|
||||||
- If histories diverge, offers merge, replace-local-with-backup, and force-push-with-lease choices.
|
- If histories diverge, offers merge, replace-local-with-backup, and force-push-with-lease choices.
|
||||||
|
|||||||
@@ -425,7 +425,9 @@ class GitTuiApp:
|
|||||||
local_has_commit = self.has_local_commit()
|
local_has_commit = self.has_local_commit()
|
||||||
|
|
||||||
if not remote_branch:
|
if not remote_branch:
|
||||||
heads_exit_code, heads_output = run_git(["ls-remote", "--heads", "origin"])
|
heads_exit_code, heads_output = self.run_git_network(
|
||||||
|
["ls-remote", "--heads", "origin"]
|
||||||
|
)
|
||||||
if heads_exit_code != 0:
|
if heads_exit_code != 0:
|
||||||
self.show_command_result(
|
self.show_command_result(
|
||||||
["git", "ls-remote", "--heads", "origin"],
|
["git", "ls-remote", "--heads", "origin"],
|
||||||
@@ -453,7 +455,9 @@ class GitTuiApp:
|
|||||||
return self.push_head_to_remote_branch(self.current_local_branch())
|
return self.push_head_to_remote_branch(self.current_local_branch())
|
||||||
|
|
||||||
fetch_command = ["git", "fetch", "origin", remote_branch]
|
fetch_command = ["git", "fetch", "origin", remote_branch]
|
||||||
fetch_exit_code, fetch_output = run_git(["fetch", "origin", remote_branch])
|
fetch_exit_code, fetch_output = self.run_git_network(
|
||||||
|
["fetch", "origin", remote_branch]
|
||||||
|
)
|
||||||
if fetch_exit_code != 0:
|
if fetch_exit_code != 0:
|
||||||
self.show_command_result(fetch_command, fetch_exit_code, fetch_output)
|
self.show_command_result(fetch_command, fetch_exit_code, fetch_output)
|
||||||
return False
|
return False
|
||||||
@@ -606,7 +610,7 @@ class GitTuiApp:
|
|||||||
def remote_default_branch(self, remote_name: str) -> str:
|
def remote_default_branch(self, remote_name: str) -> str:
|
||||||
"""Finds the default branch advertised by a remote, if it has one."""
|
"""Finds the default branch advertised by a remote, if it has one."""
|
||||||
|
|
||||||
exit_code, output = run_git(
|
exit_code, output = self.run_git_network(
|
||||||
["ls-remote", "--symref", remote_name, "HEAD"]
|
["ls-remote", "--symref", remote_name, "HEAD"]
|
||||||
)
|
)
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
@@ -663,12 +667,60 @@ class GitTuiApp:
|
|||||||
args.append("--force-with-lease")
|
args.append("--force-with-lease")
|
||||||
args.extend(["origin", f"HEAD:refs/heads/{remote_branch}"])
|
args.extend(["origin", f"HEAD:refs/heads/{remote_branch}"])
|
||||||
command = ["git", *args]
|
command = ["git", *args]
|
||||||
exit_code, output = run_git(args)
|
exit_code, output = self.run_git_network(args, capture_stdout=False)
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
self.show_command_result(command, exit_code, output)
|
self.show_command_result(command, exit_code, output)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def run_git_network(
|
||||||
|
self,
|
||||||
|
args: Sequence[str],
|
||||||
|
capture_stdout: bool = True,
|
||||||
|
) -> tuple[int, str]:
|
||||||
|
"""Runs network Git with access to the user's real terminal.
|
||||||
|
|
||||||
|
Curses is temporarily suspended so OpenSSH, Git credential helpers,
|
||||||
|
and HTTPS username/token prompts behave exactly as they do for a Git
|
||||||
|
command typed directly in the shell.
|
||||||
|
"""
|
||||||
|
|
||||||
|
command = ["git", *args]
|
||||||
|
can_use_terminal = (
|
||||||
|
hasattr(self, "stdscr")
|
||||||
|
and sys.stdin.isatty()
|
||||||
|
and sys.stdout.isatty()
|
||||||
|
)
|
||||||
|
if not can_use_terminal:
|
||||||
|
return run_command(command)
|
||||||
|
|
||||||
|
curses_suspended = False
|
||||||
|
try:
|
||||||
|
curses.def_prog_mode()
|
||||||
|
curses.endwin()
|
||||||
|
curses_suspended = True
|
||||||
|
completed = subprocess.run(
|
||||||
|
command,
|
||||||
|
check=False,
|
||||||
|
stdin=None,
|
||||||
|
stdout=subprocess.PIPE if capture_stdout else None,
|
||||||
|
stderr=None,
|
||||||
|
text=True,
|
||||||
|
env=os.environ.copy(),
|
||||||
|
)
|
||||||
|
output = completed.stdout or ""
|
||||||
|
if completed.returncode != 0:
|
||||||
|
print("\nGit command failed. Press Enter to return to git-tui.")
|
||||||
|
try:
|
||||||
|
input()
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
return completed.returncode, output.rstrip()
|
||||||
|
finally:
|
||||||
|
if curses_suspended:
|
||||||
|
curses.reset_prog_mode()
|
||||||
|
self.stdscr.refresh()
|
||||||
|
|
||||||
def create_local_backup_branch(self) -> str:
|
def create_local_backup_branch(self) -> str:
|
||||||
"""Creates a uniquely named branch preserving the current HEAD."""
|
"""Creates a uniquely named branch preserving the current HEAD."""
|
||||||
|
|
||||||
@@ -797,24 +849,90 @@ class GitTuiApp:
|
|||||||
self.status_message = "Missing Gitea base URL."
|
self.status_message = "Missing Gitea base URL."
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
resolution = self.select_option_menu(
|
||||||
|
"Resolve Gitea clone endpoint",
|
||||||
|
"The web URL does not determine the Git SSH user, host, or port",
|
||||||
|
[
|
||||||
|
("Paste exact clone URL from Gitea", "clone_url"),
|
||||||
|
("Ask the Gitea API for clone URLs", "api"),
|
||||||
|
("Cancel", "cancel"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if resolution is None or resolution == "cancel":
|
||||||
|
self.status_message = "Remote connection canceled."
|
||||||
|
return ""
|
||||||
|
if resolution == "clone_url":
|
||||||
|
clone_url = self.prompt_input("Exact Gitea SSH or HTTPS clone URL")
|
||||||
|
normalized_clone_url = self.normalize_remote_input(clone_url or "")
|
||||||
|
if self.looks_like_clone_url(normalized_clone_url):
|
||||||
|
return normalized_clone_url
|
||||||
|
self.show_output(
|
||||||
|
"Invalid clone URL",
|
||||||
|
"Copy the SSH or HTTPS clone URL shown by Gitea for this "
|
||||||
|
"repository.",
|
||||||
|
)
|
||||||
|
self.status_message = "Invalid Gitea clone URL."
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return self.resolve_existing_gitea_url_via_api(
|
||||||
|
repo_input=normalized_repo_input,
|
||||||
|
base_url=base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_existing_gitea_url_via_api(
|
||||||
|
self, repo_input: str, base_url: str
|
||||||
|
) -> str:
|
||||||
|
"""Gets exact clone endpoints for an existing repository from Gitea."""
|
||||||
|
|
||||||
|
if "/" not in repo_input:
|
||||||
|
self.show_output(
|
||||||
|
"Invalid Gitea repository",
|
||||||
|
"Provide owner/name or paste a full clone URL.",
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
owner, repo_name = repo_input.split("/", maxsplit=1)
|
||||||
|
owner = owner.strip()
|
||||||
|
repo_name = repo_name.removesuffix(".git").strip()
|
||||||
|
if not owner or not repo_name:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
token = (
|
||||||
|
os.getenv("GITEA_TOKEN", "").strip()
|
||||||
|
or os.getenv("GITEA_ACCESS_TOKEN", "").strip()
|
||||||
|
)
|
||||||
|
if not token:
|
||||||
|
token = self.prompt_secret(
|
||||||
|
"Gitea API token (blank only works for public repositories)"
|
||||||
|
) or ""
|
||||||
|
|
||||||
|
endpoint = (
|
||||||
|
f"{base_url}/api/v1/repos/{urllib_parse.quote(owner, safe='')}/"
|
||||||
|
f"{urllib_parse.quote(repo_name, safe='')}"
|
||||||
|
)
|
||||||
|
status_code, response_body, response_data = self.gitea_get_json(
|
||||||
|
url=endpoint,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
if status_code != 200:
|
||||||
|
self.show_output(
|
||||||
|
"Could not resolve Gitea clone URLs",
|
||||||
|
f"HTTP status: {status_code or 'request error'}\n"
|
||||||
|
f"Endpoint: {endpoint}\n\n"
|
||||||
|
f"{response_body or 'Repository not found or authentication required.'}",
|
||||||
|
)
|
||||||
|
self.status_message = "Could not resolve Gitea clone URLs."
|
||||||
|
return ""
|
||||||
|
|
||||||
transport = self.select_gitea_transport()
|
transport = self.select_gitea_transport()
|
||||||
if not transport:
|
if not transport:
|
||||||
return ""
|
return ""
|
||||||
|
response_key = "ssh_url" if transport == "ssh" else "clone_url"
|
||||||
remote_url = self.resolve_gitea_remote_url(
|
remote_url = str(response_data.get(response_key) or "").strip()
|
||||||
repo_input=normalized_repo_input,
|
if not self.looks_like_clone_url(remote_url):
|
||||||
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(
|
self.show_output(
|
||||||
"Invalid Gitea repository",
|
"Missing clone URL",
|
||||||
"Provide owner/name (for example acme/project) or a full "
|
f"Gitea did not return a usable {transport.upper()} clone URL.",
|
||||||
"clone URL.",
|
|
||||||
)
|
)
|
||||||
self.status_message = "Invalid Gitea repository."
|
|
||||||
return ""
|
return ""
|
||||||
return remote_url
|
return remote_url
|
||||||
|
|
||||||
@@ -835,68 +953,6 @@ class GitTuiApp:
|
|||||||
return ""
|
return ""
|
||||||
return transport
|
return transport
|
||||||
|
|
||||||
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 clone URL, or an empty string when input is
|
|
||||||
invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
normalized_input = self.normalize_remote_input(repo_input)
|
|
||||||
if not normalized_input or "/" not in normalized_input:
|
|
||||||
return ""
|
|
||||||
owner, repo_name = normalized_input.split("/", maxsplit=1)
|
|
||||||
owner = owner.strip()
|
|
||||||
repo_name = repo_name.strip()
|
|
||||||
if not owner or not repo_name:
|
|
||||||
return ""
|
|
||||||
if repo_name.endswith(".git"):
|
|
||||||
repo_name = repo_name[:-4]
|
|
||||||
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:
|
def normalize_remote_input(self, raw_value: str) -> str:
|
||||||
"""Normalizes user-provided repository input.
|
"""Normalizes user-provided repository input.
|
||||||
|
|
||||||
@@ -1281,13 +1337,7 @@ class GitTuiApp:
|
|||||||
|
|
||||||
remote_url = clone_url
|
remote_url = clone_url
|
||||||
if transport == "ssh":
|
if transport == "ssh":
|
||||||
remote_url = ssh_url or self.resolve_gitea_remote_url(
|
remote_url = ssh_url
|
||||||
repo_input=full_name or f"{owner}/{repo_name}",
|
|
||||||
base_url=base_url,
|
|
||||||
transport="ssh",
|
|
||||||
ssh_user=os.getenv("GITEA_SSH_USER", "git").strip() or "git",
|
|
||||||
ssh_port=os.getenv("GITEA_SSH_PORT", "").strip(),
|
|
||||||
)
|
|
||||||
if not remote_url:
|
if not remote_url:
|
||||||
self.show_output(
|
self.show_output(
|
||||||
"Missing SSH clone URL",
|
"Missing SSH clone URL",
|
||||||
@@ -1430,14 +1480,10 @@ class GitTuiApp:
|
|||||||
A tuple of `(status_code, response_text, response_data)`.
|
A tuple of `(status_code, response_text, response_data)`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
request = urllib_request.Request(
|
headers = {"Accept": "application/json"}
|
||||||
url=url,
|
if token:
|
||||||
method="GET",
|
headers["Authorization"] = f"token {token}"
|
||||||
headers={
|
request = urllib_request.Request(url=url, method="GET", headers=headers)
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": f"token {token}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
with urllib_request.urlopen(request, timeout=30) as response:
|
with urllib_request.urlopen(request, timeout=30) as response:
|
||||||
response_text = response.read().decode("utf-8", errors="replace")
|
response_text = response.read().decode("utf-8", errors="replace")
|
||||||
@@ -1545,7 +1591,9 @@ class GitTuiApp:
|
|||||||
if branch_name == "detached":
|
if branch_name == "detached":
|
||||||
branch_name = "main"
|
branch_name = "main"
|
||||||
command = ["git", "push", "-u", "origin", branch_name]
|
command = ["git", "push", "-u", "origin", branch_name]
|
||||||
exit_code, output = run_git(["push", "-u", "origin", branch_name])
|
exit_code, output = self.run_git_network(
|
||||||
|
["push", "-u", "origin", branch_name], capture_stdout=False
|
||||||
|
)
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
self.show_command_result(command, exit_code, output)
|
self.show_command_result(command, exit_code, output)
|
||||||
return False
|
return False
|
||||||
@@ -1858,19 +1906,23 @@ class GitTuiApp:
|
|||||||
def action_push(self) -> None:
|
def action_push(self) -> None:
|
||||||
"""Pushes current branch changes to default remote."""
|
"""Pushes current branch changes to default remote."""
|
||||||
|
|
||||||
exit_code, output = run_git(["push"])
|
exit_code, output = self.run_git_network(["push"], capture_stdout=False)
|
||||||
self.show_command_result(["git", "push"], exit_code, output)
|
self.show_command_result(["git", "push"], exit_code, output)
|
||||||
|
|
||||||
def action_pull(self) -> None:
|
def action_pull(self) -> None:
|
||||||
"""Pulls remote changes using rebase mode."""
|
"""Pulls remote changes using rebase mode."""
|
||||||
|
|
||||||
exit_code, output = run_git(["pull", "--rebase"])
|
exit_code, output = self.run_git_network(
|
||||||
|
["pull", "--rebase"], capture_stdout=False
|
||||||
|
)
|
||||||
self.show_command_result(["git", "pull", "--rebase"], exit_code, output)
|
self.show_command_result(["git", "pull", "--rebase"], exit_code, output)
|
||||||
|
|
||||||
def action_fetch(self) -> None:
|
def action_fetch(self) -> None:
|
||||||
"""Fetches updates from all remotes and prunes deleted refs."""
|
"""Fetches updates from all remotes and prunes deleted refs."""
|
||||||
|
|
||||||
exit_code, output = run_git(["fetch", "--all", "--prune"])
|
exit_code, output = self.run_git_network(
|
||||||
|
["fetch", "--all", "--prune"], capture_stdout=False
|
||||||
|
)
|
||||||
self.show_command_result(
|
self.show_command_result(
|
||||||
["git", "fetch", "--all", "--prune"], exit_code, output
|
["git", "fetch", "--all", "--prune"], exit_code, output
|
||||||
)
|
)
|
||||||
|
|||||||
+31
-32
@@ -24,47 +24,46 @@ class GiteaRemoteUrlTests(unittest.TestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.app = git_tui.GitTuiApp.__new__(git_tui.GitTuiApp)
|
self.app = git_tui.GitTuiApp.__new__(git_tui.GitTuiApp)
|
||||||
|
|
||||||
def test_resolves_ssh_url_from_https_base_url(self) -> None:
|
def test_exact_custom_ssh_clone_url_is_preserved(self) -> None:
|
||||||
remote_url = self.app.resolve_gitea_remote_url(
|
expected_url = "ssh://forge@git.example.com:2222/owner/project.git"
|
||||||
"owner/project", "https://git.example.com", transport="ssh"
|
self.app.prompt_input = lambda *_args: expected_url
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(remote_url, "git@git.example.com:owner/project.git")
|
remote_url = self.app.prompt_gitea_existing_remote_url()
|
||||||
|
|
||||||
def test_resolves_custom_ssh_user_and_port(self) -> None:
|
self.assertEqual(remote_url, expected_url)
|
||||||
remote_url = self.app.resolve_gitea_remote_url(
|
|
||||||
"owner/project",
|
def test_api_resolution_uses_giteas_advertised_ssh_url(self) -> None:
|
||||||
"https://git.example.com/gitea",
|
self.app.prompt_secret = lambda *_args: "token"
|
||||||
transport="ssh",
|
self.app.select_gitea_transport = lambda: "ssh"
|
||||||
ssh_user="forge",
|
requested_urls = []
|
||||||
ssh_port="2222",
|
|
||||||
)
|
def fake_get(url: str, token: str):
|
||||||
|
requested_urls.append((url, token))
|
||||||
|
return 200, "", {
|
||||||
|
"ssh_url": "ssh://forge@ssh.example.com:2222/owner/project.git",
|
||||||
|
"clone_url": "https://git.example.com/owner/project.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.app.gitea_get_json = fake_get
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"GITEA_TOKEN": "", "GITEA_ACCESS_TOKEN": ""},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
remote_url = self.app.resolve_existing_gitea_url_via_api(
|
||||||
|
"owner/project", "https://git.example.com"
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
remote_url,
|
remote_url,
|
||||||
"ssh://forge@git.example.com:2222/owner/project.git",
|
"ssh://forge@ssh.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(
|
self.assertEqual(
|
||||||
remote_url,
|
requested_urls,
|
||||||
"https://git.example.com/owner/project.git",
|
[("https://git.example.com/api/v1/repos/owner/project", "token")],
|
||||||
)
|
)
|
||||||
|
|
||||||
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, "")
|
|
||||||
|
|
||||||
|
|
||||||
class RepositorySyncTests(unittest.TestCase):
|
class RepositorySyncTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user