While strong arguments can be made for creating resources with permanent paths, making links to their location forever reachable, it is unavoidable and sometimes desired to reorganize and move things around. This post is about git repositories in particular.
For moved repositories accessed over http(s), there's 301 Moved Permanently (or Wikipedia), but when accessing using ssh no similar functionality exist on a protocol level. In at least my reality that means redirects work for things where I only have read access, but not for remotes which I also can push commits to. It might be theoretically possible to setup write operations over https, but ssh is much more common.
As already stated, there simply is no way to get a machine readable redirect for git over ssh. The approach here will write a banner to stderr, which ought to be noticable enough for a human git user and hopefully good enough for many cases.
The actual example code will focus on moves to some other directory on the same server, but the same method could be applied to redirect to some completely different server.
When git performs operations through an ssh server, it actually runs a bunch of
commands such as git-upload-pack
. That is where one can output
a redirection message on stderr advising about the new location.
Start by creating /etc/ssh/sshd_config.d/ssh_redir.conf
containing something
like:
Match User git
ForceCommand /opt/netizense/libexec/wrapper
Next, the referenced wrapper
could look like:
#!/bin/sh
case "$SSH_ORIGINAL_COMMAND" in
'git-upload-pack '*)
_our_dir="$( dirname "$0" )"
PATH="$_our_dir:$PATH"
export PATH
# As git-shell ignores $PATH, sh is used here.
exec /bin/sh -c "$SSH_ORIGINAL_COMMAND"
;;
'')
exec git-shell
;;
*)
exec git-shell -c "$SSH_ORIGINAL_COMMAND"
;;
esac
In plain language, the script checks whether a specific command is run. If that
is the case, it determines its own directory and adds that as the first of
$PATH
's entries, which enables finding our own version of
git-upload-pack when executing that command. All other commands are executed
with git-shell as usual. The special handling of the no arguments case is to
for interactive shells.
Note that passing arguments to /bin/sh in this manner makes arbitrary command execution possible. I trust all my git users with shell access, so its not a problem on for me. Others might have different policies, thus requiring some adaption to quote shell characters or patching the sources of the actual git-upload-pack executable and recompiling it.
Here follows the script I use for git-upload-pack
: (must be in the same
directory as wrapper
for that $our_dir construct to work)
#!/bin/sh -eu
_git_dir="$( eval echo "\$$#" )"
_real_dir="$( realpath "$_git_dir" )"
[ "$_real_dir" = "$_git_dir" ] || printf '%s\n%s\n\n%s\n\n%s\n%s\n' \
'******************************************************************' \
"Please note that $_git_dir has moved to:" \
" $_real_dir" \
'Update your repository remote!' \
'******************************************************************' \
>&2
unset _git_dir
exec git-shell -c "$SSH_ORIGINAL_COMMAND"
That script starts with getting the last argument (\$$#
) which is directory
the git repository is in. It then checks whether it is (within) a symlink or
not. In case it is, the warning is printed on stderr. It then launches the
original command.
One can of course also add logging or what not to the wrapper script, but I don't personally see a need for that. If doing so, one might want to look at wrapping more (or all) git commands.
In order for these changes to actually bite, sshd needs to be restarted.