git_infra

Git infra scripts for git.bracken.jp
git clone https://git.bracken.jp/git_infra.git
Log | Files | Refs | LICENSE

grm (10400B)


      1 #!/bin/sh
      2 # grm: git repo manager for self-hosted git servers
      3 
      4 # This is a customised version of grm.
      5 #
      6 # The original version can be obtained from:
      7 # https://sink.krj.st/grm
      8 #
      9 # Copyright 2020 by krasjet.
     10 # License: https://sink.krj.st/grm/file/LICENSE
     11 
     12 
     13 #---------------+----------+-----------------#
     14 #               |  config  |                 #
     15 #               +----------+                 #
     16 
     17 # root directory of git repositories.
     18 GRM_REPOS_ROOT="/home/git"
     19 
     20 # default owner.
     21 GRM_OWNER="Chris Bracken"
     22 
     23 # default url prefix (without ending slash)
     24 GRM_URL_PREFIX="https://git.bracken.jp"
     25 
     26 # Add Mastodon verification URL to index header.
     27 ME_URL="https://famichiki.jp/@akande"
     28 
     29 # path of the post-receive hooks for gout.
     30 GRM_POSTRECV_HOOK="/home/git/git_infra/post-receive"
     31 GRM_POSTRECV_HOOKS_DIR="/home/git/git_infra/post-receive.d"
     32 
     33 # root directory of output gout web pages.
     34 GOUT_WEB_ROOT="/var/www/git.bracken.jp"
     35 
     36 # root directory of output gout gemini pages.
     37 #
     38 # agate serves this as a virtual host, so it must be content/<hostname>/ and
     39 # agate must be started with more than one --hostname; see gout_gemini.
     40 GOUT_GEMINI_ROOT="/var/gemini/content/git.bracken.jp"
     41 
     42 #                                            #
     43 #                                            #
     44 #--------------------------------------------#
     45 
     46 # for gout
     47 export LC_CTYPE="en_US.UTF-8"
     48 
     49 prog_name="${0##*/}"
     50 repos_root="$GRM_REPOS_ROOT"
     51 web_root="$GOUT_WEB_ROOT"
     52 gemini_root="$GOUT_GEMINI_ROOT"
     53 
     54 # agate reads the gemini pages as a different user in a different jail.
     55 umask 022
     56 
     57 recompile_repo() {
     58   repo_name="$1"
     59   repo_dir="${repos_root}/${repo_name}.git"
     60   repo_web_dir="${web_root}/${repo_name}"
     61   repo_gmi_dir="${gemini_root}/${repo_name}"
     62   # One cache per output format: the cache holds log entries already rendered
     63   # in that format, so html and gemini must not share one.
     64   cachefile="${repo_dir}/.htmlcache"
     65   gmicachefile="${repo_dir}/.gmicache"
     66 
     67   [ -d "$repo_dir" ] || { echo "[$1] repo not found"; return 1; }
     68 
     69   if [ -e "$repo_dir/git-daemon-export-ok" ]; then
     70     echo "[$1] recompiling gout html pages..."
     71     mkdir -p "$repo_web_dir"
     72     cd "${repo_web_dir:?}"                  && \
     73     rm -f "$cachefile"                      && \
     74     rm -rf "commit" "file"                  && \
     75     gout -H -c "$cachefile" "$repo_dir"     && \
     76     ln -sf log.html index.html              && \
     77     ln -sf ../logo.png logo.png             && \
     78     ln -sf ../style.css style.css           && \
     79     ln -sf ../favicon.png favicon.png       || return 1
     80 
     81     echo "[$1] recompiling gout gemini pages..."
     82     mkdir -p "$repo_gmi_dir"
     83     cd "${repo_gmi_dir:?}"                  && \
     84     rm -f "$gmicachefile"                   && \
     85     rm -rf "commit" "file"                  && \
     86     gout -M -c "$gmicachefile" "$repo_dir"  && \
     87     ln -sf log.gmi index.gmi                || return 1
     88 
     89     echo "[$1] done!"
     90   else
     91     echo "[$1] Not recompiling gout pages: private repo"
     92   fi
     93 }
     94 
     95 # Lists the repos that belong in the index, one shell-quoted path per line.
     96 #
     97 # 1. find all directories in $repos_root ending with .git
     98 # 2. filter all the public repos (with git-daemon-export-ok)
     99 # 3. exclude any repo marked with gout-no-index
    100 # 4. sort the result
    101 # 5. hack for posix compatibility
    102 indexable_repos() {
    103   find "${repos_root}/." ! -name . -prune     \
    104     -type d -name "*.git"                     \
    105     -exec test -e "{}/git-daemon-export-ok" \;\
    106     -exec test ! -e "{}/gout-no-index" \;   \
    107     -print                                    \
    108     | sort -f                                 \
    109     | sed -e 's/"/"\\""/g' -e 's/.*/"&"/'
    110 }
    111 
    112 rebuild_index() {
    113   echo "[index] rebuilding html index..."
    114   mkdir -p "${web_root}" || return 1;
    115   indexable_repos                             \
    116     | xargs gout_index -H -m "${ME_URL}"    \
    117     > "${web_root}/index.html" || return 1
    118 
    119   # gout_index -M emits relative links (=> repo/log.gmi) and takes no -m: the
    120   # Mastodon verification link is an HTML-only concept.
    121   echo "[index] rebuilding gemini index..."
    122   mkdir -p "${gemini_root}" || return 1;
    123   indexable_repos                             \
    124     | xargs gout_index -M                     \
    125     > "${gemini_root}/index.gmi" || return 1
    126 
    127   echo "[index] done!"
    128 }
    129 
    130 RED="\033[91m"
    131 GREEN="\033[92m"
    132 YELLOW="\033[93m"
    133 BLUE="\033[94m"
    134 RESET="\033[0m"
    135 
    136 grm_new() {
    137   set -e
    138   default_owner=${GRM_OWNER:-$(logname)}
    139   url_prefix=${GRM_URL_PREFIX:-git://$(hostname -f)}
    140   default_desc="a work in progress"
    141 
    142   printf "%b\n> " "${BLUE}repo name (without trailing .git)${RESET}"
    143   read -r repo_name
    144   [ -z "$repo_name" ] && \
    145     { echo "no repo name given, exiting..."; exit 1; }
    146 
    147   # now we have the complete path of the repo
    148   repo_path="$repos_root/${repo_name}.git"
    149   [ -e "$repo_path" ] && \
    150     { echo "repository already exists"; exit 1; }
    151 
    152   printf "%b\n> " "${YELLOW}visibility:
    153   1) public\n  2) private\n  3) unlisted (hide from index)
    154 ${BLUE}enter index [default: ${GREEN}1${BLUE}]${RESET}"
    155   read -r visibility
    156 
    157   case $visibility in
    158      1|public) exported=1 ;;
    159      2|private) hidden=1 ;;
    160      3|unlisted) exported=1; hidden=1 ;;
    161      *) printf "%b\n" "${YELLOW}visibility defaults to ${GREEN}public${RESET}"
    162         exported=1 ;;
    163   esac
    164 
    165   printf "%b%s%b\n> " "${BLUE}description [${GREEN}" "$default_desc" "${BLUE}]${RESET}"
    166   read -r repo_desc
    167   repo_desc=${repo_desc:-$default_desc}
    168 
    169   printf "%b%s%b\n> " "${BLUE}owner [${GREEN}" "$default_owner" "${BLUE}]${RESET}"
    170   read -r owner
    171   owner=${owner:-$default_owner}
    172 
    173   printf "%b%s%b\n> " \
    174     "${BLUE}clone url [${GREEN}" "$url_prefix/$repo_name.git" "${BLUE}]${RESET}"
    175   read -r clone_url
    176   clone_url=${clone_url:-$url_prefix/$repo_name}.git
    177 
    178   # start creating repo
    179   git init --bare "$repo_path"
    180 
    181   echo "writing gout metadata..."
    182   printf "%s\n" "$repo_desc" > "$repo_path/description"
    183   printf "%s\n" "$owner" > "$repo_path/owner"
    184   printf "%s\n" "$clone_url" > "$repo_path/url"
    185 
    186   echo "setting visibility..."
    187   [ "$exported" = "1" ] && : >> "$repo_path/git-daemon-export-ok"
    188   [ "$hidden" = "1" ] && : >> "$repo_path/gout-no-index"
    189 
    190   # Link the page-generation hooks ONLY. post-receive.d also holds
    191   # publish_www and publish_gemini, which check out a work tree over the blog
    192   # and the capsule; a new repo linked to those would overwrite them on its
    193   # first push. Never replace this with a loop over the directory.
    194   echo "installing gout post-receive hooks..."
    195   ln -sf "$GRM_POSTRECV_HOOK" "$repo_path/hooks/post-receive"
    196   mkdir -p "$repo_path/hooks/post-receive.d"
    197   ln -sf "$GRM_POSTRECV_HOOKS_DIR/gout_html" \
    198          "$repo_path/hooks/post-receive.d/gout_html"
    199   ln -sf "$GRM_POSTRECV_HOOKS_DIR/gout_gemini" \
    200          "$repo_path/hooks/post-receive.d/gout_gemini"
    201 
    202   echo "done!"
    203 }
    204 
    205 grm_remove() {
    206   [ $# -gt 0 ] || { echo "no repo name given, exiting..."; exit 1; }
    207 
    208   for repo in "$@"
    209   do
    210     printf "remove %s? [y/N] " "$repo"
    211     read -r resp
    212     if echo "$resp" | grep -iq "^y$"; then
    213       rm -rf "${repos_root:?}/${repo:?}.git" || continue;
    214       rm -rf "${web_root:?}/${repo:?}" || continue;
    215       rm -rf "${gemini_root:?}/${repo:?}" || continue;
    216     fi
    217   done
    218   # only rebuild index if gout exists
    219   command -v gout_index >/dev/null && rebuild_index &
    220   wait
    221 }
    222 
    223 grm_list() {
    224   case "$1" in
    225     public)
    226       find "${repos_root}/." ! -name . -prune        \
    227         -type d -name "*.git"                        \
    228         -exec test -e "{}/git-daemon-export-ok" \;   \
    229         -exec test ! -e "{}/gout-no-index" \;      \
    230         -exec basename {} '.git' \; | sort -f ;;
    231     private)
    232       find "${repos_root}/." ! -name . -prune        \
    233         -type d -name "*.git"                        \
    234         -exec test ! -e "{}/git-daemon-export-ok" \; \
    235         -exec basename {} '.git' \; | sort -f ;;
    236     hidden|unlisted)
    237       find "${repos_root}/." ! -name . -prune        \
    238         -type d -name "*.git"                        \
    239         -exec test -e "{}/git-daemon-export-ok" \;   \
    240         -exec test -e "{}/gout-no-index" \;        \
    241         -exec basename {} '.git' \; | sort -f ;;
    242     *)
    243       find "${repos_root}/." ! -name . -prune \
    244         -type d -name "*.git"                 \
    245         -exec basename {} '.git' \; | sort -f ;;
    246   esac
    247 }
    248 
    249 grm_recompile() {
    250   for repo_name in "$@"
    251   do
    252     recompile_repo "$repo_name" &
    253   done
    254   rebuild_index &
    255   wait
    256   echo "recompilation done!"
    257 }
    258 
    259 grm_recompileall() {
    260   grm_list \
    261     | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' \
    262     | xargs "$0" rc
    263 }
    264 
    265 grm_info() {
    266   [ -z "$1" ] && { echo "no repo name given, exiting..."; exit 1; }
    267 
    268   repo_name=$1
    269   repo_dir="${repos_root}/${repo_name}.git"
    270 
    271   [ -d "$repo_dir" ] || { echo "can't find repo named $repo_name"; exit 1; }
    272   echo "name: $repo_name"
    273   printf "visibility: "
    274 
    275   if [ -e "${repo_dir}/git-daemon-export-ok" ]; then
    276     if [ -e "${repo_dir}/gout-no-index" ]; then
    277       printf "%b\n" "${YELLOW}unlisted${RESET}"
    278     else
    279       printf "%b\n" "${GREEN}public${RESET}"
    280     fi
    281   else
    282     printf "%b\n" "${RED}private${RESET}"
    283   fi
    284 
    285   [ -f "${repo_dir}/description" ] && \
    286     echo "description: $(cat "${repo_dir}/description")"
    287 
    288   [ -f "${repo_dir}/owner" ] && \
    289     echo "owner: $(cat "${repo_dir}/owner")"
    290 
    291   [ -f "${repo_dir}/url" ] && \
    292     echo "url: $(cat "${repo_dir}/url")"
    293 }
    294 
    295 show_help() {
    296   cat << EOF
    297 usage: $prog_name <command> [<args>]
    298 
    299 Git repo manager, manage git repositories on self-hosted git servers.
    300 
    301 If you have created a 'git' user for managing git repositories, this
    302 script should be run as:
    303     $ doas -u git -- $prog_name <command> [<args>]
    304 or
    305     $ sudo -u git -- $prog_name <command> [<args>]
    306 
    307 commands:
    308     new                  create a new repo
    309     info repo_name       display metadata of the repo
    310     ls                   list all repos
    311     ls public            list public repos
    312     ls private           list private repos
    313     ls unlisted          list unlisted (hidden) repos
    314     rm repo1 [repo2..]   remove repos
    315     rc                   recompile gout index (html and gemini)
    316     rc repo1 [repo2..]   recompile gout pages for repos,
    317                          and recompile index
    318     rca                  recompile all public repos
    319     help                 show help
    320 EOF
    321 }
    322 
    323 # parse subcommands
    324 case "$1" in
    325   new) cmd=new;;
    326   ls|list) cmd=list;;
    327   rm|remove) cmd=remove;;
    328   rc|recompile) cmd=recompile;;
    329   rca|recompileall) cmd=recompileall;;
    330   info) cmd=info;;
    331   *) { show_help; exit; };;
    332 esac
    333 
    334 shift
    335 grm_"$cmd" "$@"