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