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


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