git-publish (10642B)
1 #!/bin/sh 2 3 # Copyright (C) 2024, 2025 |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 ######################################################################## 34 # Helper functions 35 ######################################################################## 36 die() 37 { 38 exit "${1:-1}" # return status code (default is 1) 39 } 40 41 synopsis() 42 { 43 >&2 printf \ 44 'usage: %s [-df] [-g dir_git] [-u base_url] [-w dir_www] repository ...\n' \ 45 "${0##*/}" 46 } 47 48 check_resources() # path 49 { 50 # Copy resources using the cat command rather than cp to ensure 51 # that the file is created in accordance with the umask settings. 52 # 53 # The cp command creates destination files with the original 54 # permissions, to which the umask permissions are ultimately added. 55 # The umask therefore only has an impact if it is more restrictive 56 # than that of the original file. 57 # 58 # However, the umask should take precedence here, as the destination 59 # directory may require write access for the group so that its members 60 # can update its contents. These access rights are managed through 61 # the umask, which cp would ignore. 62 63 if [ ! -e "$1/favicon.png" ]; then 64 # Make the favicon from the logo 65 cat "${GIT_PUBLISH_RESOURCES_PATH}/logo.png" > "$1/favicon.png" 66 fi 67 68 if [ ! -e "$1/logo.png" ]; then 69 cat "${GIT_PUBLISH_RESOURCES_PATH}/logo.png" > "$1/logo.png" 70 fi 71 72 if [ ! -e "$1style.css" ]; then 73 cat "${GIT_PUBLISH_RESOURCES_PATH}/style.css" > "$1/style.css" 74 fi 75 } 76 77 check_directory() # path 78 { 79 if [ -z "$1" ] || ! cd "$1" 2> /dev/null; then 80 >&2 printf '%s: not a directory\n' "$1" 81 return 1 82 fi 83 84 cd "${OLDPWD}" 85 } 86 87 # Inputs: 88 # - repo: git bare repository 89 check_repo() 90 { 91 cd "${repo}" 92 93 if ! is_bare_repo="$(git rev-parse --is-bare-repository 2> /dev/null)" \ 94 || [ "${is_bare_repo}" = "false" ]; then 95 >&2 printf 'not a git bare repository\n' 96 return 1 97 fi 98 99 cd "${OLDPWD}" 100 } 101 102 # Inputs: 103 # - repo: git bare repository (absolute path) 104 publication_ban() 105 { 106 cd -- "${repo}" 107 108 # Retrieve the directory where git files are stored 109 if ! git_dir=$(git rev-parse --path-format=absolute --git-dir 2>&1) 110 then 111 >&2 printf '%s: %s\n' "${repo}" "${git_dir}" 112 die 113 fi 114 115 cd -- "${OLDPWD}" 116 117 # Check if the repository contains the file prohibiting publication 118 if [ -e "${git_dir}/publication_ban" ]; then 119 return 0 120 else 121 return 1 122 fi 123 } 124 125 # Inputs: 126 # - base_url: base URL under which the git HTML repository is exposed 127 # - dir_git: directory where to publish the git repository 128 # - dir_www: directory where to publish the git repository's HTML pages 129 # - repo: git bare repository 130 # - force: force generation of HTML pages from scratch 131 publish_repo() 132 { 133 repo_name=$(basename "${repo}" ".git") 134 135 # Publish the git repository, i.e. create a symbolic link to it in the 136 # publicly exposed directory 137 ln -sf "${repo}" "${dir_git}" 138 repo_git="${dir_git}/${repo_name}.git" 139 140 # Create directory publicly served by the WWW daemon 141 repo_www="${dir_www}/${repo_name}" 142 [ "${force}" -ne 0 ] && rm -rf "${repo_www}" 143 mkdir -p "${repo_www}" 144 145 # Generate HTML pages for the repository to be published 146 # Make sure the links are relative to the repository directory to 147 # avoid problems on the web server when it chroots 148 cd "${repo_www}" 149 stagit -c .cache -u "${base_url}/${repo_name}/" "${repo_git}" 150 ln -sf './log.html' ./index.html 151 ln -sf '../style.css' ./style.css 152 ln -sf '../logo.png' ./logo.png 153 ln -sf '../favicon.png' ./favicon.png 154 cd "${OLDPWD}" 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 check_post_receive_hook() 162 { 163 hook="${GIT_PUBLISH_RESOURCES_PATH}/post-receive.in" 164 165 # Setup the hook header: it identifies the hook 166 digest="$(cksum "${hook}" | cut -d' ' -f1)" 167 header='# git-publish '"${digest}" 168 169 if [ -e "$1/hooks/post-receive" ]; then 170 header2="$(sed -n '2p' "$1/hooks/post-receive")" 171 if [ "${header}" != "${header2}" ]; then 172 return 1 173 fi 174 fi 175 } 176 177 # Inputs: 178 # - base_url: base URL under which the git HTML repository is exposed 179 # - dir_git: directory where to publish the git repository 180 # - dir_www: directory where to publish the git repository's HTML pages 181 # - repo: git bare repository 182 setup_post_receive_hook() 183 { 184 # shellcheck disable=SC2310 185 if ! check_post_receive_hook "${repo}"; then 186 # Don't overwrite the repository's already configured post-receive 187 # hook if it hasn't been configured by git-publish. 188 >&2 printf 'another post-receive hook already exist\n' 189 return 1 190 fi 191 192 sed "2i ${header}" "${hook}" \ 193 | sed -e "s#@DIR_GIT@#${dir_git}#g" \ 194 -e "s#@DIR_WWW@#${dir_www}#g" \ 195 -e "s#@BASE_URL@#${base_url}#g" \ 196 > "${repo}/hooks/post-receive" 197 198 chmod 755 "${repo}/hooks/post-receive" 199 } 200 201 # Create an index from the list of directories in 'dir_www' that 202 # correspond to the list of bare repositories in 'dir_git'. 203 # 204 # Inputs: 205 # - dir_git: directory where to publish the git repository 206 # - dir_www: directory where to publish the git repository's HTML pages 207 make_index() 208 { 209 tmpfile="${TMPDIR:-/tmp}/git-publish-index.txt" 210 211 # Removes trailing slashes. This allows you to write the following 212 # regular expressions for find directives 213 dir=$(dirname "${dir_www}") 214 www=$(basename "${dir_www}") 215 dir_www="${dir}/${www}" 216 dir=$(dirname "${dir_git}") 217 git=$(basename "${dir_git}") 218 dir_git="${dir}/${git}" 219 220 # Build list of candidate git repositories from the directories of the 221 # publicly exposed WWW directory 222 find "${dir_www}" -type d -path "${dir_www}/*" -prune \ 223 -exec sh -c " 224 printf '%s\n' \"\$@\" \ 225 | sed 's;${dir_www}/\(.\{1,\}\)$;${dir_git}/\1.git;' \ 226 | sort" \ 227 -- {} + > "${tmpfile}" 228 229 # Compare the candidate list to the list of publicly exposed git 230 # repositories. The intersection corresponds to the repositories to 231 # exposed in the HTML index 232 repo_list=$(find "${dir_git}" -path "${dir_git}/*.git" -prune | sort \ 233 | join - "${tmpfile}" | tr '\n' ' ') 234 235 if [ -z "${repo_list}" ]; then 236 # No repo to index. Delete index file if any 237 rm -f "${dir_www}/index.html" 238 else 239 # Generate the index 240 # shellcheck disable=SC2086 241 stagit-index ${repo_list} > "${dir_www}/index.html" 242 fi 243 244 rm -f "${tmpfile}" 245 } 246 247 # Inputs: 248 # - @: repository list 249 # - base_url: base URL under which the git HTML repository is exposed 250 # - dir_git: directory where to publish the git repository 251 # - dir_www: directory where to publish the git repository's HTML pages 252 # - force: force generation of HTML pages from scratch 253 publish() # list of repositories 254 { 255 printf '%s\n' "$@" | while read -r repo; do 256 # Make the repository path absolute to ensure both the validity of 257 # the symbolic link and a valid repository name 258 repo="$(cd -- "${repo}" && echo "${PWD}")" 259 260 printf '%s: ' "${repo}" 261 262 # Isn't the repository prohibited from publication? 263 # shellcheck disable=SC2310 264 if publication_ban "${repo}"; then 265 printf 'ban\n' 266 267 else 268 check_repo 269 publish_repo 270 setup_post_receive_hook 271 printf 'done\n' 272 fi 273 done 274 } 275 276 # Inputs: 277 # - @ : repository list 278 # - base_url: base URL under which the git HTML repositories are exposed 279 # - dir_git: directory where to publish the git repositories 280 # - dir_www: directory where to publish the git repositories' HTML pages 281 # - force: force generation of HTML pages from scratch 282 unpublish() 283 { 284 printf '%s\n' "$@" | while read -r repo; do 285 # Make the repository path absolute to ensure both the validity of 286 # the symbolic link and a valid repository name 287 repo="$(cd -- "${repo}" && echo "${PWD}")" 288 printf '%s: ' "${repo}" 289 290 check_repo 291 292 repo_name=$(basename "${repo}" ".git") 293 repo_www="${dir_www}/${repo_name}" 294 repo_git="${dir_git}/${repo_name}.git" 295 296 rm -rf "${repo_www}" # Remove HTML pages 297 rm -f "${repo_git}" # Remove link in the publicly exposed directory 298 299 # shellcheck disable=SC2310 300 if check_post_receive_hook "${repo}"; then 301 rm -f "${repo}/hooks/post-receive" 302 fi 303 304 printf 'done\n' 305 done 306 } 307 308 ######################################################################## 309 # The script 310 ######################################################################## 311 base_url="${GIT_PUBLISH_BASE_URL:-}" 312 dir_git="${GIT_PUBLISH_DIR_GIT:-/srv/git}" 313 dir_www="${GIT_PUBLISH_DIR_WWW:-/srv/www/git}" 314 force=0 # Force HTML generation 315 delete=0 # Delete publication 316 317 # Parse input arguments 318 OPTIND=1 319 while getopts ":dfg:u:w:" opt; do 320 case "${opt}" in 321 d) delete=1 ;; 322 f) force=1 ;; 323 u) base_url="${OPTARG}" ;; 324 g) dir_git="${OPTARG}" ;; # git directory 325 w) dir_www="${OPTARG}" ;; # WWW directory 326 *) synopsis; die ;; 327 esac 328 done 329 330 # Check mandatory options 331 [ "${OPTIND}" -le $# ] || { synopsis; die; } 332 333 if [ -z "${dir_git}" ]; then 334 >&2 printf 'git directory is missing\n' 335 die 336 fi 337 if [ -z "${dir_www}" ]; then 338 >&2 printf 'WWW directory is missing\n' 339 die 340 fi 341 if [ -z "${base_url}" ] && [ "${delete}" -eq 0 ]; then 342 >&2 printf 'Base url is missing\n' 343 die 344 fi 345 346 check_directory "${dir_git}" 347 check_directory "${dir_www}" 348 349 if [ "${delete}" -eq 0 ]; then 350 check_resources "${dir_www}" 351 fi 352 353 # Skip parsed arguments 354 shift $((OPTIND - 1)) 355 356 if [ "${delete}" -eq 0 ]; then 357 publish "$@" 358 else 359 unpublish "$@" 360 fi 361 362 # [Re]generate index of publicly exposed repositories 363 make_index 364 365 die 0