From cabf3688477f55014322f8ccb4b0f3f9b9c67106 Mon Sep 17 00:00:00 2001 From: Eric Nielsen Date: Fri, 2 Dec 2016 16:49:45 -0500 Subject: [PATCH] Add git-info module based on the Prezto git module [function](https://github.com/sorin-ionescu/prezto/blob/master/modules/git/functions/git-info) and further refactored and simplified. What changed: - Commit format only in 'detached HEAD' state, so `$(coalesce "%b" "%c")` is not needed, because you only get one of those. - Removed the `added`, `deleted`, `modified`, `renamed` and `unmerged` verbose contexts. - `git status` is only used in verbose mode. - In non-verbose mode, the `untracked` context is not available, and also untracked files are not considered for computing the `dirty` context. Using `git status` or checking for untracked files is [expensive](https://gist.github.com/sindresorhus/3898739). --- modules/git-info/README.md | 98 +++++++++++ modules/git-info/init.zsh | 336 +++++++++++++++++++++++++++++++++++++ 2 files changed, 434 insertions(+) create mode 100644 modules/git-info/README.md create mode 100644 modules/git-info/init.zsh diff --git a/modules/git-info/README.md b/modules/git-info/README.md new file mode 100644 index 0000000..469ff13 --- /dev/null +++ b/modules/git-info/README.md @@ -0,0 +1,98 @@ +Git-info +======== + +Exposes git repository status information to prompts. + +Many thanks to [Sorin Ionescu](https://github.com/sorin-ionescu) and +[Colin Hebert](https://github.com/ColinHebert) for the original code. + +Git **1.7.2** is the +[minimum required version](https://github.com/sorin-ionescu/prezto/issues/219). + +Settings +-------- + +### Ignore Submodules + +Retrieving the status of a repository with submodules can take a long time. +Submodules may be ignored when 'none', 'untracked', 'dirty', or 'all', which is +the default. + + zstyle ':zim:git-info' ignore-submodules 'none' + +### Verbose Mode + +Verbose mode uses `git status` and computes the count of indexed, unindexed and +also untracked files. It can be enabled with the following zstyle: + + zstyle ':zim:git-info' verbose 'yes' + +In non-verbose mode, the 'untracked' context is not available (see Main +Contexts below), and also untracked files are not considered for computing the +'dirty' context. Using `git status` or checking for untracked files is +[expensive](https://gist.github.com/sindresorhus/3898739). + +Theming +------- + +To display information about the current repository in a prompt, define the +following styles in the `prompt_name_setup` function, where the syntax for +setting a style is: + + zstyle ':zim:git-info:context' format 'string' + +### Main Contexts + +| Name | Code | Description +| --------- | :----: | -------------------------------------------------------- +| action | %s | Special action name (see Special Action Contexts below) +| ahead | %A | Commits ahead of remote count +| behind | %B | Commits behind of remote count +| branch | %b | Branch name +| commit | %c | Commit short hash (when in 'detached HEAD' state) +| clean | %C | Clean state +| dirty | %D | Dirty state (count with untracked files if verbose enabled) +| indexed | %i | Indexed files (count if verbose enabled) +| unindexed | %I | Unindexed files (count if verbose enabled) +| position | %p | Commits from nearest tag count (when in 'detached HEAD' state) +| remote | %R | Remote name +| stashed | %S | Stashed states count +| untracked | %u | Untracked files count (only if verbose enabled) + +### Untracked Contexts + +| Name | Format | Default Value +| --------------------------- | :-----: | ------------------------------------- +| action:apply | value | 'apply' +| action:bisect | value | 'bisect' +| action:cherry-pick | value | 'cherry-pick' +| action:cherry-pick-sequence | value | 'cherry-pick-sequence' +| action:merge | value | 'merge' +| action:rebase | value | 'rebase' +| action:rebase-interactive | value | 'rebase-interactive' +| action:rebase-merge | value | 'rebase-merge' + +Formatting example for special actions: + + zstyle ':zim:git-info:action:bisect' format '' + zstyle ':zim:git-info:action:merge' format '>M<' + zstyle ':zim:git-info:action:rebase' format '>R>' + +### Usage + +First, format the repository state attributes. For example, to format the +branch name, commit, and remote name, define the following styles: + + zstyle ':zim:git-info:branch' format 'branch:%b' + zstyle ':zim:git-info:commit' format 'commit:%c' + zstyle ':zim:git-info:remote' format 'remote:%R' + +Second, format how the above attributes are displayed in prompts: + + zstyle ':zim:git-info:keys' format \ + 'prompt' 'git(%b%c)' \ + 'rprompt' '[%R]' + +Last, add `$git_info[prompt]` to `$PROMPT` and `$git_info[rprompt]` to +`$RPROMPT` respectively and call `git-info` in the `prompt_name_preexec` hook +function. diff --git a/modules/git-info/init.zsh b/modules/git-info/init.zsh new file mode 100644 index 0000000..1b4d16c --- /dev/null +++ b/modules/git-info/init.zsh @@ -0,0 +1,336 @@ +# vim: et ts=2 sts=2 sw=2 ft=zsh +# +# Exposes Git repository information via the git_info associative array. +# + +# Gets the Git special action (am, bisect, cherry, merge, rebase). +# Borrowed from vcs_info and edited. +function _git-action { + local git_dir=$(git-dir) + local action_dir + for action_dir in \ + "${git_dir}/rebase-apply" \ + "${git_dir}/rebase" \ + "${git_dir}/../.dotest" + do + if [[ -d ${action_dir} ]]; then + local apply_formatted + local rebase_formatted + zstyle -s ':zim:git-info:action:apply' format 'apply_formatted' || apply_formatted='apply' + zstyle -s ':zim:git-info:action:rebase' format 'rebase_formatted' || rebase_formatted='rebase' + + if [[ -f "${action_dir}/rebasing" ]]; then + print ${rebase_formatted} + elif [[ -f "${action_dir}/applying" ]]; then + print ${apply_formatted} + else + print "${rebase_formatted}/${apply_formatted}" + fi + + return 0 + fi + done + + for action_dir in \ + "${git_dir}/rebase-merge/interactive" \ + "${git_dir}/.dotest-merge/interactive" + do + if [[ -f ${action_dir} ]]; then + local rebase_interactive_formatted + zstyle -s ':zim:git-info:action:rebase-interactive' format 'rebase_interactive_formatted' || rebase_interactive_formatted='rebase-interactive' + print ${rebase_interactive_formatted} + return 0 + fi + done + + for action_dir in \ + "${git_dir}/rebase-merge" \ + "${git_dir}/.dotest-merge" + do + if [[ -d ${action_dir} ]]; then + local rebase_merge_formatted + zstyle -s ':zim:git-info:action:rebase-merge' format 'rebase_merge_formatted' || rebase_merge_formatted='rebase-merge' + print ${rebase_merge_formatted} + return 0 + fi + done + + if [[ -f "${git_dir}/MERGE_HEAD" ]]; then + local merge_formatted + zstyle -s ':zim:git-info:action:merge' format 'merge_formatted' || merge_formatted='merge' + print ${merge_formatted} + return 0 + fi + + if [[ -f "${git_dir}/CHERRY_PICK_HEAD" ]]; then + if [[ -d "${git_dir}/sequencer" ]]; then + local cherry_pick_sequence_formatted + zstyle -s ':zim:git-info:action:cherry-pick-sequence' format 'cherry_pick_sequence_formatted' || cherry_pick_sequence_formatted='cherry-pick-sequence' + print ${cherry_pick_sequence_formatted} + else + local cherry_pick_formatted + zstyle -s ':zim:git-info:action:cherry-pick' format 'cherry_pick_formatted' || cherry_pick_formatted='cherry-pick' + print ${cherry_pick_formatted} + fi + + return 0 + fi + + if [[ -f "${git_dir}/BISECT_LOG" ]]; then + local bisect_formatted + zstyle -s ':zim:git-info:action:bisect' format 'bisect_formatted' || bisect_formatted='bisect' + print ${bisect_formatted} + return 0 + fi + + return 1 +} + +# Gets the Git status information. +function git-info { + # Extended globbing is needed to parse repository status. + setopt LOCAL_OPTIONS EXTENDED_GLOB + + # Clean up previous git_info. + unset git_info + typeset -gA git_info + + # Return if not inside a Git repository work tree. + if ! is-true $(git rev-parse --is-inside-work-tree 2>/dev/null); then + return 1 + fi + + if (( $# )); then + if [[ $1 == [Oo][Nn] ]]; then + git config --bool prompt.showinfo true + elif [[ $1 == [Oo][Ff][Ff] ]]; then + git config --bool prompt.showinfo false + else + print "usage: $0 [ on | off ]" >&2 + fi + return 0 + fi + + # Return if git-info is disabled. + if ! is-true ${$(git config --bool prompt.showinfo):-true}; then + return 1 + fi + + # Ignore submodule status. + local ignore_submodules + zstyle -s ':zim:git-info' ignore-submodules 'ignore_submodules' || ignore_submodules='all' + + # Format stashed. + local stashed_format + local stashed_formatted + zstyle -s ':zim:git-info:stashed' format 'stashed_format' + if [[ -n ${stashed_format} && -f "$(git-dir)/refs/stash" ]]; then + local stashed + (( stashed=$(git stash list 2>/dev/null | wc -l) )) + (( stashed )) && zformat -f stashed_formatted ${stashed_format} "S:${stashed}" + fi + + # Format action. + local action_format + local action_formatted + zstyle -s ':zim:git-info:action' format 'action_format' + if [[ -n ${action_format} ]]; then + local action=$(_git-action) + if [[ -n ${action} ]]; then + zformat -f action_formatted ${action_format} "s:${action}" + fi + fi + + # Get the branch. + local branch=$(git symbolic-ref -q --short HEAD 2>/dev/null) + + local ahead_formatted + local behind_formatted + local branch_formatted + local commit_formatted + local position_formatted + local remote_formatted + if [[ -n ${branch} ]]; then + # Format branch. + local branch_format + zstyle -s ':zim:git-info:branch' format 'branch_format' + if [[ -n ${branch_format} ]]; then + zformat -f branch_formatted ${branch_format} "b:${branch}" + fi + + # Format remote. + local remote_format + zstyle -s ':zim:git-info:remote' format 'remote_format' + if [[ -n ${remote_format} ]]; then + # Gets the remote name. + local remote_cmd='git rev-parse --symbolic-full-name --verify HEAD@{upstream}' + local remote=${$(${(z)remote_cmd} 2>/dev/null)##refs/remotes/} + if [[ -n ${remote} ]]; then + zformat -f remote_formatted ${remote_format} "R:${remote}" + fi + fi + + local ahead_format + local behind_format + zstyle -s ':zim:git-info:ahead' format 'ahead_format' + zstyle -s ':zim:git-info:behind' format 'behind_format' + if [[ -n ${ahead_format} || -n ${behind_format} ]]; then + # Gets the commit difference counts between local and remote. + local ahead_and_behind_cmd='git rev-list --count --left-right HEAD...@{upstream}' + + # Get ahead and behind counts. + local ahead_and_behind=$(${(z)ahead_and_behind_cmd} 2>/dev/null) + + # Format ahead. + if [[ -n ${ahead_format} ]]; then + local ahead=${ahead_and_behind[(w)1]} + (( ahead )) && zformat -f ahead_formatted ${ahead_format} "A:${ahead}" + fi + + # Format behind. + if [[ -n ${behind_format} ]]; then + local behind=${ahead_and_behind[(w)2]} + (( behind )) && zformat -f behind_formatted ${behind_format} "B:${behind}" + fi + fi + else + # Format commit. + local commit_format + zstyle -s ':zim:git-info:commit' format 'commit_format' + if [[ -n ${commit_format} ]]; then + local commit=$(git rev-parse --short HEAD 2>/dev/null) + if [[ -n ${commit} ]]; then + zformat -f commit_formatted ${commit_format} "c:${commit}" + fi + fi + + # Format position. + local position_format + zstyle -s ':zim:git-info:position' format 'position_format' + if [[ -n ${position_format} ]]; then + local position=$(git describe --contains --all HEAD 2>/dev/null) + if [[ -n ${position} ]]; then + zformat -f position_formatted ${position_format} "p:${position}" + fi + fi + fi + + # Dirty and clean format. + local dirty_format + local dirty_formatted + local clean_format + local clean_formatted + zstyle -s ':zim:git-info:dirty' format 'dirty_format' + zstyle -s ':zim:git-info:clean' format 'clean_format' + + local dirty + local indexed + local indexed_formatted + local unindexed + local unindexed_formatted + local untracked_formatted + if ! zstyle -t ':zim:git-info' verbose; then + # Format unindexed. + local unindexed_format + zstyle -s ':zim:git-info:unindexed' format 'unindexed_format' + if [[ -n ${unindexed_format} || -n ${dirty_format} || -n ${clean_format} ]]; then + (git diff-files --no-ext-diff --quiet --ignore-submodules=${ignore_submodules} 2>/dev/null) + unindexed=$? + if (( unindexed )); then + unindexed_formatted=${unindexed_format} + dirty=${unindexed} + fi + fi + + # Format indexed. + local indexed_format + zstyle -s ':zim:git-info:indexed' format 'indexed_format' + if [[ -n ${indexed_format} || (${dirty} -eq 0 && (-n ${dirty_format} || -n ${clean_format})) ]]; then + (git diff-index --no-ext-diff --quiet --cached --ignore-submodules=${ignore_submodules} HEAD 2>/dev/null) + indexed=$? + if (( indexed )); then + indexed_formatted=${indexed_format} + dirty=${indexed} + fi + fi + + # Format dirty and clean. + if (( dirty )); then + dirty_formatted=${dirty_format} + else + clean_formatted=${clean_format} + fi + else + # Use porcelain status for easy parsing. + local status_cmd="git status --porcelain --ignore-submodules=${ignore_submodules}" + + local untracked + # Get current status. + while IFS=$'\n' read line; do + # T (type change) is undocumented, see http://git.io/FnpMGw. + if [[ ${line} == \?\?\ * ]]; then + (( untracked++ )) + elif [[ ${line} == \ [DMT]\ * ]]; then + (( unindexed++ )) + elif [[ ${line} == [ACDMRT]\ \ * ]]; then + (( indexed++ )) + elif [[ ${line} == ([ACMRT][DMT]|D[MT])\ * ]]; then + (( indexed++, unindexed++ )) + fi + (( dirty++ )) + done < <(${(z)status_cmd} 2>/dev/null) + + # Format indexed. + if (( indexed )); then + local indexed_format + zstyle -s ':zim:git-info:indexed' format 'indexed_format' + zformat -f indexed_formatted ${indexed_format} "i:${indexed}" + fi + + # Format unindexed. + if (( unindexed )); then + local unindexed_format + zstyle -s ':zim:git-info:unindexed' format 'unindexed_format' + zformat -f unindexed_formatted ${unindexed_format} "I:${unindexed}" + fi + + # Format untracked. + if (( untracked )); then + local untracked_format + zstyle -s ':zim:git-info:untracked' format 'untracked_format' + zformat -f untracked_formatted ${untracked_format} "u:${untracked}" + fi + + # Format dirty and clean. + if (( dirty )); then + zformat -f dirty_formatted ${dirty_format} "u:${dirty}" + else + clean_formatted=${clean_format} + fi + fi + + # Format info. + local info_format + local -A info_formats + local reply + zstyle -a ':zim:git-info:keys' format 'info_formats' + for info_format in ${(k)info_formats}; do + zformat -f reply "${info_formats[${info_format}]}" \ + "A:${ahead_formatted}" \ + "B:${behind_formatted}" \ + "b:${branch_formatted}" \ + "C:${clean_formatted}" \ + "c:${commit_formatted}" \ + "D:${dirty_formatted}" \ + "i:${indexed_formatted}" \ + "I:${unindexed_formatted}" \ + "p:${position_formatted}" \ + "R:${remote_formatted}" \ + "s:${action_formatted}" \ + "S:${stashed_formatted}" \ + "u:${untracked_formatted}" + git_info[${info_format}]=${reply} + done + + return 0 +}