| #! /bin/sh |
| # |
| # Copyright (C) 2019 Free Software Foundation, Inc. |
| # Written by Bruno Haible <bruno@clisp.org>, 2019. |
| # |
| # This program is free software: you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 3 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <https://www.gnu.org/licenses/>. |
| |
| # Program that manages the subdirectories of a git checkout of a package |
| # that come from other packages (called "dependency packages"). |
| # |
| # This program is similar in spirit to 'git submodule', with three |
| # essential differences: |
| # |
| # 1) Its options are easy to remember, and do not require knowledge of |
| # 'git submodule'. |
| # |
| # 2) The developer may choose to work on a different checkout for each |
| # dependency package. This is important when the developer is |
| # preparing simultaneous changes to the package and the dependency |
| # package, or is using the dependency package in several packages. |
| # |
| # The developer indicates this different checkout by setting the |
| # environment variable <SUBDIR>_SRCDIR (e.g. GNULIB_SRCDIR) to point to it. |
| # |
| # 3) The package maintainer may choose to use or not use git submodules. |
| # |
| # The advantages of management through a git submodule are: |
| # - Changes to the dependency package cannot suddenly break your package. |
| # In other words, when there is an incompatible change that will cause |
| # a breakage, you can fix things at your pace; you are not forced to |
| # cope with such breakages in an emergency. |
| # - When you need to make a change as a response to a change in the |
| # dependency package, your co-developers cannot accidentally mix things |
| # up (for example, use a combination of your newest change with an |
| # older version of the dependency package). |
| # |
| # The advantages of management without a git submodule (just as a plain |
| # subdirectory, let's call it a "subcheckout") are: |
| # - The simplicity: you are conceptually always using the newest revision |
| # of the dependency package. |
| # - You don't have to remember to periodially upgrade the dependency. |
| # Upgrading the dependency is an implicit operation. |
| |
| # This program is meant to be copied to the top-level directory of the package, |
| # together with a configuration file. The configuration is supposed to be |
| # named '.gitmodules' and to define: |
| # * The git submodules, as described in "man 5 gitmodules" or |
| # <https://git-scm.com/docs/gitmodules>. For example: |
| # |
| # [submodule "gnulib"] |
| # url = git://git.savannah.gnu.org/gnulib.git |
| # path = gnulib |
| # |
| # You don't add this piece of configuration to .gitmodules manually. Instead, |
| # you would invoke |
| # $ git submodule add --name "gnulib" -- git://git.savannah.gnu.org/gnulib.git gnulib |
| # |
| # * The subdirectories that are not git submodules, in a similar syntax. For |
| # example: |
| # |
| # [subcheckout "gnulib"] |
| # url = git://git.savannah.gnu.org/gnulib.git |
| # path = gnulib |
| # |
| # Here the URL is the one used for anonymous checkouts of the dependency |
| # package. If the developer needs a checkout with write access, they can |
| # either set the GNULIB_SRCDIR environment variable to point to that checkout |
| # or modify the gnulib/.git/config file to enter a different URL. |
| |
| scriptname="$0" |
| scriptversion='2019-04-01' |
| nl=' |
| ' |
| IFS=" "" $nl" |
| |
| # func_usage |
| # outputs to stdout the --help usage message. |
| func_usage () |
| { |
| echo "\ |
| Usage: gitsub.sh pull [SUBDIR] |
| gitsub.sh upgrade [SUBDIR] |
| gitsub.sh checkout SUBDIR REVISION |
| |
| Operations: |
| |
| gitsub.sh pull [GIT_OPTIONS] [SUBDIR] |
| You should perform this operation after 'git clone ...' and after |
| every 'git pull'. |
| It brings your checkout in sync with what the other developers of |
| your package have committed and pushed. |
| If an environment variable <SUBDIR>_SRCDIR is set, with a non-empty |
| value, nothing is done for this SUBDIR. |
| Supported GIT_OPTIONS (for expert git users) are: |
| --reference <repository> |
| --depth <depth> |
| --recursive |
| If no SUBDIR is specified, the operation applies to all dependencies. |
| |
| gitsub.sh upgrade [SUBDIR] |
| You should perform this operation periodically, to ensure currency |
| of the dependency package revisions that you use. |
| This operation pulls and checks out the changes that the developers |
| of the dependency package have committed and pushed. |
| If an environment variable <SUBDIR>_SRCDIR is set, with a non-empty |
| value, nothing is done for this SUBDIR. |
| If no SUBDIR is specified, the operation applies to all dependencies. |
| |
| gitsub.sh checkout SUBDIR REVISION |
| Checks out a specific revision for a dependency package. |
| If an environment variable <SUBDIR>_SRCDIR is set, with a non-empty |
| value, this operation fails. |
| |
| This script requires the git program in the PATH and an internet connection. |
| " |
| } |
| |
| # func_version |
| # outputs to stdout the --version message. |
| func_version () |
| { |
| year=`echo "$scriptversion" | sed -e 's/^\(....\)-.*/\1/'` |
| echo "\ |
| gitsub.sh (GNU gnulib) $scriptversion |
| Copyright (C) 2019-$year Free Software Foundation, Inc. |
| License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html> |
| This is free software: you are free to change and redistribute it. |
| There is NO WARRANTY, to the extent permitted by law. |
| " |
| printf "Written by %s.\n" "Bruno Haible" |
| } |
| |
| # func_fatal_error message |
| # outputs to stderr a fatal error message, and terminates the program. |
| # Input: |
| # - scriptname name of this program |
| func_fatal_error () |
| { |
| echo "$scriptname: *** $1" 1>&2 |
| echo "$scriptname: *** Stop." 1>&2 |
| exit 1 |
| } |
| |
| # func_warning message |
| # Outputs to stderr a warning message, |
| func_warning () |
| { |
| echo "gitsub.sh: warning: $1" 1>&2 |
| } |
| |
| # func_note message |
| # Outputs to stdout a note message, |
| func_note () |
| { |
| echo "gitsub.sh: note: $1" |
| } |
| |
| # Unset CDPATH. Otherwise, output from 'cd dir' can surprise callers. |
| (unset CDPATH) >/dev/null 2>&1 && unset CDPATH |
| |
| # Command-line option processing. |
| mode= |
| while test $# -gt 0; do |
| case "$1" in |
| --help | --hel | --he | --h ) |
| func_usage |
| exit $? ;; |
| --version | --versio | --versi | --vers | --ver | --ve | --v ) |
| func_version |
| exit $? ;; |
| -- ) |
| # Stop option processing |
| shift |
| break ;; |
| -* ) |
| echo "gitsub.sh: unknown option $1" 1>&2 |
| echo "Try 'gitsub.sh --help' for more information." 1>&2 |
| exit 1 ;; |
| * ) |
| break ;; |
| esac |
| done |
| if test $# = 0; then |
| echo "gitsub.sh: missing operation argument" 1>&2 |
| echo "Try 'gitsub.sh --help' for more information." 1>&2 |
| exit 1 |
| fi |
| case "$1" in |
| pull | upgrade | checkout ) |
| mode="$1" |
| shift ;; |
| *) |
| echo "gitsub.sh: unknown operation '$1'" 1>&2 |
| echo "Try 'gitsub.sh --help' for more information." 1>&2 |
| exit 1 ;; |
| esac |
| if { test $mode = upgrade && test $# -gt 1; } \ |
| || { test $mode = checkout && test $# -gt 2; }; then |
| echo "gitsub.sh: too many arguments in '$mode' mode" 1>&2 |
| echo "Try 'gitsub.sh --help' for more information." 1>&2 |
| exit 1 |
| fi |
| if test $# = 0 && test $mode = checkout; then |
| echo "gitsub.sh: too few arguments in '$mode' mode" 1>&2 |
| echo "Try 'gitsub.sh --help' for more information." 1>&2 |
| exit 1 |
| fi |
| |
| # Read the configuration. |
| # Output: |
| # - subcheckout_names space-separated list of subcheckout names |
| # - submodule_names space-separated list of submodule names |
| if test -f .gitmodules; then |
| subcheckout_names=`git config --file .gitmodules --get-regexp --name-only 'subcheckout\..*\.url' | sed -e 's/^subcheckout\.//' -e 's/\.url$//' | tr -d '\r' | tr '\n' ' '` |
| submodule_names=`git config --file .gitmodules --get-regexp --name-only 'submodule\..*\.url' | sed -e 's/^submodule\.//' -e 's/\.url$//' | tr -d '\r' | tr '\n' ' '` |
| else |
| subcheckout_names= |
| submodule_names= |
| fi |
| |
| # func_validate SUBDIR |
| # Verifies that the state on the file system is in sync with the declarations |
| # in the configuration file. |
| # Input: |
| # - subcheckout_names space-separated list of subcheckout names |
| # - submodule_names space-separated list of submodule names |
| # Output: |
| # - srcdirvar Environment that the user can set |
| # - srcdir Value of the environment variable |
| # - path if $srcdir = "": relative path of the subdirectory |
| # - needs_init if $srcdir = "" and $path is not yet initialized: |
| # true |
| # - url if $srcdir = "" and $path is not yet initialized: |
| # the repository URL |
| func_validate () |
| { |
| srcdirvar=`echo "$1" | LC_ALL=C sed -e 's/[^a-zA-Z0-9]/_/g' | LC_ALL=C tr '[a-z]' '[A-Z]'`"_SRCDIR" |
| eval 'srcdir=$'"$srcdirvar" |
| path= |
| url= |
| if test -n "$srcdir"; then |
| func_note "Ignoring '$1' because $srcdirvar is set." |
| else |
| found=false |
| needs_init= |
| case " $subcheckout_names " in *" $1 "*) |
| found=true |
| # It ought to be a subcheckout. |
| path=`git config --file .gitmodules "subcheckout.$1.path"` |
| if test -z "$path"; then |
| path="$1" |
| fi |
| if test -d "$path"; then |
| if test -d "$path/.git"; then |
| # It's a plain checkout. |
| : |
| else |
| if test -f "$path/.git"; then |
| # It's a submodule. |
| func_fatal_error "Subdirectory '$path' is supposed to be a plain checkout, but it is a submodule." |
| else |
| func_warning "Ignoring '$path' because it exists but is not a git checkout." |
| fi |
| fi |
| else |
| # The subdir does not yet exist. |
| needs_init=true |
| url=`git config --file .gitmodules "subcheckout.$1.url"` |
| if test -z "$url"; then |
| func_fatal_error "Property subcheckout.$1.url is not defined in .gitmodules" |
| fi |
| fi |
| ;; |
| esac |
| case " $submodule_names " in *" $1 "*) |
| found=true |
| # It ought to be a submodule. |
| path=`git config --file .gitmodules "submodule.$1.path"` |
| if test -z "$path"; then |
| path="$1" |
| fi |
| if test -d "$path"; then |
| if test -d "$path/.git" || test -f "$path/.git"; then |
| # It's likely a submodule. |
| : |
| else |
| path_if_empty=`find "$path" -prune -empty 2>/dev/null` |
| if test -n "$path_if_empty"; then |
| # The subdir is empty. |
| needs_init=true |
| else |
| # The subdir is not empty. |
| # It is important to report an error, because we don't want to erase |
| # the user's files and 'git submodule update gnulib' sometimes reports |
| # "fatal: destination path '$path' already exists and is not an empty directory." |
| # but sometimes does not. |
| func_fatal_error "Subdir '$path' exists but is not a git checkout." |
| fi |
| fi |
| else |
| # The subdir does not yet exist. |
| needs_init=true |
| fi |
| # Another way to determine needs_init could be: |
| # if git submodule status "$path" | grep '^-' > /dev/null; then |
| # needs_init=true |
| # fi |
| if test -n "$needs_init"; then |
| url=`git config --file .gitmodules "submodule.$1.url"` |
| if test -z "$url"; then |
| func_fatal_error "Property submodule.$1.url is not defined in .gitmodules" |
| fi |
| fi |
| ;; |
| esac |
| if ! $found; then |
| func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" |
| fi |
| fi |
| } |
| |
| # func_cleanup_current_git_clone |
| # Cleans up the current 'git clone' operation. |
| # Input: |
| # - path |
| func_cleanup_current_git_clone () |
| { |
| rm -rf "$path" |
| func_fatal_error "git clone failed" |
| } |
| |
| # func_pull SUBDIR GIT_OPTIONS |
| # Implements the 'pull' operation. |
| func_pull () |
| { |
| func_validate "$1" |
| if test -z "$srcdir"; then |
| case " $subcheckout_names " in *" $1 "*) |
| # It's a subcheckout. |
| if test -d "$path"; then |
| if test -d "$path/.git"; then |
| (cd "$path" && git pull) || func_fatal_error "git operation failed" |
| fi |
| else |
| # The subdir does not yet exist. Create a plain checkout. |
| trap func_cleanup_current_git_clone 1 2 13 15 |
| git clone $2 "$url" "$path" || func_cleanup_current_git_clone |
| trap - 1 2 13 15 |
| fi |
| ;; |
| esac |
| case " $submodule_names " in *" $1 "*) |
| # It's a submodule. |
| if test -n "$needs_init"; then |
| # Create a submodule checkout. |
| git submodule init -- "$path" && git submodule update $2 -- "$path" || func_fatal_error "git operation failed" |
| else |
| # See https://stackoverflow.com/questions/1030169/easy-way-to-pull-latest-of-all-git-submodules |
| # https://stackoverflow.com/questions/4611512/is-there-a-way-to-make-git-pull-automatically-update-submodules |
| git submodule update "$path" || func_fatal_error "git operation failed" |
| fi |
| ;; |
| esac |
| fi |
| } |
| |
| # func_upgrade SUBDIR |
| # Implements the 'upgrade' operation. |
| func_upgrade () |
| { |
| func_validate "$1" |
| if test -z "$srcdir"; then |
| if test -d "$path"; then |
| case " $subcheckout_names " in *" $1 "*) |
| # It's a subcheckout. |
| if test -d "$path/.git"; then |
| (cd "$path" && git pull) || func_fatal_error "git operation failed" |
| fi |
| ;; |
| esac |
| case " $submodule_names " in *" $1 "*) |
| # It's a submodule. |
| if test -z "$needs_init"; then |
| (cd "$path" && git fetch && git merge origin/master) || func_fatal_error "git operation failed" |
| fi |
| ;; |
| esac |
| else |
| # The subdir does not yet exist. |
| func_fatal_error "Subdirectory '$path' does not exist yet. Use 'gitsub.sh pull' to create it." |
| fi |
| fi |
| } |
| |
| # func_checkout SUBDIR REVISION |
| # Implements the 'checkout' operation. |
| func_checkout () |
| { |
| func_validate "$1" |
| if test -z "$srcdir"; then |
| if test -d "$path"; then |
| case " $subcheckout_names " in *" $1 "*) |
| # It's a subcheckout. |
| if test -d "$path/.git"; then |
| (cd "$path" && git checkout "$2") || func_fatal_error "git operation failed" |
| fi |
| ;; |
| esac |
| case " $submodule_names " in *" $1 "*) |
| # It's a submodule. |
| if test -z "$needs_init"; then |
| (cd "$path" && git checkout "$2") || func_fatal_error "git operation failed" |
| fi |
| ;; |
| esac |
| else |
| # The subdir does not yet exist. |
| func_fatal_error "Subdirectory '$path' does not exist yet. Use 'gitsub.sh pull' to create it." |
| fi |
| fi |
| } |
| |
| case "$mode" in |
| pull ) |
| git_options="" |
| while test $# -gt 0; do |
| case "$1" in |
| --reference=* | --depth=* | --recursive) |
| git_options="$git_options $1" |
| shift |
| ;; |
| --reference | --depth) |
| git_options="$git_options $1 $2" |
| shift; shift |
| ;; |
| *) |
| break |
| ;; |
| esac |
| done |
| if test $# -gt 1; then |
| echo "gitsub.sh: too many arguments in '$mode' mode" 1>&2 |
| echo "Try 'gitsub.sh --help' for more information." 1>&2 |
| exit 1 |
| fi |
| if test $# = 0; then |
| for sub in $subcheckout_names $submodule_names; do |
| func_pull "$sub" "$git_options" |
| done |
| else |
| valid=false |
| for sub in $subcheckout_names $submodule_names; do |
| if test "$sub" = "$1"; then |
| valid=true |
| fi |
| done |
| if $valid; then |
| func_pull "$1" "$git_options" |
| else |
| func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" |
| fi |
| fi |
| ;; |
| |
| upgrade ) |
| if test $# = 0; then |
| for sub in $subcheckout_names $submodule_names; do |
| func_upgrade "$sub" |
| done |
| else |
| valid=false |
| for sub in $subcheckout_names $submodule_names; do |
| if test "$sub" = "$1"; then |
| valid=true |
| fi |
| done |
| if $valid; then |
| func_upgrade "$1" |
| else |
| func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" |
| fi |
| fi |
| ;; |
| |
| checkout ) |
| valid=false |
| for sub in $subcheckout_names $submodule_names; do |
| if test "$sub" = "$1"; then |
| valid=true |
| fi |
| done |
| if $valid; then |
| func_checkout "$1" "$2" |
| else |
| func_fatal_error "Subdir '$1' is not configured as a subcheckout or a submodule in .gitmodules" |
| fi |
| ;; |
| esac |