From cff31e5c000887a0abc002ea738d912d558dce9a Mon Sep 17 00:00:00 2001 From: Bruno Haible Date: Mon, 1 Apr 2019 23:15:56 +0200 Subject: [PATCH] gitsub.sh: New file. * top/gitsub.sh: New file. --- ChangeLog | 5 + top/gitsub.sh | 483 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100755 top/gitsub.sh diff --git a/ChangeLog b/ChangeLog index 4c44c85f41..3e9d27a766 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2019-04-02 Bruno Haible + + gitsub.sh: New file. + * top/gitsub.sh: New file. + 2019-04-18 Akim Demaille argmatch: use void* for raw memory pointers diff --git a/top/gitsub.sh b/top/gitsub.sh new file mode 100755 index 0000000000..9665198068 --- /dev/null +++ b/top/gitsub.sh @@ -0,0 +1,483 @@ +#! /bin/sh +# +# Copyright (C) 2019 Free Software Foundation, Inc. +# Written by Bruno Haible , 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 . + +# 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 _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 +# . 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 [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 _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 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 _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 _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 +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 $# = 2 && 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 +# 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 "$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 -- "$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 ) + if test $# = 0; then + for sub in $subcheckout_names $submodule_names; do + func_pull "$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_pull "$1" + 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 -- 2.39.5