update to support 413 refusal

This commit is contained in:
LeonL committed 2026-11-06 11:42:58 -08:00
1 parent 5bb0d5e406
commit 5b1b2a7573
6 files changed
+290 -8

No files matched your search

+145 -4
View File
@@ -7,12 +7,16 @@
// If anything fails you can stay open or quit anyway; your work is always committed locally first.
//
// Menu: Tools > Git Auto Sync (on/off, ask-before-commit, and manual Pull / Commit and Push)
//
// If an HTTPS push is rejected as too large (HTTP 413, such as Cloudflare's 100 MB upload limit), the push is
// retried over SSH, which can be on a different host. Set it up in Project Settings > Git Auto Sync.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
@@ -98,6 +102,9 @@ namespace Lenrui.GitSync
[MenuItem(MenuRoot + "Commit and Push Now", false, 21)]
static void PushNow() { Guarded(() => CommitAndPush(false)); }
[MenuItem(MenuRoot + "Settings...", false, 40)]
static void OpenSettings() { SettingsService.OpenProjectSettings(GitAutoSyncSettingsProvider.SettingsPath); }
// ---------------------------------------------------------------- open: fetch + update
static void PullLatest(bool manual)
@@ -214,6 +221,7 @@ namespace Lenrui.GitSync
"It will be pushed the next time you close Unity.\n\n" + fetch.Tail, quitting);
GitResult push;
string refspec;
if (hasUpstream)
{
int ahead, behind;
@@ -233,17 +241,45 @@ namespace Lenrui.GitSync
if (ahead == 0) { Info("'" + branch + "' is already in sync.", !quitting); return true; }
string mergeRef = GitValue("config --get branch." + branch + ".merge");
push = Git("push " + remote + " HEAD:" + mergeRef, "Pushing " + ahead + " commit(s)...", 900);
refspec = "HEAD:" + mergeRef;
push = Git("push " + remote + " " + refspec, "Pushing " + ahead + " commit(s)...", 900);
}
else
{
refspec = "HEAD:refs/heads/" + branch;
push = Git("push -u " + remote + " HEAD", "Pushing new branch '" + branch + "'...", 900);
}
// Too big for the HTTPS front end (e.g. Cloudflare's 100 MB request limit): send the same push over SSH.
string pushedTo = remote;
if (!push.Ok && TooLargeForHttp(push))
{
var settings = GitAutoSyncSettings.Load();
if (!settings.sshFallback)
return Fail("Committed locally, but the server rejected the push as too large (HTTP 413). Turn on the SSH fallback in " +
"Project Settings > Git Auto Sync, or push it over SSH yourself.\n\n" + push.Tail, quitting);
string sshWhy;
string sshUrl = ResolveSshUrl(settings, RemoteUrl(remote), out sshWhy);
if (sshUrl == null)
return Fail("Committed locally, but the server rejected the push as too large (HTTP 413), and " + sshWhy, quitting);
Debug.Log("[GitAutoSync] HTTPS push was rejected as too large (HTTP 413); retrying over SSH: " + sshUrl);
push = Git("push " + Quote(sshUrl) + " " + refspec, "Too large for HTTPS, pushing over SSH...", 1800, batchSsh: true);
if (!push.Ok)
return Fail("Committed locally. The push was too large for HTTPS (HTTP 413), and pushing over SSH to " + sshUrl +
" failed too. " + SshHint(push, sshUrl) + "It will be retried the next time you close Unity.\n\n" + push.Tail, quitting);
// Pushing to a URL doesn't move origin/<branch>, so fetch to bring the remote-tracking branch up to date.
Git("fetch " + remote, "Updating " + remote + "...", 120);
if (!hasUpstream) Git("branch --set-upstream-to=" + remote + "/" + branch);
pushedTo = remote + " over SSH (" + sshUrl + ")";
}
if (!push.Ok)
return Fail("Committed locally, but the push failed. It will be retried the next time you close Unity.\n\n" + push.Tail, quitting);
Info("Pushed '" + branch + "' to " + remote + ".", !quitting);
Info("Pushed '" + branch + "' to " + pushedTo + ".", !quitting);
if (!quitting) AssetDatabase.Refresh();
return true;
}
@@ -282,6 +318,109 @@ namespace Lenrui.GitSync
static int StashCount() { return Lines(Git("stash list").Out).Length; }
// ---------------------------------------------------------------- SSH fallback
static bool TooLargeForHttp(GitResult r)
{
return Regex.IsMatch(r.Err + "\n" + r.Out, @"HTTP 413|error: 413|413 (Request Entity|Payload) Too Large", RegexOptions.IgnoreCase);
}
// The remote the current branch pushes to.
internal static string PushRemote()
{
string branch = CurrentBranch();
string remote = branch != null ? GitValue("config --get branch." + branch + ".remote") : null;
return remote ?? DefaultRemote();
}
internal static string RemoteUrl(string remote)
{
return remote == null ? null : GitValue("remote get-url --push " + remote);
}
// https://git.example.com/owner/repo.git -> ssh://git@<SSH host>[:port]/owner/repo.git
internal static string ResolveSshUrl(GitAutoSyncSettings s, string remoteUrl, out string why)
{
why = null;
string full = (s.sshUrlOverride ?? "").Trim();
if (full.Length > 0) return full;
if (string.IsNullOrEmpty(remoteUrl)) { why = "there's no git remote set up."; return null; }
Uri u;
if (!Uri.TryCreate(remoteUrl, UriKind.Absolute, out u) || (u.Scheme != Uri.UriSchemeHttps && u.Scheme != Uri.UriSchemeHttp))
{
why = "the remote (" + Redact(remoteUrl) + ") isn't an HTTPS address, so there's no SSH address to work out from it. " +
"Set a Full SSH URL in Project Settings > Git Auto Sync.";
return null;
}
string host = (s.sshHost ?? "").Trim();
string user = (s.sshUser ?? "").Trim();
if (host.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase)) host = host.Substring(6);
host = host.TrimEnd('/');
int at = host.LastIndexOf('@');
if (at >= 0) { user = host.Substring(0, at); host = host.Substring(at + 1); }
if (host.Length == 0) host = u.Host;
if (user.Length == 0) user = "git";
return "ssh://" + user + "@" + host + (s.sshPort > 0 && s.sshPort != 22 ? ":" + s.sshPort : "") + u.AbsolutePath;
}
// For the Test button in Project Settings.
internal static void TestSshConnection(GitAutoSyncSettings s)
{
Guarded(() =>
{
string why;
if (!CheckRepo(out why)) { Info(why, true); return; }
string url = ResolveSshUrl(s, RemoteUrl(PushRemote()), out why);
if (url == null) { Warn("Can't work out an SSH address: " + why, true); return; }
var r = Git("ls-remote " + Quote(url) + " HEAD", "Connecting to " + url + "...", 60, batchSsh: true);
if (r.Ok) Info("SSH works. Pushes too large for HTTPS will go to:\n\n" + url, true);
else Warn("Couldn't reach " + url + " over SSH. " + SshHint(r, url) + "\n\n" + r.Tail, true);
});
}
static string SshHint(GitResult r, string sshUrl)
{
string text = r.Err + "\n" + r.Out;
if (text.Contains("Host key verification failed"))
return "This computer doesn't trust the SSH server's host key yet: run `ssh -T " + SshTarget(sshUrl) +
"` once in a terminal and answer yes. ";
if (text.Contains("Permission denied"))
return "The SSH key was refused, or it needs a passphrase: add this computer's public key to your account on the git server " +
"and load the key into ssh-agent. ";
if (Regex.IsMatch(text, "Could not resolve hostname|Connection refused|timed out|No route to host"))
return "Couldn't connect to the SSH server; check the SSH host and port in Project Settings > Git Auto Sync. ";
if (Regex.IsMatch(text, "does not appear to be a git repository|not found|Cannot find repository", RegexOptions.IgnoreCase))
return "The SSH server has no repository at that path; check the SSH settings or set a Full SSH URL. ";
return "";
}
// "-p 2222 git@host" for an ssh:// URL, "git@host" for git@host:owner/repo.git
static string SshTarget(string sshUrl)
{
Uri u;
if (Uri.TryCreate(sshUrl, UriKind.Absolute, out u) && u.Scheme == "ssh")
return (u.Port > 0 && u.Port != 22 ? "-p " + u.Port + " " : "") + (u.UserInfo.Length > 0 ? u.UserInfo + "@" : "") + u.Host;
int colon = sshUrl.IndexOf(':');
return colon > 0 ? sshUrl.Substring(0, colon) : sshUrl;
}
static bool UserConfiguredSsh()
{
return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GIT_SSH_COMMAND")) ||
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GIT_SSH")) ||
GitValue("config --get core.sshCommand") != null;
}
static string Redact(string url) { return Regex.Replace(url ?? "", "//[^/@]+@", "//"); } // drop user:token@
static string Quote(string s) { return "\"" + s.Replace("\"", "\\\"") + "\""; }
static string GitValue(string args)
{
var r = Git(args);
@@ -289,7 +428,7 @@ namespace Lenrui.GitSync
return r.Ok && v.Length > 0 ? v : null;
}
struct GitResult
internal struct GitResult
{
public int Code;
public string Out, Err;
@@ -306,7 +445,7 @@ namespace Lenrui.GitSync
}
// Runs git in the project folder. With a progress title it shows a cancelable progress bar.
static GitResult Git(string args, string progress = null, int timeoutSeconds = 30)
static GitResult Git(string args, string progress = null, int timeoutSeconds = 30, bool batchSsh = false)
{
var psi = new ProcessStartInfo("git", args)
{
@@ -319,6 +458,8 @@ namespace Lenrui.GitSync
StandardErrorEncoding = Encoding.UTF8,
};
psi.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; // fail instead of waiting on an invisible password prompt
if (batchSsh && !UserConfiguredSsh())
psi.EnvironmentVariables["GIT_SSH_COMMAND"] = "ssh -o BatchMode=yes -o ConnectTimeout=20"; // same: no hidden SSH prompts
var output = new StringBuilder();
var errors = new StringBuilder();
@@ -0,0 +1,97 @@
// GitAutoSyncSettings.cs - part of the Git Auto Sync package (net.lenrui.git-auto-sync).
//
// Project Settings > Git Auto Sync. Saved to ProjectSettings/GitAutoSyncSettings.json, so it gets committed
// along with the project and every computer that works on it uses the same SSH fallback.
using System;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Lenrui.GitSync
{
[Serializable]
internal class GitAutoSyncSettings
{
public bool sshFallback = true;
public string sshHost = "";
public string sshUser = "git";
public int sshPort = 22;
public string sshUrlOverride = "";
static string FilePath
{
get { return Path.GetFullPath(Path.Combine(Application.dataPath, "../ProjectSettings/GitAutoSyncSettings.json")); }
}
public static GitAutoSyncSettings Load()
{
var s = new GitAutoSyncSettings();
try { if (File.Exists(FilePath)) JsonUtility.FromJsonOverwrite(File.ReadAllText(FilePath), s); }
catch (Exception e) { Debug.LogWarning("[GitAutoSync] Couldn't read " + FilePath + ": " + e.Message); }
return s;
}
public void Save()
{
try { File.WriteAllText(FilePath, JsonUtility.ToJson(this, true)); }
catch (Exception e) { Debug.LogWarning("[GitAutoSync] Couldn't save " + FilePath + ": " + e.Message); }
}
}
public static class GitAutoSyncSettingsProvider
{
public const string SettingsPath = "Project/Git Auto Sync";
static GitAutoSyncSettings settings;
static string remoteUrl;
static bool lookedUpRemote;
[SettingsProvider]
public static SettingsProvider Create()
{
return new SettingsProvider(SettingsPath, SettingsScope.Project)
{
label = "Git Auto Sync",
keywords = new[] { "git", "ssh", "push", "413", "cloudflare", "sync" },
guiHandler = search => Draw(),
deactivateHandler = () => { settings = null; lookedUpRemote = false; },
};
}
static void Draw()
{
if (settings == null) settings = GitAutoSyncSettings.Load();
if (!lookedUpRemote) { remoteUrl = GitAutoSync.RemoteUrl(GitAutoSync.PushRemote()); lookedUpRemote = true; }
var s = settings;
EditorGUIUtility.labelWidth = 200;
EditorGUILayout.LabelField("SSH fallback for large pushes", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("If the server rejects an HTTPS push as too large (HTTP 413, such as Cloudflare's 100 MB upload limit), " +
"the same push is retried over SSH. The SSH host can be different from the HTTPS one.", MessageType.Info);
EditorGUI.BeginChangeCheck();
s.sshFallback = EditorGUILayout.Toggle("Retry over SSH on HTTP 413", s.sshFallback);
using (new EditorGUI.DisabledScope(!s.sshFallback))
{
s.sshHost = EditorGUILayout.TextField(new GUIContent("SSH Host", "For example ssh.example.com. Leave blank to use the HTTPS remote's host."), s.sshHost);
s.sshUser = EditorGUILayout.TextField(new GUIContent("SSH User", "Usually git."), s.sshUser);
s.sshPort = Mathf.Clamp(EditorGUILayout.IntField("SSH Port", s.sshPort), 1, 65535);
s.sshUrlOverride = EditorGUILayout.TextField(new GUIContent("Full SSH URL (optional)",
"Use this exact address instead of working one out, for example ssh://[email protected]/owner/repo.git"), s.sshUrlOverride);
EditorGUILayout.Space();
string why;
string url = GitAutoSync.ResolveSshUrl(s, remoteUrl, out why);
EditorGUILayout.LabelField("Large pushes go to", url ?? "Nowhere: " + why, EditorStyles.wordWrappedLabel);
if (GUILayout.Button("Test SSH Connection", GUILayout.Width(180)))
{
var snapshot = s;
EditorApplication.delayCall += () => GitAutoSync.TestSshConnection(snapshot);
}
}
if (EditorGUI.EndChangeCheck()) s.Save();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec4b24c52eb94f88a28ae083fc626960
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: