updated gitdesk for 100mb limit bypass

This commit is contained in:
Lenrui committed 2026-11-05 23:51:06 -08:00
1 parent 874df3230e
commit 0d1ff65479
2 files changed
+132 -4

No files matched your search

+7
View File
@@ -82,6 +82,13 @@ Tip: put this folder on your PATH (Windows), or symlink it on Linux/macOS
- **Switching branch blocked by your changes:** it offers *Stash & switch*.
- **A network command needs a password, token, SSH passphrase or host-key confirmation:**
click **Retry in terminal**. gitdesk steps aside so git can ask you, then comes back.
- **Push refused as too big over HTTPS (HTTP 413)**, e.g. by Cloudflare's 100 MB upload limit:
gitdesk offers **Push over SSH** with the SSH address filled in. You can change the address,
and it's remembered per repo.
- Tick *Always push this repository over SSH* to set git's push URL, so pushes go over SSH
while fetches stay on HTTPS.
- If the site is behind Cloudflare, SSH needs an address that reaches the server directly:
a DNS-only hostname or the server's IP.
- **Another program has the repository busy** (GitHub Desktop, VS Code, Unity, OneDrive,
antivirus): gitdesk waits and retries for a few seconds. If a `.git/index.lock` is left
behind by a crashed git, it offers **Remove lock file & retry**.
+125 -4
View File
@@ -195,6 +195,27 @@ def looks_lockish(r: Result) -> bool:
return not residue or bool(LOCKISH_RE.search(residue))
TOO_LARGE_RE = re.compile(r"HTTP[/ \d.]*\s413\b|error: 413\b|status(?: code)?:? 413\b|\b413 Request"
r"|Request Entity Too Large|Payload Too Large|Content Too Large", re.I)
def too_large(r: Result) -> bool:
"""The server (or a proxy like Cloudflare) refused an HTTP upload as too big."""
return bool(TOO_LARGE_RE.search(f"{r.err}\n{r.out}"))
def ssh_url_for(http_url: str) -> str:
"""'https://git.example.net/Owner/Repo.git' -> '[email protected]:Owner/Repo.git'."""
m = re.match(r"^https?://(?:[^@/]+@)?([^/:]+)(?::\d+)?/(.+?)(?:\.git)?/*$", http_url.strip())
return f"git@{m.group(1)}:{m.group(2)}.git" if m else ""
def user_ssh_command_set(path: str) -> bool:
if os.environ.get("GIT_SSH_COMMAND") or os.environ.get("GIT_SSH"):
return True
return run_git(path, ["config", "--get", "core.sshCommand"]).ok
def hint_for(message: str) -> str:
"""Plain-English next step for common git errors."""
if "Filename too long" in message:
@@ -1790,6 +1811,62 @@ class CloneDialog(ChooseFolderMixin, ModalScreen):
self.dismiss(None)
class SshPushDialog(ModalScreen):
"""Offered when an HTTPS push is refused as too large. Returns (ssh_url, always) or None."""
BINDINGS = [Binding("escape", "cancel", "Cancel")]
def __init__(self, error: str, suggestion: str) -> None:
super().__init__()
self.error, self.suggestion = error, suggestion
def compose(self) -> ComposeResult:
with Vertical(classes="dialog wide"):
yield Label(Text("Push is too big for HTTPS", style="bold"), classes="dialog-title")
yield Static(Text(self.error, style="#f97583"))
yield Static(Text(
"\nThe server - or a proxy in front of it - refused this push because the upload is too big "
"(HTTP 413). Cloudflare's free plan, for example, stops any single upload over 100 MB. Git LFS "
"files upload one at a time so they usually get through, but the rest of the push is sent "
"in one piece. Pushing over SSH avoids the limit."))
yield Label("SSH address of this repository", classes="field-label")
yield Input(value=self.suggestion, placeholder="git@host:owner/repo.git or ssh://git@host:22/owner/repo.git",
id="ssh-url", compact=True)
yield Static(Text(
"If the website goes through Cloudflare or another proxy, SSH has to use an address that reaches "
"the server directly - a DNS-only hostname or the server's IP, e.g. "
"ssh://[email protected]:22/owner/repo.git", style=C_MUTED), classes="hint")
yield Checkbox("Always push this repository over SSH (fetching stays on HTTPS)", value=True,
id="ssh-always", compact=True)
yield Static(id="ssh-error", classes="error")
with Horizontal(classes="dialog-buttons"):
yield Button("Push over SSH", variant="primary", name="push", compact=True)
yield Button("Cancel", name="cancel", compact=True)
def on_mount(self) -> None:
self.query_one("#ssh-url", Input).focus()
def on_input_submitted(self, event: Input.Submitted) -> None:
event.stop()
self._submit()
def _submit(self) -> None:
url = self.query_one("#ssh-url", Input).value.strip()
if not url:
self.query_one("#ssh-error", Static).update(Text("Enter the SSH address.", style=C_RED))
return
self.dismiss((url, self.query_one("#ssh-always", Checkbox).value))
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop()
if event.button.name == "push":
self._submit()
else:
self.dismiss(None)
def action_cancel(self) -> None:
self.dismiss(None)
ACTIONS = [
("fetch", "Fetch"),
("pull", "Pull"),
@@ -1938,6 +2015,7 @@ ModalScreen { align: center middle; background: rgba(0, 0, 0, 0.55); }
.hint { height: auto; color: #959da5; }
.error { height: auto; margin-top: 1; }
#nr-readme { margin-top: 1; }
#ssh-always { margin-top: 1; }
.path-row { height: auto; }
.path-row Input { width: 1fr; }
.path-row Button { margin-left: 1; min-width: 12; }
@@ -2804,13 +2882,19 @@ class GitDesk(App):
buttons = [("retry", "Try again", "primary"), ("close", "Close", "default")]
return await self.ask("The repository is busy", body, buttons, cancel="close")
async def net(self, args: List[str], what: str) -> bool:
"""Run a network git command in the background; on failure offer to retry."""
async def net(self, args: List[str], what: str, push: Optional[Tuple[str, List[str]]] = None) -> bool:
"""Run a network git command in the background; on failure offer to retry.
push=(remote, push args): if an HTTPS push is refused as too big (HTTP 413),
offer to push over SSH instead.
"""
while True:
async with self._net_lock:
r = await self.bg(*args, serialize=False)
if r.ok:
return True
if push and too_large(r) and self.push_url(push[0]).startswith(("http://", "https://")):
return await self.ssh_push_flow(push[0], push[1], r)
tail = "\n".join(r.message.splitlines()[-10:])
body = Text.assemble((tail, "#f97583"), "\n\n",
("If git needs a password or token, an SSH passphrase, or wants you to "
@@ -2824,6 +2908,41 @@ class GitDesk(App):
if choice != "retry":
return False
def push_url(self, remote: str) -> str:
r = self.git.run("config", "--get", f"remote.{remote}.pushurl", log=False)
return r.out.strip() if r.ok and r.out.strip() else (self.state.remote_urls.get(remote, "") if self.state else "")
async def ssh_push_flow(self, remote: str, push_args: List[str], failed: Result) -> bool:
saved = self.git.run("config", "--get", "gitdesk.sshUrl", log=False).out.strip()
suggestion = saved or ssh_url_for(self.push_url(remote))
detail = "\n".join(line for line in failed.message.splitlines()
if "413" in line or "RPC failed" in line or "hung up" in line)[-600:]
res = await self.push_screen_wait(SshPushDialog(detail or failed.message[-600:], suggestion))
if not res:
return False
url, always = res
await self.bg("config", "gitdesk.sshUrl", url) # prefilled next time
prefix: List[str] = []
if not await asyncio.to_thread(user_ssh_command_set, self.git.path):
# fail fast (instead of hanging for minutes) if the SSH port isn't reachable
prefix = ["-c", "core.sshCommand=ssh -o ConnectTimeout=15"]
if always:
r = await self.gitop("remote", "set-url", "--push", remote, url)
if not r.ok:
await self.fail("Couldn't save the SSH push address", r)
return False
args = prefix + ["push", *push_args]
else:
args = prefix + ["-c", f"remote.{remote}.pushurl={url}", "push", *push_args]
ok = await self.net(args, f"push to {remote} over SSH")
if ok and always:
self.notify("Pushed over SSH. This repository will push over SSH from now on; fetching still "
"uses HTTPS.", timeout=10)
elif not ok and always:
# didn't work - put pushing back the way it was
await self.gitop("config", "--unset", f"remote.{remote}.pushurl")
return ok
def run_in_terminal(self, args: List[str], cwd: Optional[str] = None) -> bool:
rc = 1
try:
@@ -3054,7 +3173,8 @@ class GitDesk(App):
return
remote = self.remote_name()
if not st.has_upstream:
if await self.net(["push", "-u", remote, st.branch], f"publish to {remote}"):
if await self.net(["push", "-u", remote, st.branch], f"publish to {remote}",
push=(remote, ["-u", remote, st.branch])):
self.notify(f"Published '{st.branch}' to {remote}.")
return
if not await self.do_fetch():
@@ -3077,7 +3197,8 @@ class GitDesk(App):
return
target = st.upstream[len(remote) + 1:]
n = st.ahead
if await self.net(["push", remote, f"HEAD:refs/heads/{target}"], f"push to {remote}"):
if await self.net(["push", remote, f"HEAD:refs/heads/{target}"], f"push to {remote}",
push=(remote, [remote, f"HEAD:refs/heads/{target}"])):
self.notify(f"Pushed {plural(n, 'commit')} to {remote}.")
async def commit_flow(self) -> None: