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 (14699B)


      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     # Check that the directory where to publish the repository is not
    297     # the directory where the repository is located.
    298     _dir0="$(cd "${_repo_git}" && pwd -L)"
    299     _dir1="$(cd "${_repo}"     && pwd -L)"
    300     if [ "${_dir0}" = "${_dir1}" ]; then
    301       >&2 printf \
    302 'the directory "%s" where to publish the repository "%s" '\
    303 'is the directory in which the repository is located\n.' \
    304         "${dir_git}" "${_repo}"
    305       return 1
    306     fi
    307 
    308     # If the path to the published repository already exists,
    309     # verify that it matches the repository to publish...
    310     _dir0="$(cd "${_repo_git}" && pwd -P)"
    311     _dir1="$(cd "${_repo}"     && pwd -P)"
    312     if [ "${_dir0}" != "${_dir1}" ]; then
    313       >&2 printf \
    314         '"%s" already exists and is not the public version of "%s"\n' \
    315         "${_repo_git}" "${_repo}"
    316       return 1
    317     fi
    318 
    319     # ... and make sure that the source path points to a directory and
    320     # not a symbolic link. The "mv_repo" option is discussed below.
    321     if [ -L "${_repo}" ]; then
    322       rm "${_repo}"
    323       mv "${_repo_git}" "${_repo}"
    324     fi
    325 
    326     # Finally, delete the symbolic link in the public directory,
    327     # if it exists.
    328     rm -f "${_repo_git}"
    329   fi
    330 
    331   # Publish the git repository, i.e., create a symbolic link to it in
    332   # the publicly accessible directory, or move it to the public
    333   # directory and create a symbolic link from the original path to its
    334   # public version.
    335   if [ "${mv_repo}" -eq 0 ]; then
    336     ln -s "${_repo}" "${dir_git}"
    337   else
    338     mv "${_repo}" "${dir_git}"
    339     ln -s "${_repo_git}" "$(dirname "${_repo}")"
    340   fi
    341 
    342   # Create directory publicly served by the WWW daemon
    343   _repo_www="${dir_www}/${_repo_name}"
    344   [ "${force}" -ne 0 ] && rm -rf "${_repo_www}"
    345   mkdir -p "${_repo_www}"
    346 
    347   _umask="$(umask)"
    348   if [ -n "${group}" ]; then
    349     # Grant write access to the defined group if any.
    350     chown -R :"${group}" "${_repo_www}"
    351     chmod 2775 "${_repo_www}"
    352     umask "002"
    353   fi
    354 
    355   # Generate HTML pages for the repository to be published
    356   # Make sure the links are relative to the repository directory to
    357   # avoid problems on the web server when it chroots
    358   cd "${_repo_www}"
    359   stagit -c .cache -u "${base_url}/${_repo_name}/" "${_repo_git}"
    360   ln -sf './log.html' ./index.html
    361   ln -sf '../style.css' ./style.css
    362   ln -sf '../logo.png' ./logo.png
    363   ln -sf '../favicon.png' ./favicon.png
    364   cd "${OLDPWD}"
    365 
    366   # Restore default permissions
    367   umask "${_umask}"
    368 
    369   setup_post_receive_hook "${_repo}"
    370 
    371   printf 'done\n'
    372 }
    373 
    374 # Inputs:
    375 #   - @: repository list
    376 #   - base_url: base URL under which the git HTML repository is exposed
    377 #   - dir_git: directory where to publish the git repository
    378 #   - dir_www: directory where to publish the git repository's HTML pages
    379 #   - force: force generation of HTML pages from scratch
    380 #   - group: group to grant write access to
    381 publish() # list of repositories
    382 {
    383   printf '%s\n' "$@" | while read -r _i; do
    384     publish_repo "${_i}"
    385   done
    386 }
    387 
    388 # Inputs:
    389 #   - dir_git: directory where to publish the git repositories
    390 #   - dir_www: directory where to publish the git repositories' HTML pages
    391 #   - repo: git bare repository
    392 unpublish_repo()
    393 {
    394   # shellcheck disable=SC2310
    395   check_repo "$1" || return 1
    396 
    397   # Make the repository path absolute to ensure both the validity of
    398   # the symbolic link and a valid repository name
    399   _repo="$(cd -- "$1" && echo "${PWD}")"
    400   printf '%s: ' "${_repo}"
    401 
    402   _repo_name=$(basename "${_repo}" ".git")
    403   _repo_www="${dir_www}/${_repo_name}"
    404   _repo_git="${dir_git}/${_repo_name}.git"
    405 
    406   # shellcheck disable=SC2310
    407   check_repo "${_repo_git}" || return 1;
    408 
    409   # Make sure that both paths point to the same physical repository.
    410   # In other words, that one is a symbolic link to the other
    411   _dir0="$(cd "${_repo}"     && pwd -P)"
    412   _dir1="$(cd "${_repo_git}" && pwd -P)"
    413   if [ "${_dir0}" != "${_dir1}" ]; then
    414     >&2 printf 'not published\n'
    415     return 1
    416   fi
    417 
    418   # Remove publicly exposed directory
    419   if [ -L "${_repo_git}" ]; then
    420     rm "${_repo_git}"
    421   elif [ -L "${_repo}" ]; then # rev_symlink
    422     rm "${_repo}";
    423     mv "${_repo_git}" "$(dirname "${_repo}")"
    424   else
    425     >&2 printf 'Unexpected error\n' # This should not happen
    426     die 1
    427   fi
    428 
    429   rm -rf "${_repo_www}" # Remove HTML pages
    430 
    431   # shellcheck disable=SC2310
    432   if check_post_receive_hook "${_repo}"; then
    433     rm -f "${_repo}/hooks/post-receive"
    434   fi
    435 
    436   printf 'done\n'
    437 }
    438 
    439 # Inputs:
    440 #   - @ : repository list
    441 #   - base_url: base URL under which the git HTML repositories are exposed
    442 #   - dir_git: directory where to publish the git repositories
    443 #   - dir_www: directory where to publish the git repositories' HTML pages
    444 #   - force: force generation of HTML pages from scratch
    445 unpublish()
    446 {
    447   printf '%s\n' "$@" | while read -r _i; do
    448     unpublish_repo "${_i}"
    449   done
    450 }
    451 
    452 ########################################################################
    453 # The script
    454 ########################################################################
    455 # Parse input arguments
    456 OPTIND=1
    457 while getopts ":dfg:ms:u:w:" opt; do
    458   case "${opt}" in
    459     d) delete=1 ;;
    460     f) force=1 ;;
    461     g) dir_git="${OPTARG}" ;; # git directory
    462     m) mv_repo=1;; # Reverse symlink
    463     s) group="${OPTARG}" ;;
    464     u) base_url="${OPTARG}" ;;
    465     w) dir_www="${OPTARG}" ;; # WWW directory
    466     *) synopsis; die ;;
    467   esac
    468 done
    469 
    470 # Check mandatory options
    471 [ "${OPTIND}" -le $# ] || { synopsis; die; }
    472 
    473 if [ -z "${dir_git}" ]; then
    474   >&2 printf 'git directory is missing\n'
    475   die
    476 fi
    477 if [ -z "${dir_www}" ]; then
    478   >&2 printf 'WWW directory is missing\n'
    479   die
    480 fi
    481 if [ -z "${base_url}" ] && [ "${delete}" -eq 0 ]; then
    482   >&2 printf 'Base url is missing\n'
    483   die
    484 fi
    485 
    486 check_directory "${dir_git}"
    487 check_directory "${dir_www}"
    488 
    489 if [ "${delete}" -eq 0 ]; then
    490   check_resources "${dir_www}"
    491 fi
    492 
    493 # Skip parsed arguments
    494 shift $((OPTIND - 1))
    495 
    496 if [ "${delete}" -eq 0 ]; then
    497   publish "$@"
    498 else
    499   unpublish "$@"
    500 fi
    501 
    502 # [Re]generate index of publicly exposed repositories
    503 make_index
    504 
    505 die 0