Le prompt Bash qui change la vie avec Git


Lors de l’atelier de samedi je vous ai promis un prompt Bash qui déboîte quand on fait du Git.

Celui-ci repose sur une série de fonctions Bash dont la plus « high-level » est appelée à chaque prompt. Elle permet de voir immédiatement :

  • Dans quelle branche on est
  • Si on a des modifs en working tree
  • Si on a des modifs en stage
  • Si on a des fichiers untracked
  • Si on a du stash

Toutes choses fort utiles au moment de faire son commit, histoire de ne rien oublier…

Voici le code source avec quelques commentaires. Vous pouvez aussi aller piocher le Gist directement.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# Scavenged from Git 1.6.5.x contrib/completion/git_completion.bash
# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
# returns text to add to bash PS1 prompt (includes branch name)
__gitdir ()
{
  if [ -z "${1-}" ]; then
    if [ -n "${__git_dir-}" ]; then
      echo "$__git_dir"
    elif [ -d .git ]; then
      echo .git
    else
      git rev-parse --git-dir 2>/dev/null
    fi
  elif [ -d "$1/.git" ]; then
    echo "$1/.git"
  else
    echo "$1"
  fi
}
__git_ps1 ()
{
  local g="$(__gitdir)"
  if [ -n "$g" ]; then
    local r
    local b
    if [ -f "$g/rebase-merge/interactive" ]; then
      r="|REBASE-i"
      b="$(cat "$g/rebase-merge/head-name")"
    elif [ -d "$g/rebase-merge" ]; then
      r="|REBASE-m"
      b="$(cat "$g/rebase-merge/head-name")"
    else
      if [ -d "$g/rebase-apply" ]; then
        if [ -f "$g/rebase-apply/rebasing" ]; then
          r="|REBASE"
        elif [ -f "$g/rebase-apply/applying" ]; then
          r="|AM"
        else
          r="|AM/REBASE"
        fi
      elif [ -f "$g/MERGE_HEAD" ]; then
        r="|MERGING"
      elif [ -f "$g/BISECT_LOG" ]; then
        r="|BISECTING"
      fi

      b="$(git symbolic-ref HEAD 2>/dev/null)" || {

        b="$(
        case "
${GIT_PS1_DESCRIBE_STYLE-}" in
        (contains)
          git describe --contains HEAD ;;
        (branch)
          git describe --contains --all HEAD ;;
        (describe)
          git describe HEAD ;;
        (* | default)
          git describe --exact-match HEAD ;;
        esac 2>/dev/null)"
||

        b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
        b="unknown"
        b="($b)"
      }
    fi

    local w
    local i
    local s
    local u
    local c

    if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
      if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
        c="BARE:"
      else
        b="GIT_DIR!"
      fi
    elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
      if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ]; then
        if [ "$(git config --bool bash.showDirtyState)" != "false" ]; then
          git diff --no-ext-diff --ignore-submodules \
            --quiet --exit-code || w="*"
          if git rev-parse --quiet --verify HEAD >/dev/null; then
            git diff-index --cached --quiet \
              --ignore-submodules HEAD -- || i="+"
          else
            i="#"
          fi
        fi
      fi
      if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
              git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
      fi

      if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
         if [ -n "$(git ls-files --others --exclude-standard)" ]; then
            u="%"
         fi
      fi
    fi

    if [ -n "${1-}" ]; then
      printf "$1" "$c${b##refs/heads/}$w$i$s$u$r"
    else
      printf " (%s)" "$c${b##refs/heads/}$w$i$s$u$r"
    fi
  fi
}

export GIT_PS1_SHOWDIRTYSTATE=1 GIT_PS1_SHOWUNTRACKEDFILES=1 GIT_PS1_SHOWSTASHSTATE=1

# Basique…
# export PS1='\u@\h:\W$(__git_ps1) \$ '
# Avec plein de couleurs…
export PS1='\[\033[0;37m\]\u@\h:\[\033[0;33m\]\W\[\033[0m\]\[\033[1;32m\]$(__git_ps1)\[\033[0m\] \$ '

Notez les variables qui configurent ce qui est affiché :

  • GIT_PS1_SHOWDIRTYSTATE fait remonter les modifs en working tree (symbole *) et en stage (symbole +)
  • GIT_PS1_SHOWUNTRACKED_FILES fait remonter les fichiers non présents dans l’index (non pris en compte jusqu’à ajout explicite) (symbole %)
  • GIT_PS1_SHOWSTASHSTATE indique qu’on a des trucs dans le stash (symbole $)

Ça ralentit un tout petit peu le premier prompt de votre session dans un dépôt non trivial, le temps pour les commandes Git internes de construire les caches d’accès à l’index et à l’historique… Ensuite c’est instantané.

Je ne pourrais pas vivre sans !

Enjoy!

Faites passer :
  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Google Buzz
  • Netvibes
  • Reddit
  • Tumblr
  • viadeo FR
  • Wikio FR
  1. #1 by Sunny on 07/19/2010 - 14:49

    Malheureusement sur de gros dépôts au bureau le prompt mets longtemps à se construire la première fois mais pas loin de 2 secondes à s’afficher les fois suivante… (Avec des volumes montés sur le réseau.)

    Mais en désactivant GIT_PS1_SHOWDIRTYSTATE GIT_PS1_SHOWUNTRACKEDFILES ça va bien mieux.

    [WORDPRESS HASHCASH] The poster sent us ’0 which is not a hashcash value.

    • #2 by Christophe on 07/23/2010 - 22:21

      Sunny: j’ai des dépôts de Gros Bill™, et si le prompt toutes-options met en effet quelques secondes à se construire au départ, il est instantané ensuite. Je pense que le fautif ici est ton montage réseau, qui doit beaucoup ralentir le parsing interne de .git… :-/

  2. #3 by Kaelig on 07/23/2010 - 10:09

    Merci Christophe, précisons que c’est à coller dans .bash_profile (par ex).

    [WORDPRESS HASHCASH] The poster sent us ’0 which is not a hashcash value.

    • #4 by Christophe on 07/23/2010 - 22:21

      En effet. Our .profile, voire .bashrc suivant la config en vigueur.

(ne sera pas publié)