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*.
|
- **Switching branch blocked by your changes:** it offers *Stash & switch*.
|
||||||
- **A network command needs a password, token, SSH passphrase or host-key confirmation:**
|
- **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.
|
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,
|
- **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
|
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**.
|
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))
|
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:
|
def hint_for(message: str) -> str:
|
||||||
"""Plain-English next step for common git errors."""
|
"""Plain-English next step for common git errors."""
|
||||||
if "Filename too long" in message:
|
if "Filename too long" in message:
|
||||||
@@ -1790,6 +1811,62 @@ class CloneDialog(ChooseFolderMixin, ModalScreen):
|
|||||||
self.dismiss(None)
|
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 = [
|
ACTIONS = [
|
||||||
("fetch", "Fetch"),
|
("fetch", "Fetch"),
|
||||||
("pull", "Pull"),
|
("pull", "Pull"),
|
||||||
@@ -1938,6 +2015,7 @@ ModalScreen { align: center middle; background: rgba(0, 0, 0, 0.55); }
|
|||||||
.hint { height: auto; color: #959da5; }
|
.hint { height: auto; color: #959da5; }
|
||||||
.error { height: auto; margin-top: 1; }
|
.error { height: auto; margin-top: 1; }
|
||||||
#nr-readme { margin-top: 1; }
|
#nr-readme { margin-top: 1; }
|
||||||
|
#ssh-always { margin-top: 1; }
|
||||||
.path-row { height: auto; }
|
.path-row { height: auto; }
|
||||||
.path-row Input { width: 1fr; }
|
.path-row Input { width: 1fr; }
|
||||||
.path-row Button { margin-left: 1; min-width: 12; }
|
.path-row Button { margin-left: 1; min-width: 12; }
|
||||||
@@ -2804,13 +2882,19 @@ class GitDesk(App):
|
|||||||
buttons = [("retry", "Try again", "primary"), ("close", "Close", "default")]
|
buttons = [("retry", "Try again", "primary"), ("close", "Close", "default")]
|
||||||
return await self.ask("The repository is busy", body, buttons, cancel="close")
|
return await self.ask("The repository is busy", body, buttons, cancel="close")
|
||||||
|
|
||||||
async def net(self, args: List[str], what: str) -> bool:
|
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."""
|
"""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:
|
while True:
|
||||||
async with self._net_lock:
|
async with self._net_lock:
|
||||||
r = await self.bg(*args, serialize=False)
|
r = await self.bg(*args, serialize=False)
|
||||||
if r.ok:
|
if r.ok:
|
||||||
return True
|
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:])
|
tail = "\n".join(r.message.splitlines()[-10:])
|
||||||
body = Text.assemble((tail, "#f97583"), "\n\n",
|
body = Text.assemble((tail, "#f97583"), "\n\n",
|
||||||
("If git needs a password or token, an SSH passphrase, or wants you to "
|
("If git needs a password or token, an SSH passphrase, or wants you to "
|
||||||
@@ -2824,6 +2908,41 @@ class GitDesk(App):
|
|||||||
if choice != "retry":
|
if choice != "retry":
|
||||||
return False
|
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:
|
def run_in_terminal(self, args: List[str], cwd: Optional[str] = None) -> bool:
|
||||||
rc = 1
|
rc = 1
|
||||||
try:
|
try:
|
||||||
@@ -3054,7 +3173,8 @@ class GitDesk(App):
|
|||||||
return
|
return
|
||||||
remote = self.remote_name()
|
remote = self.remote_name()
|
||||||
if not st.has_upstream:
|
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}.")
|
self.notify(f"Published '{st.branch}' to {remote}.")
|
||||||
return
|
return
|
||||||
if not await self.do_fetch():
|
if not await self.do_fetch():
|
||||||
@@ -3077,7 +3197,8 @@ class GitDesk(App):
|
|||||||
return
|
return
|
||||||
target = st.upstream[len(remote) + 1:]
|
target = st.upstream[len(remote) + 1:]
|
||||||
n = st.ahead
|
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}.")
|
self.notify(f"Pushed {plural(n, 'commit')} to {remote}.")
|
||||||
|
|
||||||
async def commit_flow(self) -> None:
|
async def commit_flow(self) -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user