sketch: "git push" button
Ultimately, we want to allow users to push their changes to github, and
thereby do a good chunk of work without resorting to the terminal (and
figuring out how to move the git references around, which requires a
bunch of esotiric and annoying expertise).
This commit introduces:
1. For outtie's HTTP server (which is now comically Go HTTP ->
CGI-effing-bin -> git -> shell script -> git in this case), there's a
custom git hook that forwards changes to refs/remotes/origin/foo to
origin/foo. This is a git proxy of sorts. By forwarding the
SSH_AUTH_SOCK, we can use outtie's auth options without giving innie
the actual credentials. This works by creating a temporary directory
for git hooks (for outtie).
2. Innie sets up a new remote, "upstream" when a "passthrough-upstream"
flag is pasksed. This remote kind of looks like the real upstream (so
upstream/foo) is fetched. This will let the agent handle rebases
better.
3. Innie exposes a /pushinfo handler that returns the list of remotes
and the current commit and such. These have nice display names for
the outtie's machine and github if useful.
There's also a /push handler. This is the thing that knows about the
refs/remotes/origin/foo thing. There's no magic git push refspec that
makes this all work without that, I think. (Maybe there is? I don't
think there is.)
Note that there's been some changes about what the remotes look like,
and when we use the remotes and when we use agent.GitOrigin().
We may be able to simplify this by using git's insteadof
configurations, but I think it's fine.
4. The web UI exposes a button to push, choose the remote and branch,
and such. If it can't do the push, you'll get a button to try to get
the agent to rebase.
We don't allow force pushes in the UI. We're treating those
as an advanced feature, and, if you need to do that, you can
figure it out.
This was collaboration with a gazillion sketch sessions.
diff --git a/dockerimg/pre-receive.sh b/dockerimg/pre-receive.sh
new file mode 100644
index 0000000..dfabb64
--- /dev/null
+++ b/dockerimg/pre-receive.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+# Pre-receive hook for sketch git http server
+# Handles refs/remotes/origin/Y pushes by forwarding them to origin/Y
+
+set -e
+
+# Timeout function for commands (macOS compatible)
+# Usage: run_with_timeout <timeout_seconds> <command> [args...]
+#
+# This is here because OX X doesn't ship /usr/bin/timeout by default!?!?
+run_with_timeout() {
+ local timeout=$1
+ shift
+
+ # Run command in background and capture PID
+ "$@" &
+ local cmd_pid=$!
+
+ # Start timeout killer in background
+ (
+ sleep "$timeout"
+ if kill -0 "$cmd_pid" 2>/dev/null; then
+ echo "Command timed out after ${timeout}s, killing process" >&2
+ kill -TERM "$cmd_pid" 2>/dev/null || true
+ sleep 2
+ kill -KILL "$cmd_pid" 2>/dev/null || true
+ fi
+ ) &
+ local killer_pid=$!
+
+ # Wait for command to complete
+ local exit_code=0
+ if wait "$cmd_pid" 2>/dev/null; then
+ exit_code=$?
+ else
+ exit_code=124 # timeout exit code
+ fi
+
+ # Clean up timeout killer
+ kill "$killer_pid" 2>/dev/null || true
+ wait "$killer_pid" 2>/dev/null || true
+
+ return $exit_code
+}
+
+# Read stdin for ref updates
+while read oldrev newrev refname; do
+ # Check if this is a push to refs/remotes/origin/Y pattern
+ if [[ "$refname" =~ ^refs/remotes/origin/(.+)$ ]]; then
+ branch_name="${BASH_REMATCH[1]}"
+
+ # Check if this is a force push by seeing if oldrev is not ancestor of newrev
+ if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
+ # Check if this is a fast-forward (oldrev is ancestor of newrev)
+ if ! git merge-base --is-ancestor "$oldrev" "$newrev" 2>/dev/null; then
+ echo "Error: Force push detected to refs/remotes/origin/$branch_name" >&2
+ echo "Force pushes are not allowed" >&2
+ exit 1
+ fi
+ fi
+
+ echo "Detected push to refs/remotes/origin/$branch_name" >&2
+
+ # Verify HTTP_USER_AGENT is set to sketch-intentional-push for forwarding
+ if [ "$HTTP_USER_AGENT" != "sketch-intentional-push" ]; then
+ echo "Error: Unauthorized push to refs/remotes/origin/$branch_name" >&2
+ exit 1
+ fi
+
+ echo "Authorization verified, forwarding to origin" >&2
+
+ # Push to origin using the new commit with 10 second timeout
+ # There's an innocous "ref updates forbidden inside quarantine environment" warning that we can ignore.
+ if ! run_with_timeout 10 git push origin "$newrev:refs/heads/$branch_name"; then
+ echo "Error: Failed to push $newrev to origin/$branch_name (may have timed out)" >&2
+ exit 1
+ fi
+
+ echo "Successfully pushed to origin/$branch_name" >&2
+ fi
+done
+
+exit 0