updated gitdesk for 100mb limit bypass
This commit is contained in:
1 parent
874df3230e
commit
0d1ff65479
2 files changed
+132
-4
No files matched your search
@@ -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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user