git-repo

Tools for sharing git bare repositories
git clone git://git.meso-star.fr/git-repo.git
Log | Files | Refs | README | LICENSE

git-publish (10642B)


      1 #!/bin/sh
      2 
      3 # Copyright (C) 2024, 2025 |Méso|Star> (contact@meso-star.com)
      4 #
      5 # This program is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as published by
      7 # the Free Software Foundation, either version 3 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public License
     16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
     17 
     18 set -e
     19 
     20 # Use string concatenation to check whether the RESOURCES_PATH
     21 # meta-variable has been substituted. If not, it is assumed that the
     22 # script is not installed and can therefore access resources locally.
     23 # If not, this certainly means that the script has been installed, and
     24 # therefore its resources too. RESOURCES_PATH should therefore define
     25 # the installation path for the script's resources.
     26 # shellcheck disable=SC2050
     27 if [ "@RESOURCES_PATH@" = '@'"RESOURCES_PATH"'@' ]; then
     28   GIT_PUBLISH_RESOURCES_PATH="."
     29 else
     30   GIT_PUBLISH_RESOURCES_PATH="@RESOURCES_PATH@"
     31 fi
     32 
     33 ########################################################################
     34 # Helper functions
     35 ########################################################################
     36 die()
     37 {
     38   exit "${1:-1}" # return status code (default is 1)
     39 }
     40 
     41 synopsis()
     42 {
     43   >&2 printf \
     44 'usage: %s [-df] [-g dir_git] [-u base_url] [-w dir_www] repository ...\n' \
     45     "${0##*/}"
     46 }
     47 
     48 check_resources() # path
     49 {
     50   # Copy resources using the cat command rather than cp to ensure
     51   # that the file is created in accordance with the umask settings.
     52   #
     53   # The cp command creates destination files with the original
     54   # permissions, to which the umask permissions are ultimately added.
     55   # The umask therefore only has an impact if it is more restrictive
     56   # than that of the original file.
     57   #
     58   # However, the umask should take precedence here, as the destination
     59   # directory may require write access for the group so that its members
     60   # can update its contents.  These access rights are managed through
     61   # the umask, which cp would ignore.
     62 
     63   if [ ! -e "$1/favicon.png" ]; then
     64     # Make the favicon from the logo
     65     cat "${GIT_PUBLISH_RESOURCES_PATH}/logo.png" > "$1/favicon.png"
     66   fi
     67 
     68   if [ ! -e "$1/logo.png" ]; then
     69     cat "${GIT_PUBLISH_RESOURCES_PATH}/logo.png" > "$1/logo.png"
     70   fi
     71 
     72   if [ ! -e "$1style.css" ]; then
     73     cat "${GIT_PUBLISH_RESOURCES_PATH}/style.css" > "$1/style.css"
     74   fi
     75 }
     76 
     77 check_directory() # path
     78 {
     79   if [ -z "$1" ] || ! cd "$1" 2> /dev/null; then
     80     >&2 printf '%s: not a directory\n' "$1"
     81     return 1
     82   fi
     83 
     84   cd "${OLDPWD}"
     85 }
     86 
     87 # Inputs:
     88 #   - repo: git bare repository
     89 check_repo()
     90 {
     91   cd "${repo}"
     92 
     93   if ! is_bare_repo="$(git rev-parse --is-bare-repository 2> /dev/null)" \
     94   || [ "${is_bare_repo}" = "false" ]; then
     95     >&2 printf 'not a git bare repository\n'
     96     return 1
     97   fi
     98 
     99   cd "${OLDPWD}"
    100 }
    101 
    102 # Inputs:
    103 #   - repo: git bare repository (absolute path)
    104 publication_ban()
    105 {
    106   cd -- "${repo}"
    107 
    108   # Retrieve the directory where git files are stored
    109   if ! git_dir=$(git rev-parse --path-format=absolute --git-dir 2>&1)
    110   then
    111     >&2 printf '%s: %s\n' "${repo}" "${git_dir}"
    112     die
    113   fi
    114 
    115   cd -- "${OLDPWD}"
    116 
    117   # Check if the repository contains the file prohibiting publication
    118   if [ -e "${git_dir}/publication_ban" ]; then
    119     return 0
    120   else
    121     return 1
    122   fi
    123 }
    124 
    125 # Inputs:
    126 #   - base_url: base URL under which the git HTML repository is exposed
    127 #   - dir_git: directory where to publish the git repository
    128 #   - dir_www: directory where to publish the git repository's HTML pages
    129 #   - repo: git bare repository
    130 #   - force: force generation of HTML pages from scratch
    131 publish_repo()
    132 {
    133   repo_name=$(basename "${repo}" ".git")
    134 
    135   # Publish the git repository, i.e. create a symbolic link to it in the
    136   # publicly exposed directory
    137   ln -sf "${repo}" "${dir_git}"
    138   repo_git="${dir_git}/${repo_name}.git"
    139 
    140   # Create directory publicly served by the WWW daemon
    141   repo_www="${dir_www}/${repo_name}"
    142   [ "${force}" -ne 0 ] && rm -rf "${repo_www}"
    143   mkdir -p "${repo_www}"
    144 
    145   # Generate HTML pages for the repository to be published
    146   # Make sure the links are relative to the repository directory to
    147   # avoid problems on the web server when it chroots
    148   cd "${repo_www}"
    149   stagit -c .cache -u "${base_url}/${repo_name}/" "${repo_git}"
    150   ln -sf './log.html' ./index.html
    151   ln -sf '../style.css' ./style.css
    152   ln -sf '../logo.png' ./logo.png
    153   ln -sf '../favicon.png' ./favicon.png
    154   cd "${OLDPWD}"
    155 }
    156 
    157 # Returns 0 if the repository has no receive hook or if it is the one
    158 # configured by git-publish, and >0 otherwise.
    159 # Inputs:
    160 #   - 1: repository
    161 check_post_receive_hook()
    162 {
    163   hook="${GIT_PUBLISH_RESOURCES_PATH}/post-receive.in"
    164 
    165   # Setup the hook header: it identifies the hook
    166   digest="$(cksum "${hook}" | cut -d' ' -f1)"
    167   header='# git-publish '"${digest}"
    168 
    169   if [ -e "$1/hooks/post-receive" ]; then
    170     header2="$(sed -n '2p' "$1/hooks/post-receive")"
    171     if [ "${header}" != "${header2}" ]; then
    172       return 1
    173     fi
    174   fi
    175 }
    176 
    177 # Inputs:
    178 #   - base_url: base URL under which the git HTML repository is exposed
    179 #   - dir_git: directory where to publish the git repository
    180 #   - dir_www: directory where to publish the git repository's HTML pages
    181 #   - repo: git bare repository
    182 setup_post_receive_hook()
    183 {
    184   # shellcheck disable=SC2310
    185   if ! check_post_receive_hook "${repo}"; then
    186     # Don't overwrite the repository's already configured post-receive
    187     # hook if it hasn't been configured by git-publish.
    188     >&2 printf 'another post-receive hook already exist\n'
    189     return 1
    190   fi
    191 
    192     sed "2i ${header}" "${hook}" \
    193   | sed -e "s#@DIR_GIT@#${dir_git}#g" \
    194         -e "s#@DIR_WWW@#${dir_www}#g" \
    195         -e "s#@BASE_URL@#${base_url}#g" \
    196         > "${repo}/hooks/post-receive"
    197 
    198   chmod 755 "${repo}/hooks/post-receive"
    199 }
    200 
    201 # Create an index from the list of directories in 'dir_www' that
    202 # correspond to the list of bare repositories in 'dir_git'.
    203 #
    204 # Inputs:
    205 #   - dir_git: directory where to publish the git repository
    206 #   - dir_www: directory where to publish the git repository's HTML pages
    207 make_index()
    208 {
    209   tmpfile="${TMPDIR:-/tmp}/git-publish-index.txt"
    210 
    211   # Removes trailing slashes. This allows you to write the following
    212   # regular expressions for find directives
    213   dir=$(dirname  "${dir_www}")
    214   www=$(basename "${dir_www}")
    215   dir_www="${dir}/${www}"
    216   dir=$(dirname  "${dir_git}")
    217   git=$(basename "${dir_git}")
    218   dir_git="${dir}/${git}"
    219 
    220   # Build list of candidate git repositories from the directories of the
    221   # publicly exposed WWW directory
    222   find "${dir_www}" -type d -path "${dir_www}/*" -prune \
    223     -exec sh -c "
    224         printf '%s\n' \"\$@\" \
    225       | sed 's;${dir_www}/\(.\{1,\}\)$;${dir_git}/\1.git;' \
    226       | sort" \
    227     -- {} + > "${tmpfile}"
    228 
    229   # Compare the candidate list to the list of publicly exposed git
    230   # repositories. The intersection corresponds to the repositories to
    231   # exposed in the HTML index
    232   repo_list=$(find "${dir_git}" -path "${dir_git}/*.git" -prune | sort \
    233     | join - "${tmpfile}" | tr '\n' ' ')
    234 
    235   if [ -z "${repo_list}" ]; then
    236     # No repo to index. Delete index file if any
    237     rm -f "${dir_www}/index.html"
    238   else
    239     # Generate the index
    240     # shellcheck disable=SC2086
    241     stagit-index ${repo_list} > "${dir_www}/index.html"
    242   fi
    243 
    244   rm -f "${tmpfile}"
    245 }
    246 
    247 # Inputs:
    248 #   - @: repository list
    249 #   - base_url: base URL under which the git HTML repository is exposed
    250 #   - dir_git: directory where to publish the git repository
    251 #   - dir_www: directory where to publish the git repository's HTML pages
    252 #   - force: force generation of HTML pages from scratch
    253 publish() # list of repositories
    254 {
    255   printf '%s\n' "$@" | while read -r repo; do
    256     # Make the repository path absolute to ensure both the validity of
    257     # the symbolic link and a valid repository name
    258     repo="$(cd -- "${repo}" && echo "${PWD}")"
    259 
    260     printf '%s: ' "${repo}"
    261 
    262     # Isn't the repository prohibited from publication?
    263     # shellcheck disable=SC2310
    264     if publication_ban "${repo}"; then
    265       printf 'ban\n'
    266 
    267     else
    268       check_repo
    269       publish_repo
    270       setup_post_receive_hook
    271       printf 'done\n'
    272     fi
    273   done
    274 }
    275 
    276 # Inputs:
    277 #   - @ : repository list
    278 #   - base_url: base URL under which the git HTML repositories are exposed
    279 #   - dir_git: directory where to publish the git repositories
    280 #   - dir_www: directory where to publish the git repositories' HTML pages
    281 #   - force: force generation of HTML pages from scratch
    282 unpublish()
    283 {
    284   printf '%s\n' "$@" | while read -r repo; do
    285     # Make the repository path absolute to ensure both the validity of
    286     # the symbolic link and a valid repository name
    287     repo="$(cd -- "${repo}" && echo "${PWD}")"
    288     printf '%s: ' "${repo}"
    289 
    290     check_repo
    291 
    292     repo_name=$(basename "${repo}" ".git")
    293     repo_www="${dir_www}/${repo_name}"
    294     repo_git="${dir_git}/${repo_name}.git"
    295 
    296     rm -rf "${repo_www}" # Remove HTML pages
    297     rm -f "${repo_git}" # Remove link in the publicly exposed directory
    298 
    299     # shellcheck disable=SC2310
    300     if check_post_receive_hook "${repo}"; then
    301       rm -f "${repo}/hooks/post-receive"
    302     fi
    303 
    304     printf 'done\n'
    305   done
    306 }
    307 
    308 ########################################################################
    309 # The script
    310 ########################################################################
    311 base_url="${GIT_PUBLISH_BASE_URL:-}"
    312 dir_git="${GIT_PUBLISH_DIR_GIT:-/srv/git}"
    313 dir_www="${GIT_PUBLISH_DIR_WWW:-/srv/www/git}"
    314 force=0 # Force HTML generation
    315 delete=0 # Delete publication
    316 
    317 # Parse input arguments
    318 OPTIND=1
    319 while getopts ":dfg:u:w:" opt; do
    320   case "${opt}" in
    321     d) delete=1 ;;
    322     f) force=1 ;;
    323     u) base_url="${OPTARG}" ;;
    324     g) dir_git="${OPTARG}" ;; # git directory
    325     w) dir_www="${OPTARG}" ;; # WWW directory
    326     *) synopsis; die ;;
    327   esac
    328 done
    329 
    330 # Check mandatory options
    331 [ "${OPTIND}" -le $# ] || { synopsis; die; }
    332 
    333 if [ -z "${dir_git}" ]; then
    334   >&2 printf 'git directory is missing\n'
    335   die
    336 fi
    337 if [ -z "${dir_www}" ]; then
    338   >&2 printf 'WWW directory is missing\n'
    339   die
    340 fi
    341 if [ -z "${base_url}" ] && [ "${delete}" -eq 0 ]; then
    342   >&2 printf 'Base url is missing\n'
    343   die
    344 fi
    345 
    346 check_directory "${dir_git}"
    347 check_directory "${dir_www}"
    348 
    349 if [ "${delete}" -eq 0 ]; then
    350   check_resources "${dir_www}"
    351 fi
    352 
    353 # Skip parsed arguments
    354 shift $((OPTIND - 1))
    355 
    356 if [ "${delete}" -eq 0 ]; then
    357   publish "$@"
    358 else
    359   unpublish "$@"
    360 fi
    361 
    362 # [Re]generate index of publicly exposed repositories
    363 make_index
    364 
    365 die 0