< Back to all hacks

#11 Git Wrapper sed Rewrite

Runtime
Problem
npm passes SSH URLs in 3 formats: git+ssh://, ssh://, git@. Bash substitution catches only one.
Solution
Use sed in git wrapper to rewrite all 3 SSH URL patterns to HTTPS in a single pass.
Lesson
GitHub SSH URLs have three distinct formats in the wild. Any SSH-to-HTTPS rewriter must handle all three or npm installs will fail on whichever format a transitive dependency uses.

Context

npm packages specify git dependencies using SSH URLs for GitHub authentication. Inside the proot environment, there is no SSH agent, no SSH keys, and no way to authenticate to GitHub over SSH. All git operations must use HTTPS (for public repos) or HTTPS with a token (for private repos).

The initial git wrapper used a single bash string substitution: ${URL/git@github.com:/https://github.com/}. This handled only the git@github.com:user/repo SCP-style format. In practice, npm and its transitive dependencies use at least three distinct URL formats:

  • git+ssh://git@github.com/user/repo.git -- the most common format in package.json
  • ssh://git@github.com/user/repo.git -- used by some older packages
  • git@github.com:user/repo.git -- SCP-style, used by git submodules and some dependencies

A single sed command with multiple -e expressions handles all three in one pass. This is a LEGACY hack, but the URL rewriting logic was carried forward into the native migration's git configuration.

Implementation

The sed rewrite function, used inside the git wrapper (see hack 4 for the full argument parser):

#!/data/data/com.termux/files/usr/bin/bash
# Git wrapper — URL rewriting via sed
# Deployed to: $ROOTFS/usr/bin/git

REAL_GIT="/termux-prefix/bin/git"

rewrite_url() {
  echo "$1" | sed \
    -e 's|^git+ssh://git@github.com/|https://github.com/|' \
    -e 's|^git+ssh://git@github\.com:|https://github.com/|' \
    -e 's|^ssh://git@github.com/|https://github.com/|' \
    -e 's|^git@github\.com:|https://github.com/|'
}

The four sed expressions cover all observed patterns, including the rare variant where git+ssh:// uses a colon instead of a slash after the hostname.

For private repos, inject a GitHub personal access token into the URL:

rewrite_url_with_auth() {
  local url="$1"
  local token="${GITHUB_TOKEN:-}"
  local rewritten
  rewritten=$(echo "$url" | sed \
    -e 's|^git+ssh://git@github.com/|https://github.com/|' \
    -e 's|^git+ssh://git@github\.com:|https://github.com/|' \
    -e 's|^ssh://git@github.com/|https://github.com/|' \
    -e 's|^git@github\.com:|https://github.com/|')
  if [ -n "$token" ]; then
    rewritten=$(echo "$rewritten" | sed "s|https://github.com/|https://$token@github.com/|")
  fi
  echo "$rewritten"
}

Integrated into the full wrapper with argument parsing (hack 4):

#!/data/data/com.termux/files/usr/bin/bash
REAL_GIT="/termux-prefix/bin/git"

if [ "$1" = "clone" ]; then
  shift
  URL="" ; DEST="" ; FLAGS=()
  while [ $# -gt 0 ]; do
    case "$1" in
      --depth|--branch|-b|-c|--reference|--origin|-o)
        FLAGS+=("$1" "$2"); shift 2 ;;
      --mirror|--bare|--recursive|--no-checkout)
        FLAGS+=("$1"); shift ;;
      -*)  FLAGS+=("$1"); shift ;;
      *)   if [ -z "$URL" ]; then URL="$1"; elif [ -z "$DEST" ]; then DEST="$1"; fi; shift ;;
    esac
  done

  # Rewrite all SSH URL formats to HTTPS
  URL=$(echo "$URL" | sed \
    -e 's|^git+ssh://git@github.com/|https://github.com/|' \
    -e 's|^git+ssh://git@github\.com:|https://github.com/|' \
    -e 's|^ssh://git@github.com/|https://github.com/|' \
    -e 's|^git@github\.com:|https://github.com/|')

  if [ -n "$DEST" ]; then
    exec "$REAL_GIT" clone "${FLAGS[@]}" "$URL" "$DEST"
  else
    exec "$REAL_GIT" clone "${FLAGS[@]}" "$URL"
  fi
else
  exec "$REAL_GIT" "$@"
fi

Deploy into the proot rootfs:

ROOTFS="$PREFIX/var/lib/proot-distro/installed-rootfs/ubuntu"
# Write the script (use scp or cat > from Termux)
chmod +x "$ROOTFS/usr/bin/git"

Verification

Test each URL format directly:

# From inside proot, or test the function in isolation
rewrite_url "git+ssh://git@github.com/user/repo.git"
# Output: https://github.com/user/repo.git

rewrite_url "ssh://git@github.com/user/repo.git"
# Output: https://github.com/user/repo.git

rewrite_url "git@github.com:user/repo.git"
# Output: https://github.com/user/repo.git

# HTTPS URLs pass through unchanged
rewrite_url "https://github.com/user/repo.git"
# Output: https://github.com/user/repo.git

End-to-end verification with npm:

# Inside proot, install a package known to have git dependencies
npm install some-package-with-git-deps 2>&1 | grep -i "clone"
# All clone URLs in the output should show https://

Gotchas

  • The git+ssh:// format sometimes uses a colon instead of a slash after the hostname: git+ssh://git@github.com:user/repo. This is technically invalid per RFC but appears in real package.json files. The second sed expression (\.com:) handles this edge case.
  • sed on Termux uses the busybox or GNU variant depending on what is installed. Stick to basic regex and -e flags for each pattern. Extended regex (-E) works but is not needed here since all patterns are simple fixed prefixes.
  • The .git suffix may or may not be present on the original URL. The sed rewrite does not add or remove it. Suffix normalization is handled separately in the argument parser (hack 4).
  • If a URL points to a non-GitHub host (e.g., git@gitlab.com:), these sed rules do not match and the URL passes through unchanged. This is correct behavior since only GitHub URLs need rewriting in this setup.
  • Order of sed -e expressions does not matter here because the four patterns are mutually exclusive (different prefixes). Only one can match per URL.
  • The rewrite happens in a subshell ($(echo ... | sed ...)), which costs one fork. On proot where forks are expensive (ptrace overhead), this adds a few milliseconds per git clone. Acceptable since clones are infrequent.

Result

All three SSH URL formats are transparently rewritten to HTTPS. npm install succeeds for packages with git dependencies regardless of which URL format their maintainers chose. The rewriting is invisible to npm and to the git operations themselves. Combined with the argument parser (hack 4), this gives proot a fully functional git layer without any SSH infrastructure on the device.