Code

tag: die when listing missing or corrupt objects
[git.git] / git-submodule.sh
index b40f876a2ca9fe985cedc622ab28a9f461edc5ab..c94218b8779be63e1441480ef3828fb4ce0b61b0 100755 (executable)
@@ -4,44 +4,57 @@
 #
 # Copyright (c) 2007 Lars Hjemli
 
-USAGE="[--quiet] [--cached] \
-[add <repo> [-b branch] <path>]|[status|init|update [-i|--init]|summary [-n|--summary-limit <n>] [<commit>]] \
-[--] [<path>...]"
+dashless=$(basename "$0" | sed -e 's/-/ /')
+USAGE="[--quiet] add [-b branch] [-f|--force] [--reference <repository>] [--] <repository> [<path>]
+   or: $dashless [--quiet] status [--cached] [--recursive] [--] [<path>...]
+   or: $dashless [--quiet] init [--] [<path>...]
+   or: $dashless [--quiet] update [--init] [-N|--no-fetch] [-f|--force] [--rebase] [--reference <repository>] [--merge] [--recursive] [--] [<path>...]
+   or: $dashless [--quiet] summary [--cached|--files] [--summary-limit <n>] [commit] [--] [<path>...]
+   or: $dashless [--quiet] foreach [--recursive] <command>
+   or: $dashless [--quiet] sync [--] [<path>...]"
 OPTIONS_SPEC=
 . git-sh-setup
+. git-parse-remote
 require_work_tree
 
 command=
 branch=
-quiet=
+force=
+reference=
 cached=
-
-#
-# print stuff on stdout unless -q was specified
-#
-say()
-{
-       if test -z "$quiet"
-       then
-               echo "$@"
-       fi
-}
+recursive=
+init=
+files=
+nofetch=
+update=
+prefix=
 
 # Resolve relative url by appending to parent's url
 resolve_relative_url ()
 {
-       branch="$(git symbolic-ref HEAD 2>/dev/null)"
-       remote="$(git config branch.${branch#refs/heads/}.remote)"
-       remote="${remote:-origin}"
+       remote=$(get_default_remote)
        remoteurl=$(git config "remote.$remote.url") ||
-               die "remote ($remote) does not have a url defined in .git/config"
+               remoteurl=$(pwd) # the repository is its own authoritative upstream
        url="$1"
+       remoteurl=${remoteurl%/}
+       sep=/
        while test -n "$url"
        do
                case "$url" in
                ../*)
                        url="${url#../}"
-                       remoteurl="${remoteurl%/*}"
+                       case "$remoteurl" in
+                       */*)
+                               remoteurl="${remoteurl%/*}"
+                               ;;
+                       *:*)
+                               remoteurl="${remoteurl%:*}"
+                               sep=:
+                               ;;
+                       *)
+                               die "cannot strip one component off url '$remoteurl'"
+                               ;;
+                       esac
                        ;;
                ./*)
                        url="${url#./}"
@@ -50,7 +63,33 @@ resolve_relative_url ()
                        break;;
                esac
        done
-       echo "$remoteurl/$url"
+       echo "$remoteurl$sep${url%/}"
+}
+
+#
+# Get submodule info for registered submodules
+# $@ = path to limit submodule list
+#
+module_list()
+{
+       git ls-files --error-unmatch --stage -- "$@" |
+       perl -e '
+       my %unmerged = ();
+       my ($null_sha1) = ("0" x 40);
+       while (<STDIN>) {
+               chomp;
+               my ($mode, $sha1, $stage, $path) =
+                       /^([0-7]+) ([0-9a-f]{40}) ([0-3])\t(.*)$/;
+               next unless $mode eq "160000";
+               if ($stage ne "0") {
+                       if (!$unmerged{$path}++) {
+                               print "$mode $null_sha1 U\t$path\n";
+                       }
+                       next;
+               }
+               print "$_\n";
+       }
+       '
 }
 
 #
@@ -81,22 +120,19 @@ module_clone()
 {
        path=$1
        url=$2
-
-       # If there already is a directory at the submodule path,
-       # expect it to be empty (since that is the default checkout
-       # action) and try to remove it.
-       # Note: if $path is a symlink to a directory the test will
-       # succeed but the rmdir will fail. We might want to fix this.
-       if test -d "$path"
+       reference="$3"
+       quiet=
+       if test -n "$GIT_QUIET"
        then
-               rmdir "$path" 2>/dev/null ||
-               die "Directory '$path' exist, but is neither empty nor a git repository"
+               quiet=-q
        fi
 
-       test -e "$path" &&
-       die "A file already exist at path '$path'"
-
-       git-clone -n "$url" "$path" ||
+       if test -n "$reference"
+       then
+               git-clone $quiet "$reference" -n "$url" "$path"
+       else
+               git-clone $quiet -n "$url" "$path"
+       fi ||
        die "Clone of '$url' into submodule path '$path' failed"
 }
 
@@ -118,8 +154,20 @@ cmd_add()
                        branch=$2
                        shift
                        ;;
+               -f | --force)
+                       force=$1
+                       ;;
                -q|--quiet)
-                       quiet=1
+                       GIT_QUIET=1
+                       ;;
+               --reference)
+                       case "$2" in '') usage ;; esac
+                       reference="--reference=$2"
+                       shift
+                       ;;
+               --reference=*)
+                       reference="$1"
+                       shift
                        ;;
                --)
                        shift
@@ -138,6 +186,11 @@ cmd_add()
        repo=$1
        path=$2
 
+       if test -z "$path"; then
+               path=$(echo "$repo" |
+                       sed -e 's|/$||' -e 's|:*/*\.git$||' -e 's|.*[/:]||g')
+       fi
+
        if test -z "$repo" -o -z "$path"; then
                usage
        fi
@@ -157,12 +210,29 @@ cmd_add()
        ;;
        esac
 
-       # strip trailing slashes from path
-       path=$(echo "$path" | sed -e 's|/*$||')
-
+       # normalize path:
+       # multiple //; leading ./; /./; /../; trailing /
+       path=$(printf '%s/\n' "$path" |
+               sed -e '
+                       s|//*|/|g
+                       s|^\(\./\)*||
+                       s|/\./|/|g
+                       :start
+                       s|\([^/]*\)/\.\./||
+                       tstart
+                       s|/*$||
+               ')
        git ls-files --error-unmatch "$path" > /dev/null 2>&1 &&
        die "'$path' already exists in the index"
 
+       if test -z "$force" && ! git add --dry-run --ignore-missing "$path" > /dev/null 2>&1
+       then
+               echo >&2 "The following path is ignored by one of your .gitignore files:" &&
+               echo >&2 $path &&
+               echo >&2 "Use -f if you really want to add it."
+               exit 1
+       fi
+
        # perhaps the path exists and is already a git repo, else clone it
        if test -e "$path"
        then
@@ -173,31 +243,86 @@ cmd_add()
                        die "'$path' already exists and is not a valid git repo"
                fi
 
-               case "$repo" in
-               ./*|../*)
-                       url=$(resolve_relative_url "$repo") || exit
-                   ;;
-               *)
-                       url="$repo"
-                       ;;
-               esac
-               git config submodule."$path".url "$url"
        else
 
-               module_clone "$path" "$realrepo" || exit
-               (unset GIT_DIR; cd "$path" && git checkout -q ${branch:+-b "$branch" "origin/$branch"}) ||
-               die "Unable to checkout submodule '$path'"
+               module_clone "$path" "$realrepo" "$reference" || exit
+               (
+                       clear_local_git_env
+                       cd "$path" &&
+                       # ash fails to wordsplit ${branch:+-b "$branch"...}
+                       case "$branch" in
+                       '') git checkout -f -q ;;
+                       ?*) git checkout -f -q -B "$branch" "origin/$branch" ;;
+                       esac
+               ) || die "Unable to checkout submodule '$path'"
        fi
+       git config submodule."$path".url "$realrepo"
 
-       git add "$path" ||
+       git add $force "$path" ||
        die "Failed to add submodule '$path'"
 
        git config -f .gitmodules submodule."$path".path "$path" &&
        git config -f .gitmodules submodule."$path".url "$repo" &&
-       git add .gitmodules ||
+       git add --force .gitmodules ||
        die "Failed to register submodule '$path'"
 }
 
+#
+# Execute an arbitrary command sequence in each checked out
+# submodule
+#
+# $@ = command to execute
+#
+cmd_foreach()
+{
+       # parse $args after "submodule ... foreach".
+       while test $# -ne 0
+       do
+               case "$1" in
+               -q|--quiet)
+                       GIT_QUIET=1
+                       ;;
+               --recursive)
+                       recursive=1
+                       ;;
+               -*)
+                       usage
+                       ;;
+               *)
+                       break
+                       ;;
+               esac
+               shift
+       done
+
+       toplevel=$(pwd)
+
+       # dup stdin so that it can be restored when running the external
+       # command in the subshell (and a recursive call to this function)
+       exec 3<&0
+
+       module_list |
+       while read mode sha1 stage path
+       do
+               if test -e "$path"/.git
+               then
+                       say "Entering '$prefix$path'"
+                       name=$(module_name "$path")
+                       (
+                               prefix="$prefix$path/"
+                               clear_local_git_env
+                               cd "$path" &&
+                               eval "$@" &&
+                               if test -n "$recursive"
+                               then
+                                       cmd_foreach "--recursive" "$@"
+                               fi
+                       ) <&3 3<&- ||
+                       die "Stopping at '$path'; script returned non-zero status."
+               fi
+       done
+}
+
 #
 # Register submodules in .git/config
 #
@@ -210,7 +335,7 @@ cmd_init()
        do
                case "$1" in
                -q|--quiet)
-                       quiet=1
+                       GIT_QUIET=1
                        ;;
                --)
                        shift
@@ -226,27 +351,33 @@ cmd_init()
                shift
        done
 
-       git ls-files --stage -- "$@" | grep '^160000 ' |
+       module_list "$@" |
        while read mode sha1 stage path
        do
                # Skip already registered paths
                name=$(module_name "$path") || exit
-               url=$(git config submodule."$name".url)
-               test -z "$url" || continue
-
-               url=$(git config -f .gitmodules submodule."$name".url)
-               test -z "$url" &&
-               die "No url found for submodule path '$path' in .gitmodules"
-
-               # Possibly a url relative to parent
-               case "$url" in
-               ./*|../*)
-                       url=$(resolve_relative_url "$url") || exit
-                       ;;
-               esac
+               if test -z "$(git config "submodule.$name.url")"
+               then
+                       url=$(git config -f .gitmodules submodule."$name".url)
+                       test -z "$url" &&
+                       die "No url found for submodule path '$path' in .gitmodules"
+
+                       # Possibly a url relative to parent
+                       case "$url" in
+                       ./*|../*)
+                               url=$(resolve_relative_url "$url") || exit
+                               ;;
+                       esac
+                       git config submodule."$name".url "$url" ||
+                       die "Failed to register url for submodule path '$path'"
+               fi
 
-               git config submodule."$name".url "$url" ||
-               die "Failed to register url for submodule path '$path'"
+               # Copy "update" setting when it is not set yet
+               upd="$(git config -f .gitmodules submodule."$name".update)"
+               test -z "$upd" ||
+               test -n "$(git config submodule."$name".update)" ||
+               git config submodule."$name".update "$upd" ||
+               die "Failed to register update mode for submodule path '$path'"
 
                say "Submodule '$name' ($url) registered for path '$path'"
        done
@@ -260,16 +391,39 @@ cmd_init()
 cmd_update()
 {
        # parse $args after "submodule ... update".
+       orig_flags=
        while test $# -ne 0
        do
                case "$1" in
                -q|--quiet)
-                       shift
-                       quiet=1
+                       GIT_QUIET=1
                        ;;
                -i|--init)
+                       init=1
+                       ;;
+               -N|--no-fetch)
+                       nofetch=1
+                       ;;
+               -f|--force)
+                       force=$1
+                       ;;
+               -r|--rebase)
+                       update="rebase"
+                       ;;
+               --reference)
+                       case "$2" in '') usage ;; esac
+                       reference="--reference=$2"
+                       orig_flags="$orig_flags $(git rev-parse --sq-quote "$1")"
                        shift
-                       cmd_init "$@" || return
+                       ;;
+               --reference=*)
+                       reference="$1"
+                       ;;
+               -m|--merge)
+                       update="merge"
+                       ;;
+               --recursive)
+                       recursive=1
                        ;;
                --)
                        shift
@@ -282,47 +436,113 @@ cmd_update()
                        break
                        ;;
                esac
+               orig_flags="$orig_flags $(git rev-parse --sq-quote "$1")"
+               shift
        done
 
-       git ls-files --stage -- "$@" | grep '^160000 ' |
+       if test -n "$init"
+       then
+               cmd_init "--" "$@" || return
+       fi
+
+       cloned_modules=
+       module_list "$@" |
        while read mode sha1 stage path
        do
+               if test "$stage" = U
+               then
+                       echo >&2 "Skipping unmerged submodule $path"
+                       continue
+               fi
                name=$(module_name "$path") || exit
                url=$(git config submodule."$name".url)
+               update_module=$(git config submodule."$name".update)
                if test -z "$url"
                then
                        # Only mention uninitialized submodules when its
                        # path have been specified
                        test "$#" != "0" &&
-                       say "Submodule path '$path' not initialized"
+                       say "Submodule path '$path' not initialized" &&
                        say "Maybe you want to use 'update --init'?"
                        continue
                fi
 
                if ! test -d "$path"/.git -o -f "$path"/.git
                then
-                       module_clone "$path" "$url" || exit
+                       module_clone "$path" "$url" "$reference"|| exit
+                       cloned_modules="$cloned_modules;$name"
                        subsha1=
                else
-                       subsha1=$(unset GIT_DIR; cd "$path" &&
+                       subsha1=$(clear_local_git_env; cd "$path" &&
                                git rev-parse --verify HEAD) ||
                        die "Unable to find current revision in submodule path '$path'"
                fi
 
+               if ! test -z "$update"
+               then
+                       update_module=$update
+               fi
+
                if test "$subsha1" != "$sha1"
                then
-                       (unset GIT_DIR; cd "$path" && git-fetch &&
-                               git-checkout -q "$sha1") ||
-                       die "Unable to checkout '$sha1' in submodule path '$path'"
+                       subforce=$force
+                       # If we don't already have a -f flag and the submodule has never been checked out
+                       if test -z "$subsha1" -a -z "$force"
+                       then
+                               subforce="-f"
+                       fi
 
-                       say "Submodule path '$path': checked out '$sha1'"
+                       if test -z "$nofetch"
+                       then
+                               # Run fetch only if $sha1 isn't present or it
+                               # is not reachable from a ref.
+                               (clear_local_git_env; cd "$path" &&
+                                       ( (rev=$(git rev-list -n 1 $sha1 --not --all 2>/dev/null) &&
+                                        test -z "$rev") || git-fetch)) ||
+                               die "Unable to fetch in submodule path '$path'"
+                       fi
+
+                       # Is this something we just cloned?
+                       case ";$cloned_modules;" in
+                       *";$name;"*)
+                               # then there is no local change to integrate
+                               update_module= ;;
+                       esac
+
+                       case "$update_module" in
+                       rebase)
+                               command="git rebase"
+                               action="rebase"
+                               msg="rebased onto"
+                               ;;
+                       merge)
+                               command="git merge"
+                               action="merge"
+                               msg="merged in"
+                               ;;
+                       *)
+                               command="git checkout $subforce -q"
+                               action="checkout"
+                               msg="checked out"
+                               ;;
+                       esac
+
+                       (clear_local_git_env; cd "$path" && $command "$sha1") ||
+                       die "Unable to $action '$sha1' in submodule path '$path'"
+                       say "Submodule path '$path': $msg '$sha1'"
+               fi
+
+               if test -n "$recursive"
+               then
+                       (clear_local_git_env; cd "$path" && eval cmd_update "$orig_flags") ||
+                       die "Failed to recurse into submodule path '$path'"
                fi
        done
 }
 
 set_name_rev () {
        revname=$( (
-               unset GIT_DIR
+               clear_local_git_env
                cd "$1" && {
                        git describe "$2" 2>/dev/null ||
                        git describe --tags "$2" 2>/dev/null ||
@@ -343,6 +563,7 @@ set_name_rev () {
 cmd_summary() {
        summary_limit=-1
        for_status=
+       diff_cmd=diff-index
 
        # parse $args after "submodule ... summary".
        while test $# -ne 0
@@ -351,6 +572,9 @@ cmd_summary() {
                --cached)
                        cached="$1"
                        ;;
+               --files)
+                       files="$1"
+                       ;;
                --for-status)
                        for_status="$1"
                        ;;
@@ -379,18 +603,31 @@ cmd_summary() {
 
        test $summary_limit = 0 && return
 
-       if rev=$(git rev-parse --verify "$1^0" 2>/dev/null)
+       if rev=$(git rev-parse -q --verify --default HEAD ${1+"$1"})
        then
                head=$rev
-               shift
+               test $# = 0 || shift
+       elif test -z "$1" -o "$1" = "HEAD"
+       then
+               # before the first commit: compare with an empty tree
+               head=$(git hash-object -w -t tree --stdin </dev/null)
+               test -z "$1" || shift
        else
-               head=HEAD
+               head="HEAD"
+       fi
+
+       if [ -n "$files" ]
+       then
+               test -n "$cached" &&
+               die "--cached cannot be used with --files"
+               diff_cmd=diff-files
+               head=
        fi
 
        cd_to_toplevel
        # Get modified modules cared by user
-       modules=$(git diff-index $cached --raw $head -- "$@" |
-               grep -e '^:160000' -e '^:[0-7]* 160000' |
+       modules=$(git $diff_cmd $cached --ignore-submodules=dirty --raw $head -- "$@" |
+               sane_egrep '^:([0-7]* )?160000' |
                while read mod_src mod_dst sha1_src sha1_dst status name
                do
                        # Always show modules deleted or type-changed (blob<->module)
@@ -403,8 +640,8 @@ cmd_summary() {
 
        test -z "$modules" && return
 
-       git diff-index $cached --raw $head -- $modules |
-       grep -e '^:160000' -e '^:[0-7]* 160000' |
+       git $diff_cmd $cached --ignore-submodules=dirty --raw $head -- $modules |
+       sane_egrep '^:([0-7]* )?160000' |
        cut -c2- |
        while read mod_src mod_dst sha1_src sha1_dst status name
        do
@@ -430,11 +667,11 @@ cmd_summary() {
                missing_dst=
 
                test $mod_src = 160000 &&
-               ! GIT_DIR="$name/.git" git-rev-parse --verify $sha1_src^0 >/dev/null 2>&1 &&
+               ! GIT_DIR="$name/.git" git-rev-parse -q --verify $sha1_src^0 >/dev/null &&
                missing_src=t
 
                test $mod_dst = 160000 &&
-               ! GIT_DIR="$name/.git" git-rev-parse --verify $sha1_dst^0 >/dev/null 2>&1 &&
+               ! GIT_DIR="$name/.git" git-rev-parse -q --verify $sha1_dst^0 >/dev/null &&
                missing_dst=t
 
                total_commits=
@@ -461,7 +698,7 @@ cmd_summary() {
                                range=$sha1_dst
                        fi
                        GIT_DIR="$name/.git" \
-                       git log --pretty=oneline --first-parent $range | wc -l
+                       git rev-list --first-parent $range -- | wc -l
                        )
                        total_commits=" ($(($total_commits + 0)))"
                        ;;
@@ -506,7 +743,11 @@ cmd_summary() {
                echo
        done |
        if test -n "$for_status"; then
-               echo "# Modified submodules:"
+               if [ -n "$files" ]; then
+                       echo "# Submodules changed but not updated:"
+               else
+                       echo "# Submodule changes to be committed:"
+               fi
                echo "#"
                sed -e 's|^|# |' -e 's|^# $|#|'
        else
@@ -526,15 +767,19 @@ cmd_summary() {
 cmd_status()
 {
        # parse $args after "submodule ... status".
+       orig_flags=
        while test $# -ne 0
        do
                case "$1" in
                -q|--quiet)
-                       quiet=1
+                       GIT_QUIET=1
                        ;;
                --cached)
                        cached=1
                        ;;
+               --recursive)
+                       recursive=1
+                       ;;
                --)
                        shift
                        break
@@ -546,30 +791,105 @@ cmd_status()
                        break
                        ;;
                esac
+               orig_flags="$orig_flags $(git rev-parse --sq-quote "$1")"
                shift
        done
 
-       git ls-files --stage -- "$@" | grep '^160000 ' |
+       module_list "$@" |
        while read mode sha1 stage path
        do
                name=$(module_name "$path") || exit
                url=$(git config submodule."$name".url)
+               displaypath="$prefix$path"
+               if test "$stage" = U
+               then
+                       say "U$sha1 $displaypath"
+                       continue
+               fi
                if test -z "$url" || ! test -d "$path"/.git -o -f "$path"/.git
                then
-                       say "-$sha1 $path"
+                       say "-$sha1 $displaypath"
                        continue;
                fi
                set_name_rev "$path" "$sha1"
-               if git diff-files --quiet -- "$path"
+               if git diff-files --ignore-submodules=dirty --quiet -- "$path"
                then
-                       say " $sha1 $path$revname"
+                       say " $sha1 $displaypath$revname"
                else
                        if test -z "$cached"
                        then
-                               sha1=$(unset GIT_DIR; cd "$path" && git rev-parse --verify HEAD)
+                               sha1=$(clear_local_git_env; cd "$path" && git rev-parse --verify HEAD)
                                set_name_rev "$path" "$sha1"
                        fi
-                       say "+$sha1 $path$revname"
+                       say "+$sha1 $displaypath$revname"
+               fi
+
+               if test -n "$recursive"
+               then
+                       (
+                               prefix="$displaypath/"
+                               clear_local_git_env
+                               cd "$path" &&
+                               eval cmd_status "$orig_args"
+                       ) ||
+                       die "Failed to recurse into submodule path '$path'"
+               fi
+       done
+}
+#
+# Sync remote urls for submodules
+# This makes the value for remote.$remote.url match the value
+# specified in .gitmodules.
+#
+cmd_sync()
+{
+       while test $# -ne 0
+       do
+               case "$1" in
+               -q|--quiet)
+                       GIT_QUIET=1
+                       shift
+                       ;;
+               --)
+                       shift
+                       break
+                       ;;
+               -*)
+                       usage
+                       ;;
+               *)
+                       break
+                       ;;
+               esac
+       done
+       cd_to_toplevel
+       module_list "$@" |
+       while read mode sha1 stage path
+       do
+               name=$(module_name "$path")
+               url=$(git config -f .gitmodules --get submodule."$name".url)
+
+               # Possibly a url relative to parent
+               case "$url" in
+               ./*|../*)
+                       url=$(resolve_relative_url "$url") || exit
+                       ;;
+               esac
+
+               if git config "submodule.$name.url" >/dev/null 2>/dev/null
+               then
+                       say "Synchronizing submodule url for '$name'"
+                       git config submodule."$name".url "$url"
+
+                       if test -e "$path"/.git
+                       then
+                       (
+                               clear_local_git_env
+                               cd "$path"
+                               remote=$(get_default_remote)
+                               git config remote."$remote".url "$url"
+                       )
+                       fi
                fi
        done
 }
@@ -583,11 +903,11 @@ cmd_status()
 while test $# != 0 && test -z "$command"
 do
        case "$1" in
-       add | init | update | status | summary)
+       add | foreach | init | update | status | summary | sync)
                command=$1
                ;;
        -q|--quiet)
-               quiet=1
+               GIT_QUIET=1
                ;;
        -b|--branch)
                case "$2" in