mktime: prefer static_assert to verify
[gnulib.git] / top / gitsub.sh
blob38b15a4e24b50de5f4f2f0d7ab47e878e988b124
1 #! /bin/sh
3 # Copyright (C) 2019-2024 Free Software Foundation, Inc.
4 # Written by Bruno Haible <bruno@clisp.org>, 2019.
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.
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.
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/>.
19 # Program that manages the subdirectories of a git checkout of a package
20 # that come from other packages (called "dependency packages").
22 # This program is similar in spirit to 'git submodule', with three
23 # essential differences:
25 # 1) Its options are easy to remember, and do not require knowledge of
26 # 'git submodule'.
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.
33 # The developer indicates this different checkout by setting the
34 # environment variable <SUBDIR>_SRCDIR (e.g. GNULIB_SRCDIR) to point to it.
36 # 3) The package maintainer may choose to use or not use git submodules.
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).
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 periodically upgrade the dependency.
53 # Upgrading the dependency is an implicit operation.
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:
61 # [submodule "gnulib"]
62 # url = https://git.savannah.gnu.org/git/gnulib.git
63 # path = gnulib
65 # You don't add this piece of configuration to .gitmodules manually. Instead,
66 # you would invoke
67 # $ git submodule add --name "gnulib" -- https://git.savannah.gnu.org/git/gnulib.git gnulib
69 # * The subdirectories that are not git submodules, in a similar syntax. For
70 # example:
72 # [subcheckout "gnulib"]
73 # url = https://git.savannah.gnu.org/git/gnulib.git
74 # path = gnulib
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.
81 scriptname="$0"
82 scriptversion='2019-04-01'
83 nl='
85 IFS=" "" $nl"
87 # func_usage
88 # outputs to stdout the --help usage message.
89 func_usage ()
91 echo "\
92 Usage: gitsub.sh pull [SUBDIR]
93 gitsub.sh upgrade [SUBDIR]
94 gitsub.sh checkout SUBDIR REVISION
96 Operations:
98 gitsub.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.
111 gitsub.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.
120 gitsub.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.
125 This script requires the git program in the PATH and an internet connection.
129 # func_version
130 # outputs to stdout the --version message.
131 func_version ()
133 year=`echo "$scriptversion" | sed -e 's/^\(....\)-.*/\1/'`
134 echo "\
135 gitsub.sh (GNU gnulib) $scriptversion
136 Copyright (C) 2019-$year Free Software Foundation, Inc.
137 License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
138 This is free software: you are free to change and redistribute it.
139 There is NO WARRANTY, to the extent permitted by law.
141 printf "Written by %s.\n" "Bruno Haible"
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
148 func_fatal_error ()
150 echo "$scriptname: *** $1" 1>&2
151 echo "$scriptname: *** Stop." 1>&2
152 exit 1
155 # func_warning message
156 # Outputs to stderr a warning message,
157 func_warning ()
159 echo "gitsub.sh: warning: $1" 1>&2
162 # func_note message
163 # Outputs to stdout a note message,
164 func_note ()
166 echo "gitsub.sh: note: $1"
169 # Unset CDPATH. Otherwise, output from 'cd dir' can surprise callers.
170 (unset CDPATH) >/dev/null 2>&1 && unset CDPATH
172 # Command-line option processing.
173 mode=
174 while 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 ;;
191 break ;;
192 esac
193 done
194 if 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
199 case "$1" in
200 pull | upgrade | checkout )
201 mode="$1"
202 shift ;;
204 echo "gitsub.sh: unknown operation '$1'" 1>&2
205 echo "Try 'gitsub.sh --help' for more information." 1>&2
206 exit 1 ;;
207 esac
208 if { 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
214 if 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
220 # Read the configuration.
221 # Output:
222 # - subcheckout_names space-separated list of subcheckout names
223 # - submodule_names space-separated list of submodule names
224 if 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' ' '`
227 else
228 subcheckout_names=
229 submodule_names=
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
246 func_validate ()
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"
264 if test -d "$path"; then
265 if test -d "$path/.git"; then
266 # It's a plain checkout.
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."
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"
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"
293 if test -d "$path"; then
294 if test -d "$path/.git" || test -f "$path/.git"; then
295 # It's likely a submodule.
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."
311 else
312 # The subdir does not yet exist.
313 needs_init=true
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"
326 esac
327 if ! $found; then
328 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules"
333 # func_cleanup_current_git_clone
334 # Cleans up the current 'git clone' operation.
335 # Input:
336 # - path
337 func_cleanup_current_git_clone ()
339 rm -rf "$path"
340 func_fatal_error "git clone failed"
343 # func_pull SUBDIR GIT_OPTIONS
344 # Implements the 'pull' operation.
345 func_pull ()
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"
355 else
356 # The subdir does not yet exist. Create a plain checkout.
357 trap func_cleanup_current_git_clone HUP INT PIPE TERM
358 git clone $2 "$url" "$path" || func_cleanup_current_git_clone
359 trap - HUP INT PIPE TERM
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"
374 esac
378 # func_upgrade SUBDIR
379 # Implements the 'upgrade' operation.
380 func_upgrade ()
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"
391 esac
392 case " $submodule_names " in *" $1 "*)
393 # It's a submodule.
394 if test -z "$needs_init"; then
395 (cd "$path" \
396 && git fetch \
397 && branch=`git branch --show-current` \
398 && { test -n "$branch" || branch=HEAD; } \
399 && sed_escape_dots='s/\([.]\)/\\\1/g' \
400 && branch_escaped=`echo "$branch" | sed -e "${sed_escape_dots}"` \
401 && remote=`git branch -r | sed -n -e "s| origin/${branch_escaped} -> ||p"` \
402 && { test -n "$remote" || remote="origin/${branch}"; } \
403 && echo "In subdirectory $path: Running \"git merge $remote\"" \
404 && git merge "$remote"
405 ) || func_fatal_error "git operation failed"
408 esac
409 else
410 # The subdir does not yet exist.
411 func_fatal_error "Subdirectory '$path' does not exist yet. Use 'gitsub.sh pull' to create it."
416 # func_checkout SUBDIR REVISION
417 # Implements the 'checkout' operation.
418 func_checkout ()
420 func_validate "$1"
421 if test -z "$srcdir"; then
422 if test -d "$path"; then
423 case " $subcheckout_names " in *" $1 "*)
424 # It's a subcheckout.
425 if test -d "$path/.git"; then
426 (cd "$path" && git checkout "$2") || func_fatal_error "git operation failed"
429 esac
430 case " $submodule_names " in *" $1 "*)
431 # It's a submodule.
432 if test -z "$needs_init"; then
433 (cd "$path" && git checkout "$2") || func_fatal_error "git operation failed"
436 esac
437 else
438 # The subdir does not yet exist.
439 func_fatal_error "Subdirectory '$path' does not exist yet. Use 'gitsub.sh pull' to create it."
444 case "$mode" in
445 pull )
446 git_options=""
447 while test $# -gt 0; do
448 case "$1" in
449 --reference=* | --depth=* | --recursive)
450 git_options="$git_options $1"
451 shift
453 --reference | --depth)
454 git_options="$git_options $1 $2"
455 shift; shift
458 break
460 esac
461 done
462 if test $# -gt 1; then
463 echo "gitsub.sh: too many arguments in '$mode' mode" 1>&2
464 echo "Try 'gitsub.sh --help' for more information." 1>&2
465 exit 1
467 if test $# = 0; then
468 for sub in $subcheckout_names $submodule_names; do
469 func_pull "$sub" "$git_options"
470 done
471 else
472 valid=false
473 for sub in $subcheckout_names $submodule_names; do
474 if test "$sub" = "$1"; then
475 valid=true
477 done
478 if $valid; then
479 func_pull "$1" "$git_options"
480 else
481 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules"
486 upgrade )
487 if test $# = 0; then
488 for sub in $subcheckout_names $submodule_names; do
489 func_upgrade "$sub"
490 done
491 else
492 valid=false
493 for sub in $subcheckout_names $submodule_names; do
494 if test "$sub" = "$1"; then
495 valid=true
497 done
498 if $valid; then
499 func_upgrade "$1"
500 else
501 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules"
506 checkout )
507 valid=false
508 for sub in $subcheckout_names $submodule_names; do
509 if test "$sub" = "$1"; then
510 valid=true
512 done
513 if $valid; then
514 func_checkout "$1" "$2"
515 else
516 func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules"
519 esac