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