VC Shuttle: Advice-Based Git Sync for Air-Gapped Emacs
I work on an air-gapped VM, which is to say, a virtual machine with no network access at all. You copy code in via USB or a shared folder, develop, and then you need to get that code back out to the host machine. Git is running inside the VM, of course, it's the sensible thing to do for version control, but git push wants a remote, and there is no remote. What you actually need is a file copy operation that shuttles the repo data to a shared folder, where the host can pick it up.
So I have a pair of bash scripts, unimaginatively called in and out. out copies the source tree to the shared folder so the host sees my commits, and in pulls changes back from the host into the VM. Works fine from the terminal, but I live in Emacs, and I want to trigger this from within Emacs rather than jumping to a shell.
The obvious answer is to bind a key to async-shell-command and call it done. But then I remembered, Emacs already has this infrastructure - the VC mode's push command. When you press P in a vc-dir buffer, or invoke vc-push from the mode-line popup, Emacs runs vc-git-push which calls git push. I don't want that, I want my out script. The pull side is the same problem. So the question becomes: can I make Emacs's VC commands do something else?
Yes, obviously, this is Emacs. advice-add lets you replace any function, and vc-git-push is just a function. The :override strategy means my function runs entirely in place of the original, which is what I want - there's no useful fallback to git push on this machine, it would just fail.
These two functions live in my coding.el starter snippet, here's the push side:
(defun my/vc-git-push-shuttle (_file-list &rest _args)
"Override vc-push to run a local sync script instead of git push."
(interactive)
(let ((script-path "/home/jdyer/bin/out"))
(if (file-executable-p script-path)
(progn
(message "Syncing source TO shared folder...")
(async-shell-command script-path "*out*"))
(error "Sync script not found or not executable at %s"
script-path))))
The _file-list and _rest _args soak up whatever VC passes to the push function, I don't care about the arguments since my script doesn't need them. The function checks the script is executable, shows a message, and runs it asynchronously into the *out* buffer so I can inspect the output if something goes wrong. Pull is the same thing pointing at /home/jdyer/bin/in into *in*, so:
(defun my/vc-git-pull-shuttle (_file-list &rest _args)
"Override vc-pull to run a local sync script instead of git pull."
(interactive)
(let ((script-path "/home/jdyer/bin/in"))
(if (file-executable-p script-path)
(progn
(message "Syncing source FROM shared folder...")
(async-shell-command script-path "*in*"))
(error "Sync script not found or not executable at %s"
script-path))))
Then the advice:
(advice-add 'vc-git-push :override #'my/vc-git-push-shuttle)
(advice-add 'vc-git-pull :override #'my/vc-git-pull-shuttle)
And that's it. Now every Emacs command that would have called git push or git pull runs my shuttle scripts instead. When I press P in vc-dir the code ends up in the shared folder ready for the host to collect. No context switch to a terminal.
The same pattern would work for any situation where you want VC's push/pull to do something domain-specific - copying to a local archive, rsyncing to a staging server, or in my case, bridging an air gap.