Subject: Re: [PATCH 2/4] git-prompt.sh: rework of submodule indicator

From: SZEDER Gábor <[email-masked]>
Date: Tue, 31 Jan 2017 19:06:33 +0100

> Rework of the first patch. The prompt now will look like this:
> (+name:master). I tried to considere all suggestions.
> Tests still missing.
> 
> Signed-off-by: Benjamin Fuchs <[email-masked]>
> ---
>  contrib/completion/git-prompt.sh | 49 ++++++++++++++++++++--------------------
>  1 file changed, 24 insertions(+), 25 deletions(-)
> 
> diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh
> index 4c82e7f..c44b9a2 100644
> --- a/contrib/completion/git-prompt.sh
> +++ b/contrib/completion/git-prompt.sh
> @@ -95,8 +95,10 @@
>  # repository level by setting bash.hideIfPwdIgnored to "false".
>  #
>  # If you would like __git_ps1 to indicate that you are in a submodule,
> -# set GIT_PS1_SHOWSUBMODULE. In this case a "sub:" will be added before
> -# the branch name.
> +# set GIT_PS1_SHOWSUBMODULE to a nonempty value. In this case the name
> +# of the submodule will be prepended to the branch name (e.g. module:master).
> +# The name will be prepended by "+" if the currently checked out submodule
> +# commit does not match the SHA-1 found in the index of the containing repository.
>  
>  # check whether printf supports -v
>  __git_printf_supports_v=
> @@ -288,30 +290,27 @@ __git_eread ()
>  	test -r "$f" && read "$@" <"$f"
>  }
>  
> -# __git_is_submodule
> -# Based on:
> -# http://stackoverflow.com/questions/7359204/git-command-line-know-if-in-submodule
> -__git_is_submodule ()
> -{
> -	local git_dir parent_git module_name path
> -	# Find the root of this git repo, then check if its parent dir is also a repo
> -	git_dir="$(git rev-parse --show-toplevel)"
> -	module_name="$(basename "$git_dir")"
> -	parent_git="$(cd "$git_dir/.." && git rev-parse --show-toplevel 2> /dev/null)"
> -	if [[ -n $parent_git ]]; then
> -		# List all the submodule paths for the parent repo
> -		while read path
> -		do
> -			if [[ "$path" != "$module_name" ]]; then continue; fi
> -			if [[ -d "$git_dir/../$path" ]];    then return 0; fi
> -		done < <(cd $parent_git && git submodule --quiet foreach 'echo $path' 2> /dev/null)
> -    fi
> -    return 1
> -}
> -
> +# __git_ps1_submodule
> +#
> +# $1 - git_dir path
> +#
> +# Returns the name of the submodule if we are currently inside one. The name
> +# will be prepended by "+" if the currently checked out submodule commit does
> +# not match the SHA-1 found in the index of the containing repository.
> +# NOTE: git_dir looks like in a ...
> +# - submodule: "GIT_PARENT/.git/modules/PATH_TO_SUBMODULE"
> +# - parent: "GIT_PARENT/.git" (exception "." in .git)
>  __git_ps1_submodule ()
>  {
> -	__git_is_submodule && printf "sub:"
> +	local git_dir="$1"
> +	local submodule_name="$(basename "$git_dir")"
> +	if [ "$submodule_name" != ".git" ] && [ "$submodule_name" != "." ]; then
> +		local parent_top="${git_dir%.git*}"
> +		local submodule_top="${git_dir#*modules}"
> +		local status=""
> +		git diff -C "$parent_top" --no-ext-diff --ignore-submodules=dirty --quiet -- "$submodule_top" 2>/dev/null || status="+"

This 'git diff' has to read the index of the parent repository, right?
That can be potentially very expensive, if the parent repository, and
thus its index, is big.

You might want to provide finer granularity controls and separate the
"$PWD is in a submodule" indicator from the "submodule commit doesn't
match" indicator.  Even if someone is in general interested in the
former, he might have some huge repositories where he would prefer to
disable the latter, because executing 'git diff' would make the prompt
lag.

I'm not sure we need brand new control knobs, though.  Perhaps we can
get away by checking the env and config variables used for the dirty
index and worktree status indicator, after all they, too, are about
whether to run 'git diff' for the prompt in a repository or not.

> +		printf "$status$submodule_name:"
> +	fi
>  }
>  
>  # __git_ps1 accepts 0 or 1 arguments (i.e., format string)
> @@ -545,7 +544,7 @@ __git_ps1 ()
>  
>  	local sub=""
>  	if [ -n "${GIT_PS1_SHOWSUBMODULE}" ]; then
> -		sub="$(__git_ps1_submodule)"
> +		sub="$(__git_ps1_submodule $g)"

In Bash, and in shells in general, the visibility of variables works
differently than in "regular" programming languages:

  - Any variable existing in a caller, even the ones declared as
    'local' in the caller, are visible in all callees.
    
    This means you don't have to pass $g as parameter to
    __git_ps1_submodule(), because you can access it inside that
    function directly.  This has the benefit that there is one less
    place where you can forget to quote a path variable :)

  - If the callee modifies any variable that isn't declared as local
    in the callee, then the caller will see the modified value of that
    variable, unless the callee was invoked in a subshell.

    Here your sole purpose is to set $sub to something like "+sub:",
    which is determined in __git_ps1_submodule().  You don't need the
    command substitution for that, you can set $sub directly in
    __git_ps1_submodule(), sparing the overhead of fork()ing a
    subshell.  That's important for the sake of git users on Windows.

>  	fi
>  	local f="$w$i$s$u"
-- 
2.7.4