git-barepo

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

git-publish (14256B)


      1 #!/bin/sh
      2 
      3 # Copyright (C) 2024-2026 |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 trim_trailing_slash() # path
     34 {
     35   _dir=$(dirname "$1")
     36   _file=$(basename "$1")
     37   printf '%s/%s\n' "${_dir}" "${_file}"
     38 }
     39 
     40 base_url="${GIT_PUBLISH_BASE_URL:-}"
     41 dir_git="$(trim_trailing_slash "${GIT_PUBLISH_DIR_GIT:-/srv/git}")"
     42 dir_www="$(trim_trailing_slash "${GIT_PUBLISH_DIR_WWW:-/srv/www/git}")"
     43 
     44 force=0 # Force HTML generation
     45 delete=0 # Delete publication
     46 group="" # Group with which write access is shared
     47 
     48 # Move the repository to the publication directory instead of creating a
     49 # link to it there. The original repository then becomes a symbolic link
     50 # to the published repository.
     51 mv_repo=0
     52 
     53 hook="${GIT_PUBLISH_RESOURCES_PATH}/post-receive.in"
     54 
     55 # Setup the hook header: it identifies the hook
     56 digest="$(cksum "${hook}" | cut -d' ' -f1)"
     57 header='# git-publish '"${digest}"
     58 
     59 ########################################################################
     60 # Helper functions
     61 ########################################################################
     62 die()
     63 {
     64   exit "${1:-1}" # return status code (default is 1)
     65 }
     66 
     67 synopsis()
     68 {
     69   >&2 printf \
     70 'usage: %s [-dfm] [-g dir_git] [-s group] [-u base_url] [-w dir_www]\n'\
     71 '                   repository ...\n' "${0##*/}"
     72 }
     73 
     74 check_resources() # path, group
     75 {
     76   # Copy resources using the cat command rather than cp to ensure
     77   # that the file is created in accordance with the umask settings.
     78   #
     79   # The cp command creates destination files with the original
     80   # permissions, to which the umask permissions are ultimately added.
     81   # The umask therefore only has an impact if it is more restrictive
     82   # than that of the original file.
     83   #
     84   # However, the umask should take precedence here, as the destination
     85   # directory may require write access for the group so that its members
     86   # can update its contents.  These access rights are managed through
     87   # the umask, which cp would ignore.
     88 
     89   if [ ! -e "$1/favicon.png" ]; then
     90     # Make the favicon from the logo
     91     cat "${GIT_PUBLISH_RESOURCES_PATH}/logo.png" > "$1/favicon.png"
     92   fi
     93 
     94   if [ ! -e "$1/logo.png" ]; then
     95     cat "${GIT_PUBLISH_RESOURCES_PATH}/logo.png" > "$1/logo.png"
     96   fi
     97 
     98   if [ ! -e "$1style.css" ]; then
     99     cat "${GIT_PUBLISH_RESOURCES_PATH}/style.css" > "$1/style.css"
    100   fi
    101 }
    102 
    103 check_directory() # path
    104 {
    105   if [ -z "$1" ] || ! cd "$1" 2> /dev/null; then
    106     >&2 printf '%s: not a directory\n' "$1"
    107     return 1
    108   fi
    109 
    110   cd "${OLDPWD}"
    111 }
    112 
    113 check_repo() # repo
    114 {
    115   _err=0
    116 
    117   if [ ! -e "$1" ] || [ -f "$1" ]; then
    118     _err=1
    119   else
    120     cd "$1"
    121 
    122     if ! _is_bare_repo="$(git rev-parse --is-bare-repository 2> /dev/null)" \
    123     || [ "${_is_bare_repo}" = "false" ]; then
    124       _err=1;
    125     fi
    126 
    127     cd "${OLDPWD}"
    128   fi
    129 
    130   if [ "${_err}" -ne 0 ]; then
    131     >&2 printf '%s: not a git bare repository\n' "$1"
    132     return 1
    133   fi
    134 }
    135 
    136 publication_ban() # repo
    137 {
    138   cd -- "$1"
    139 
    140   # Retrieve the directory where git files are stored
    141   if ! git_dir=$(git rev-parse --path-format=absolute --git-dir 2>&1)
    142   then
    143     >&2 printf '%s: %s\n' "$1" "${git_dir}"
    144     die
    145   fi
    146 
    147   cd -- "${OLDPWD}"
    148 
    149   # Check if the repository contains the file prohibiting publication
    150   if [ -e "${git_dir}/publication_ban" ]; then
    151     return 0
    152   else
    153     return 1
    154   fi
    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 #   - header: hook magic cookie
    162 #   - hook: path to the hook template
    163 check_post_receive_hook()
    164 {
    165   if [ -e "$1/hooks/post-receive" ]; then
    166     _header2="$(sed -n '2p' "$1/hooks/post-receive")"
    167     if [ "${header}" != "${_header2}" ]; then
    168       return 1
    169     fi
    170   fi
    171 }
    172 
    173 # Inputs:
    174 #   - 1: git bare repository
    175 #   - base_url: base URL under which the git HTML repository is exposed
    176 #   - dir_git: directory where to publish the git repository
    177 #   - dir_www: directory where to publish the git repository's HTML pages
    178 #   - group: group to grant write access to
    179 #   - header: hook magic cookie
    180 #   - hook: path to the hook template
    181 setup_post_receive_hook()
    182 {
    183   # shellcheck disable=SC2310
    184   if ! check_post_receive_hook "$1"; then
    185     # Don't overwrite the repository's already configured post-receive
    186     # hook if it hasn't been configured by git-publish.
    187     >&2 printf 'another post-receive hook already exist\n'
    188     return 1
    189   fi
    190 
    191   # Grant write access to the defined group if any.
    192   if [ -n "${group}" ]; then
    193     _umask="002"
    194   else
    195     _umask="$(umask)"
    196   fi
    197 
    198     sed "2i ${header}" "${hook}" \
    199   | sed -e "s#@DIR_GIT@#${dir_git}#g" \
    200         -e "s#@DIR_WWW@#${dir_www}#g" \
    201         -e "s#@BASE_URL@#${base_url}#g" \
    202 	-e "s#@UMASK@#${_umask}#g" \
    203         > "$1/hooks/post-receive"
    204 
    205   chmod 755 "$1/hooks/post-receive"
    206 }
    207 
    208 # Create an index from the list of directories in 'dir_www' that
    209 # correspond to the list of bare repositories in 'dir_git'.
    210 #
    211 # Inputs:
    212 #   - dir_git: directory where to publish the git repository
    213 #   - dir_www: directory where to publish the git repository's HTML pages
    214 #   - group: group to grant write access to
    215 make_index()
    216 {
    217   _tmpfile="${TMPDIR:-/tmp}/git-publish-index.txt"
    218 
    219   # Removes trailing slashes. This allows you to write the following
    220   # regular expressions for find directives
    221   _dir=$(dirname  "${dir_www}")
    222   _www=$(basename "${dir_www}")
    223   dir_www="${_dir}/${_www}"
    224   _dir=$(dirname  "${dir_git}")
    225   _git=$(basename "${dir_git}")
    226   dir_git="${_dir}/${_git}"
    227 
    228   # Build list of candidate git repositories from the directories of the
    229   # publicly exposed WWW directory
    230   find "${dir_www}" -type d -path "${dir_www}/*" -prune \
    231     -exec sh -c "
    232         printf '%s\n' \"\$@\" \
    233       | sed 's;${dir_www}/\(.\{1,\}\)$;${dir_git}/\1.git;' \
    234       | sort" \
    235     -- {} + > "${_tmpfile}"
    236 
    237   # Compare the candidate list to the list of publicly exposed git
    238   # repositories. The intersection corresponds to the repositories to
    239   # exposed in the HTML index
    240   _repo_list=$(find "${dir_git}" -path "${dir_git}/*.git" -prune | sort \
    241     | join - "${_tmpfile}" | tr '\n' ' ')
    242 
    243   rm -f "${dir_www}/index.html"
    244 
    245   if [ -n "${_repo_list}" ]; then
    246     # Generate the index
    247     # shellcheck disable=SC2086
    248     stagit-index ${_repo_list} > "${dir_www}/index.html"
    249 
    250     if [ -n "${group}" ]; then
    251       # Grant write access to the specified group, if it exists. This
    252       # allows the index to be updated automatically by the
    253       # "post-receive" hook whenever a user in that group updates the
    254       # repository.
    255       chown :"${group}" "${dir_www}/index.html"
    256       chmod 664 "${dir_www}/index.html"
    257     fi
    258   fi
    259 
    260   rm -f "${_tmpfile}"
    261 }
    262 
    263 # Inputs:
    264 #   - 1: git bare repository
    265 #   - base_url: base URL under which the git HTML repository is exposed
    266 #   - dir_git: directory where to publish the git repository
    267 #   - dir_www: directory where to publish the git repository's HTML pages
    268 #   - force: force generation of HTML pages from scratch
    269 #   - group: group to grant write access to
    270 #   - mv_repo: reverse the symbolic link
    271 publish_repo()
    272 {
    273   # shellcheck disable=SC2310
    274   check_repo "$1" || return 1
    275 
    276   # Make the repository path absolute to ensure both the validity of
    277   # the symbolic link and a valid repository name
    278   _repo="$(cd -- "$1" && echo "${PWD}")"
    279 
    280   # Isn't the repository prohibited from publication?
    281   # shellcheck disable=SC2310
    282   if publication_ban "${_repo}"; then
    283     printf '%s: BAN\n' "${_repo}"
    284     return 0
    285   fi
    286 
    287   printf '%s: ' "${_repo}"
    288 
    289   _repo_name=$(basename "${_repo}" ".git")
    290   _repo_git="${dir_git}/${_repo_name}.git"
    291 
    292   if [ -e "${_repo_git}" ]; then
    293     # shellcheck disable=SC2310
    294     check_repo "${_repo_git}" || return 1
    295 
    296     # If the path to the published repository already exists,
    297     # verify that it matches the repository to publish...
    298     _dir0="$(cd "${_repo_git}" && pwd -P)"
    299     _dir1="$(cd "${_repo}"     && pwd -P)"
    300     if [ "${_dir0}" != "${_dir1}" ]; then
    301       >&2 printf \
    302         '"%s" already exists and is not the public version of "%s"\n' \
    303         "${_repo_git}" "${_repo}"
    304       return 1
    305     fi
    306 
    307     # ... and make sure that the source path points to a directory and
    308     # not a symbolic link. The "mv_repo" option is discussed below.
    309     if [ -L "${_repo}" ]; then
    310       rm "${_repo}"
    311       mv "${_repo_git}" "${_repo}"
    312     fi
    313 
    314     # Finally, delete the symbolic link in the public directory,
    315     # if it exists.
    316     rm -f "${_repo_git}"
    317   fi
    318 
    319   # Publish the git repository, i.e., create a symbolic link to it in
    320   # the publicly accessible directory, or move it to the public
    321   # directory and create a symbolic link from the original path to its
    322   # public version.
    323   if [ "${mv_repo}" -eq 0 ]; then
    324     ln -s "${_repo}" "${dir_git}"
    325   else
    326     mv "${_repo}" "${dir_git}"
    327     ln -s "${_repo_git}" "$(dirname "${_repo}")"
    328   fi
    329 
    330   # Create directory publicly served by the WWW daemon
    331   _repo_www="${dir_www}/${_repo_name}"
    332   [ "${force}" -ne 0 ] && rm -rf "${_repo_www}"
    333   mkdir -p "${_repo_www}"
    334 
    335   _umask="$(umask)"
    336   if [ -n "${group}" ]; then
    337     # Grant write access to the defined group if any.
    338     chown -R :"${group}" "${_repo_www}"
    339     chmod 2775 "${_repo_www}"
    340     umask "002"
    341   fi
    342 
    343   # Generate HTML pages for the repository to be published
    344   # Make sure the links are relative to the repository directory to
    345   # avoid problems on the web server when it chroots
    346   cd "${_repo_www}"
    347   stagit -c .cache -u "${base_url}/${_repo_name}/" "${_repo_git}"
    348   ln -sf './log.html' ./index.html
    349   ln -sf '../style.css' ./style.css
    350   ln -sf '../logo.png' ./logo.png
    351   ln -sf '../favicon.png' ./favicon.png
    352   cd "${OLDPWD}"
    353 
    354   # Restore default permissions
    355   umask "${_umask}"
    356 
    357   setup_post_receive_hook "${_repo}"
    358 
    359   printf 'done\n'
    360 }
    361 
    362 # Inputs:
    363 #   - @: repository list
    364 #   - base_url: base URL under which the git HTML repository is exposed
    365 #   - dir_git: directory where to publish the git repository
    366 #   - dir_www: directory where to publish the git repository's HTML pages
    367 #   - force: force generation of HTML pages from scratch
    368 #   - group: group to grant write access to
    369 publish() # list of repositories
    370 {
    371   printf '%s\n' "$@" | while read -r _i; do
    372     publish_repo "${_i}"
    373   done
    374 }
    375 
    376 # Inputs:
    377 #   - dir_git: directory where to publish the git repositories
    378 #   - dir_www: directory where to publish the git repositories' HTML pages
    379 #   - repo: git bare repository
    380 unpublish_repo()
    381 {
    382   # shellcheck disable=SC2310
    383   check_repo "$1" || return 1
    384 
    385   # Make the repository path absolute to ensure both the validity of
    386   # the symbolic link and a valid repository name
    387   _repo="$(cd -- "$1" && echo "${PWD}")"
    388   printf '%s: ' "${_repo}"
    389 
    390   _repo_name=$(basename "${_repo}" ".git")
    391   _repo_www="${dir_www}/${_repo_name}"
    392   _repo_git="${dir_git}/${_repo_name}.git"
    393 
    394   # shellcheck disable=SC2310
    395   check_repo "${_repo_git}" || return 1;
    396 
    397   # Make sure that both paths point to the same physical repository.
    398   # In other words, that one is a symbolic link to the other
    399   _dir0="$(cd "${_repo}"     && pwd -P)"
    400   _dir1="$(cd "${_repo_git}" && pwd -P)"
    401   if [ "${_dir0}" != "${_dir1}" ]; then
    402     >&2 printf 'not published\n'
    403     return 1
    404   fi
    405 
    406   # Remove publicly exposed directory
    407   if [ -L "${_repo_git}" ]; then
    408     rm "${_repo_git}"
    409   elif [ -L "${_repo}" ]; then # rev_symlink
    410     rm "${_repo}";
    411     mv "${_repo_git}" "$(dirname "${_repo}")"
    412   else
    413     >&2 printf 'Unexpected error\n' # This should not happen
    414     die 1
    415   fi
    416 
    417   rm -rf "${_repo_www}" # Remove HTML pages
    418 
    419   # shellcheck disable=SC2310
    420   if check_post_receive_hook "${_repo}"; then
    421     rm -f "${_repo}/hooks/post-receive"
    422   fi
    423 
    424   printf 'done\n'
    425 }
    426 
    427 # Inputs:
    428 #   - @ : repository list
    429 #   - base_url: base URL under which the git HTML repositories are exposed
    430 #   - dir_git: directory where to publish the git repositories
    431 #   - dir_www: directory where to publish the git repositories' HTML pages
    432 #   - force: force generation of HTML pages from scratch
    433 unpublish()
    434 {
    435   printf '%s\n' "$@" | while read -r _i; do
    436     unpublish_repo "${_i}"
    437   done
    438 }
    439 
    440 ########################################################################
    441 # The script
    442 ########################################################################
    443 # Parse input arguments
    444 OPTIND=1
    445 while getopts ":dfg:ms:u:w:" opt; do
    446   case "${opt}" in
    447     d) delete=1 ;;
    448     f) force=1 ;;
    449     g) dir_git="${OPTARG}" ;; # git directory
    450     m) mv_repo=1;; # Reverse symlink
    451     s) group="${OPTARG}" ;;
    452     u) base_url="${OPTARG}" ;;
    453     w) dir_www="${OPTARG}" ;; # WWW directory
    454     *) synopsis; die ;;
    455   esac
    456 done
    457 
    458 # Check mandatory options
    459 [ "${OPTIND}" -le $# ] || { synopsis; die; }
    460 
    461 if [ -z "${dir_git}" ]; then
    462   >&2 printf 'git directory is missing\n'
    463   die
    464 fi
    465 if [ -z "${dir_www}" ]; then
    466   >&2 printf 'WWW directory is missing\n'
    467   die
    468 fi
    469 if [ -z "${base_url}" ] && [ "${delete}" -eq 0 ]; then
    470   >&2 printf 'Base url is missing\n'
    471   die
    472 fi
    473 
    474 check_directory "${dir_git}"
    475 check_directory "${dir_www}"
    476 
    477 if [ "${delete}" -eq 0 ]; then
    478   check_resources "${dir_www}"
    479 fi
    480 
    481 # Skip parsed arguments
    482 shift $((OPTIND - 1))
    483 
    484 if [ "${delete}" -eq 0 ]; then
    485   publish "$@"
    486 else
    487   unpublish "$@"
    488 fi
    489 
    490 # [Re]generate index of publicly exposed repositories
    491 make_index
    492 
    493 die 0