1#! /bin/sh 2# 3# Copyright (C) 2019 Free Software Foundation, Inc. 4# Written by Bruno Haible <bruno@clisp.org>, 2019. 5# 6# This program is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 3 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <https://www.gnu.org/licenses/>. 18 19# Program that manages the subdirectories of a git checkout of a package 20# that come from other packages (called "dependency packages"). 21# 22# This program is similar in spirit to 'git submodule', with three 23# essential differences: 24# 25# 1) Its options are easy to remember, and do not require knowledge of 26# 'git submodule'. 27# 28# 2) The developer may choose to work on a different checkout for each 29# dependency package. This is important when the developer is 30# preparing simultaneous changes to the package and the dependency 31# package, or is using the dependency package in several packages. 32# 33# The developer indicates this different checkout by setting the 34# environment variable <SUBDIR>_SRCDIR (e.g. GNULIB_SRCDIR) to point to it. 35# 36# 3) The package maintainer may choose to use or not use git submodules. 37# 38# The advantages of management through a git submodule are: 39# - Changes to the dependency package cannot suddenly break your package. 40# In other words, when there is an incompatible change that will cause 41# a breakage, you can fix things at your pace; you are not forced to 42# cope with such breakages in an emergency. 43# - When you need to make a change as a response to a change in the 44# dependency package, your co-developers cannot accidentally mix things 45# up (for example, use a combination of your newest change with an 46# older version of the dependency package). 47# 48# The advantages of management without a git submodule (just as a plain 49# subdirectory, let's call it a "subcheckout") are: 50# - The simplicity: you are conceptually always using the newest revision 51# of the dependency package. 52# - You don't have to remember to periodially upgrade the dependency. 53# Upgrading the dependency is an implicit operation. 54 55# This program is meant to be copied to the top-level directory of the package, 56# together with a configuration file. The configuration is supposed to be 57# named '.gitmodules' and to define: 58# * The git submodules, as described in "man 5 gitmodules" or 59# <https://git-scm.com/docs/gitmodules>. For example: 60# 61# [submodule "gnulib"] 62# url = git://git.savannah.gnu.org/gnulib.git 63# path = gnulib 64# 65# You don't add this piece of configuration to .gitmodules manually. Instead, 66# you would invoke 67# $ git submodule add --name "gnulib" -- git://git.savannah.gnu.org/gnulib.git gnulib 68# 69# * The subdirectories that are not git submodules, in a similar syntax. For 70# example: 71# 72# [subcheckout "gnulib"] 73# url = git://git.savannah.gnu.org/gnulib.git 74# path = gnulib 75# 76# Here the URL is the one used for anonymous checkouts of the dependency 77# package. If the developer needs a checkout with write access, they can 78# either set the GNULIB_SRCDIR environment variable to point to that checkout 79# or modify the gnulib/.git/config file to enter a different URL. 80 81scriptname="$0" 82scriptversion='2019-04-01' 83nl=' 84' 85IFS=" "" $nl" 86 87# func_usage 88# outputs to stdout the --help usage message. 89func_usage () 90{ 91 echo "\ 92Usage: gitsub.sh pull [SUBDIR] 93 gitsub.sh upgrade [SUBDIR] 94 gitsub.sh checkout SUBDIR REVISION 95 96Operations: 97 98gitsub.sh pull [GIT_OPTIONS] [SUBDIR] 99 You should perform this operation after 'git clone ...' and after 100 every 'git pull'. 101 It brings your checkout in sync with what the other developers of 102 your package have committed and pushed. 103 If an environment variable <SUBDIR>_SRCDIR is set, with a non-empty 104 value, nothing is done for this SUBDIR. 105 Supported GIT_OPTIONS (for expert git users) are: 106 --reference <repository> 107 --depth <depth> 108 --recursive 109 If no SUBDIR is specified, the operation applies to all dependencies. 110 111gitsub.sh upgrade [SUBDIR] 112 You should perform this operation periodically, to ensure currency 113 of the dependency package revisions that you use. 114 This operation pulls and checks out the changes that the developers 115 of the dependency package have committed and pushed. 116 If an environment variable <SUBDIR>_SRCDIR is set, with a non-empty 117 value, nothing is done for this SUBDIR. 118 If no SUBDIR is specified, the operation applies to all dependencies. 119 120gitsub.sh checkout SUBDIR REVISION 121 Checks out a specific revision for a dependency package. 122 If an environment variable <SUBDIR>_SRCDIR is set, with a non-empty 123 value, this operation fails. 124 125This script requires the git program in the PATH and an internet connection. 126" 127} 128 129# func_version 130# outputs to stdout the --version message. 131func_version () 132{ 133 year=`echo "$scriptversion" | sed -e 's/^\(....\)-.*/\1/'` 134 echo "\ 135gitsub.sh (GNU gnulib) $scriptversion 136Copyright (C) 2019-$year Free Software Foundation, Inc. 137License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html> 138This is free software: you are free to change and redistribute it. 139There is NO WARRANTY, to the extent permitted by law. 140" 141 printf "Written by %s.\n" "Bruno Haible" 142} 143 144# func_fatal_error message 145# outputs to stderr a fatal error message, and terminates the program. 146# Input: 147# - scriptname name of this program 148func_fatal_error () 149{ 150 echo "$scriptname: *** $1" 1>&2 151 echo "$scriptname: *** Stop." 1>&2 152 exit 1 153} 154 155# func_warning message 156# Outputs to stderr a warning message, 157func_warning () 158{ 159 echo "gitsub.sh: warning: $1" 1>&2 160} 161 162# func_note message 163# Outputs to stdout a note message, 164func_note () 165{ 166 echo "gitsub.sh: note: $1" 167} 168 169# Unset CDPATH. Otherwise, output from 'cd dir' can surprise callers. 170(unset CDPATH) >/dev/null 2>&1 && unset CDPATH 171 172# Command-line option processing. 173mode= 174while test $# -gt 0; do 175 case "$1" in 176 --help | --hel | --he | --h ) 177 func_usage 178 exit $? ;; 179 --version | --versio | --versi | --vers | --ver | --ve | --v ) 180 func_version 181 exit $? ;; 182 -- ) 183 # Stop option processing 184 shift 185 break ;; 186 -* ) 187 echo "gitsub.sh: unknown option $1" 1>&2 188 echo "Try 'gitsub.sh --help' for more information." 1>&2 189 exit 1 ;; 190 * ) 191 break ;; 192 esac 193done 194if test $# = 0; then 195 echo "gitsub.sh: missing operation argument" 1>&2 196 echo "Try 'gitsub.sh --help' for more information." 1>&2 197 exit 1 198fi 199case "$1" in 200 pull | upgrade | checkout ) 201 mode="$1" 202 shift ;; 203 *) 204 echo "gitsub.sh: unknown operation '$1'" 1>&2 205 echo "Try 'gitsub.sh --help' for more information." 1>&2 206 exit 1 ;; 207esac 208if { test $mode = upgrade && test $# -gt 1; } \ 209 || { test $mode = checkout && test $# -gt 2; }; then 210 echo "gitsub.sh: too many arguments in '$mode' mode" 1>&2 211 echo "Try 'gitsub.sh --help' for more information." 1>&2 212 exit 1 213fi 214if test $# = 0 && test $mode = checkout; then 215 echo "gitsub.sh: too few arguments in '$mode' mode" 1>&2 216 echo "Try 'gitsub.sh --help' for more information." 1>&2 217 exit 1 218fi 219 220# Read the configuration. 221# Output: 222# - subcheckout_names space-separated list of subcheckout names 223# - submodule_names space-separated list of submodule names 224if test -f .gitmodules; then 225 subcheckout_names=`git config --file .gitmodules --get-regexp --name-only 'subcheckout\..*\.url' | sed -e 's/^subcheckout\.//' -e 's/\.url$//' | tr -d '\r' | tr '\n' ' '` 226 submodule_names=`git config --file .gitmodules --get-regexp --name-only 'submodule\..*\.url' | sed -e 's/^submodule\.//' -e 's/\.url$//' | tr -d '\r' | tr '\n' ' '` 227else 228 subcheckout_names= 229 submodule_names= 230fi 231 232# func_validate SUBDIR 233# Verifies that the state on the file system is in sync with the declarations 234# in the configuration file. 235# Input: 236# - subcheckout_names space-separated list of subcheckout names 237# - submodule_names space-separated list of submodule names 238# Output: 239# - srcdirvar Environment that the user can set 240# - srcdir Value of the environment variable 241# - path if $srcdir = "": relative path of the subdirectory 242# - needs_init if $srcdir = "" and $path is not yet initialized: 243# true 244# - url if $srcdir = "" and $path is not yet initialized: 245# the repository URL 246func_validate () 247{ 248 srcdirvar=`echo "$1" | LC_ALL=C sed -e 's/[^a-zA-Z0-9]/_/g' | LC_ALL=C tr '[a-z]' '[A-Z]'`"_SRCDIR" 249 eval 'srcdir=$'"$srcdirvar" 250 path= 251 url= 252 if test -n "$srcdir"; then 253 func_note "Ignoring '$1' because $srcdirvar is set." 254 else 255 found=false 256 needs_init= 257 case " $subcheckout_names " in *" $1 "*) 258 found=true 259 # It ought to be a subcheckout. 260 path=`git config --file .gitmodules "subcheckout.$1.path"` 261 if test -z "$path"; then 262 path="$1" 263 fi 264 if test -d "$path"; then 265 if test -d "$path/.git"; then 266 # It's a plain checkout. 267 : 268 else 269 if test -f "$path/.git"; then 270 # It's a submodule. 271 func_fatal_error "Subdirectory '$path' is supposed to be a plain checkout, but it is a submodule." 272 else 273 func_warning "Ignoring '$path' because it exists but is not a git checkout." 274 fi 275 fi 276 else 277 # The subdir does not yet exist. 278 needs_init=true 279 url=`git config --file .gitmodules "subcheckout.$1.url"` 280 if test -z "$url"; then 281 func_fatal_error "Property subcheckout.$1.url is not defined in .gitmodules" 282 fi 283 fi 284 ;; 285 esac 286 case " $submodule_names " in *" $1 "*) 287 found=true 288 # It ought to be a submodule. 289 path=`git config --file .gitmodules "submodule.$1.path"` 290 if test -z "$path"; then 291 path="$1" 292 fi 293 if test -d "$path"; then 294 if test -d "$path/.git" || test -f "$path/.git"; then 295 # It's likely a submodule. 296 : 297 else 298 path_if_empty=`find "$path" -prune -empty 2>/dev/null` 299 if test -n "$path_if_empty"; then 300 # The subdir is empty. 301 needs_init=true 302 else 303 # The subdir is not empty. 304 # It is important to report an error, because we don't want to erase 305 # the user's files and 'git submodule update gnulib' sometimes reports 306 # "fatal: destination path '$path' already exists and is not an empty directory." 307 # but sometimes does not. 308 func_fatal_error "Subdir '$path' exists but is not a git checkout." 309 fi 310 fi 311 else 312 # The subdir does not yet exist. 313 needs_init=true 314 fi 315 # Another way to determine needs_init could be: 316 # if git submodule status "$path" | grep '^-' > /dev/null; then 317 # needs_init=true 318 # fi 319 if test -n "$needs_init"; then 320 url=`git config --file .gitmodules "submodule.$1.url"` 321 if test -z "$url"; then 322 func_fatal_error "Property submodule.$1.url is not defined in .gitmodules" 323 fi 324 fi 325 ;; 326 esac 327 if ! $found; then 328 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" 329 fi 330 fi 331} 332 333# func_cleanup_current_git_clone 334# Cleans up the current 'git clone' operation. 335# Input: 336# - path 337func_cleanup_current_git_clone () 338{ 339 rm -rf "$path" 340 func_fatal_error "git clone failed" 341} 342 343# func_pull SUBDIR GIT_OPTIONS 344# Implements the 'pull' operation. 345func_pull () 346{ 347 func_validate "$1" 348 if test -z "$srcdir"; then 349 case " $subcheckout_names " in *" $1 "*) 350 # It's a subcheckout. 351 if test -d "$path"; then 352 if test -d "$path/.git"; then 353 (cd "$path" && git pull) || func_fatal_error "git operation failed" 354 fi 355 else 356 # The subdir does not yet exist. Create a plain checkout. 357 trap func_cleanup_current_git_clone 1 2 13 15 358 git clone $2 "$url" "$path" || func_cleanup_current_git_clone 359 trap - 1 2 13 15 360 fi 361 ;; 362 esac 363 case " $submodule_names " in *" $1 "*) 364 # It's a submodule. 365 if test -n "$needs_init"; then 366 # Create a submodule checkout. 367 git submodule init -- "$path" && git submodule update $2 -- "$path" || func_fatal_error "git operation failed" 368 else 369 # See https://stackoverflow.com/questions/1030169/easy-way-to-pull-latest-of-all-git-submodules 370 # https://stackoverflow.com/questions/4611512/is-there-a-way-to-make-git-pull-automatically-update-submodules 371 git submodule update "$path" || func_fatal_error "git operation failed" 372 fi 373 ;; 374 esac 375 fi 376} 377 378# func_upgrade SUBDIR 379# Implements the 'upgrade' operation. 380func_upgrade () 381{ 382 func_validate "$1" 383 if test -z "$srcdir"; then 384 if test -d "$path"; then 385 case " $subcheckout_names " in *" $1 "*) 386 # It's a subcheckout. 387 if test -d "$path/.git"; then 388 (cd "$path" && git pull) || func_fatal_error "git operation failed" 389 fi 390 ;; 391 esac 392 case " $submodule_names " in *" $1 "*) 393 # It's a submodule. 394 if test -z "$needs_init"; then 395 (cd "$path" && git fetch && git merge origin/master) || func_fatal_error "git operation failed" 396 fi 397 ;; 398 esac 399 else 400 # The subdir does not yet exist. 401 func_fatal_error "Subdirectory '$path' does not exist yet. Use 'gitsub.sh pull' to create it." 402 fi 403 fi 404} 405 406# func_checkout SUBDIR REVISION 407# Implements the 'checkout' operation. 408func_checkout () 409{ 410 func_validate "$1" 411 if test -z "$srcdir"; then 412 if test -d "$path"; then 413 case " $subcheckout_names " in *" $1 "*) 414 # It's a subcheckout. 415 if test -d "$path/.git"; then 416 (cd "$path" && git checkout "$2") || func_fatal_error "git operation failed" 417 fi 418 ;; 419 esac 420 case " $submodule_names " in *" $1 "*) 421 # It's a submodule. 422 if test -z "$needs_init"; then 423 (cd "$path" && git checkout "$2") || func_fatal_error "git operation failed" 424 fi 425 ;; 426 esac 427 else 428 # The subdir does not yet exist. 429 func_fatal_error "Subdirectory '$path' does not exist yet. Use 'gitsub.sh pull' to create it." 430 fi 431 fi 432} 433 434case "$mode" in 435 pull ) 436 git_options="" 437 while test $# -gt 0; do 438 case "$1" in 439 --reference=* | --depth=* | --recursive) 440 git_options="$git_options $1" 441 shift 442 ;; 443 --reference | --depth) 444 git_options="$git_options $1 $2" 445 shift; shift 446 ;; 447 *) 448 break 449 ;; 450 esac 451 done 452 if test $# -gt 1; then 453 echo "gitsub.sh: too many arguments in '$mode' mode" 1>&2 454 echo "Try 'gitsub.sh --help' for more information." 1>&2 455 exit 1 456 fi 457 if test $# = 0; then 458 for sub in $subcheckout_names $submodule_names; do 459 func_pull "$sub" "$git_options" 460 done 461 else 462 valid=false 463 for sub in $subcheckout_names $submodule_names; do 464 if test "$sub" = "$1"; then 465 valid=true 466 fi 467 done 468 if $valid; then 469 func_pull "$1" "$git_options" 470 else 471 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" 472 fi 473 fi 474 ;; 475 476 upgrade ) 477 if test $# = 0; then 478 for sub in $subcheckout_names $submodule_names; do 479 func_upgrade "$sub" 480 done 481 else 482 valid=false 483 for sub in $subcheckout_names $submodule_names; do 484 if test "$sub" = "$1"; then 485 valid=true 486 fi 487 done 488 if $valid; then 489 func_upgrade "$1" 490 else 491 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" 492 fi 493 fi 494 ;; 495 496 checkout ) 497 valid=false 498 for sub in $subcheckout_names $submodule_names; do 499 if test "$sub" = "$1"; then 500 valid=true 501 fi 502 done 503 if $valid; then 504 func_checkout "$1" "$2" 505 else 506 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" 507 fi 508 ;; 509esac 510