# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 ##@cooked nocomments ##@require nulib.sh ##@require base module: git "" "Fonctions pour faciliter l'utilisation de git" require: nulib base function: git_geturl "" function git_geturl() { git config --get remote.origin.url } function: git_have_annex "" function git_have_annex() { [ -n "$(git config --get annex.uuid)" ] } NULIB_GIT_FUNCTIONS=( git_check_gitvcs git_ensure_gitvcs git_list_branches git_list_rbranches git_have_branch git_have_rbranch git_get_branch git_is_branch git_have_remote git_track_branch git_check_cleancheckout git_ensure_cleancheckout git_is_ancestor git_should_ff git_should_push git_is_merged ) NULIB_GIT_FUNCTIONS_MAP=( cg:git_check_gitvcs eg:git_ensure_gitvcs lbs:git_list_branches rbs:git_list_rbranches hlb:git_have_branch hrb:git_have_rbranch gb:git_get_branch ib:git_is_branch hr:git_have_remote tb:git_track_branch cc:git_check_cleancheckout ec:git_ensure_cleancheckout ia:git_is_ancestor sff:git_should_ff spu:git_should_push im:git_is_merged ) function: git_check_gitvcs "" function git_check_gitvcs() { git rev-parse --show-toplevel >&/dev/null } function: git_ensure_gitvcs "" function git_ensure_gitvcs() { git_check_gitvcs || edie "Ce n'est pas un dépôt git" || return } function: git_list_branches "" function git_list_branches() { git for-each-ref refs/heads/ --format='%(refname:short)' | csort } function: git_list_rbranches "" function git_list_rbranches() { git for-each-ref "refs/remotes/${1:-origin}/" --format='%(refname:short)' | csort } function: git_list_pbranches "lister les branches locales et celles qui existent dans l'origine \$1(=origin) et qui pourraient devenir une branche locale avec la commande git checkout -b" function git_list_pbranches() { local prefix="${1:-origin}/" { git for-each-ref refs/heads/ --format='%(refname:short)' git for-each-ref "refs/remotes/$prefix" --format='%(refname:short)' | grep -F "$prefix" | cut -c $((${#prefix} + 1))- } | grep -vF HEAD | csort -u } function: git_have_branch "" function git_have_branch() { git_list_branches | grep -qF "$1" } function: git_have_rbranch "" function git_have_rbranch() { git_list_rbranches "${2:-origin}" | grep -qF "$1" } function: git_get_branch "" function git_get_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null } function: git_get_branch_remote "" function git_get_branch_remote() { local branch="$1" [ -n "$branch" ] || branch="$(git_get_branch)" [ -n "$branch" ] || return git config --get "branch.$branch.remote" } function: git_get_branch_merge "" function git_get_branch_merge() { local branch="$1" [ -n "$branch" ] || branch="$(git_get_branch)" [ -n "$branch" ] || return git config --get "branch.$branch.merge" } function: git_get_branch_rbranch "" function git_get_branch_rbranch() { local branch="$1" remote="$2" merge [ -n "$branch" ] || branch="$(git_get_branch)" [ -n "$branch" ] || return [ -n "$remote" ] || remote="$(git_get_branch_remote "$branch")" [ -n "$remote" ] || return merge="$(git_get_branch_merge "$branch")" [ -n "$merge" ] || return echo "refs/remotes/$remote/${merge#refs/heads/}" } function: git_is_branch "" function git_is_branch() { [ "$(git_get_branch)" == "${1:-master}" ] } function: git_have_remote "" function git_have_remote() { [ -n "$(git config --get remote.${1:-origin}.url)" ] } function: git_track_branch "" function git_track_branch() { local branch="$1" origin="${2:-origin}" [ -n "$branch" ] || return git_have_remote "$origin" || return [ "$(git config --get branch.$branch.remote)" == "$origin" ] && return if git_have_rbranch "$branch" "$origin"; then if git_have_branch "$branch"; then git branch -u "$origin/$branch" "$branch" else git branch -t "$branch" "$origin/$branch" fi elif git_have_branch "$branch"; then git push -u "$origin" "$branch" || return fi } function: git_ensure_branch " @return 0 si la branche a été créée, 1 si elle existait déjà, 2 en cas d'erreur" function git_ensure_branch() { local branch="$1" source="${2:-master}" origin="${3:-origin}" [ -n "$branch" ] || return 2 git_have_branch "$branch" && return 1 if git_have_rbranch "$branch" "$origin"; then # une branche du même nom existe dans l'origine. faire une copie de cette branche git branch -t "$branch" "$origin/$branch" || return 2 else # créer une nouvelle branche du nom spécifié git_have_branch "$source" || return 2 git branch "$branch" "$source" || return 2 if [ -z "$NULIB_GIT_OFFLINE" ]; then git_have_remote "$origin" && git_track_branch "$branch" "$origin" fi fi return 0 } function: git_check_cleancheckout "vérifier qu'il n'y a pas de modification locales dans le dépôt correspondant au répertoire courant." function git_check_cleancheckout() { [ -z "$(git status --porcelain 2>/dev/null)" ] } function: git_ensure_cleancheckout "" function git_ensure_cleancheckout() { git_check_cleancheckout || edie "Vous avez des modifications locales. Enregistrez ces modifications avant de continuer" || return } function git__init_ff() { o="${3:-origin}" b="$1" s="${2:-refs/remotes/$o/$1}" b="$(git rev-parse --verify --quiet "$b")" || return 1 s="$(git rev-parse --verify --quiet "$s")" || return 1 return 0 } function git__can_ff() { [ "$1" == "$(git merge-base "$1" "$2")" ] } function: git_is_ancestor "vérifier que la branche \$1 est un ancêtre direct de la branche \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1 note: cette fonction retourne vrai si \$1 et \$2 identifient le même commit" function git_is_ancestor() { local o b s; git__init_ff "$@" || return git__can_ff "$b" "$s" } function: git_should_ff "vérifier si la branche \$1 devrait être fast-forwardée à partir de la branche d'origine \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1 note: cette fonction est similaire à git_is_ancestor(), mais retourne false si \$1 et \$2 identifient le même commit" function git_should_ff() { local o b s; git__init_ff "$@" || return [ "$b" != "$s" ] || return 1 git__can_ff "$b" "$s" } function: git_should_push "vérifier si la branche \$1 devrait être poussée vers la branche de même nom dans l'origine \$2(=origin), parce que l'origin peut-être fast-forwardée à partir de cette branche." function git_should_push() { git_should_ff "refs/remotes/${2:-origin}/$1" "$1" } function: git_fast_forward "vérifier que la branche courante est bien \$1, puis tester s'il faut la fast-forwarder à partir de la branche d'origine \$2, puis le faire si c'est nécessaire. la branche d'origine \$2 vaut par défaut refs/remotes/origin/\$1" function git_fast_forward() { local o b s; git__init_ff "$@" || return [ "$b" != "$s" ] || return 1 local head="$(git rev-parse HEAD)" [ "$head" == "$b" ] || return 1 git__can_ff "$b" "$s" || return 1 git merge --ff-only "$s" } function: git_is_merged "vérifier que les branches \$1 et \$2 ont un ancêtre commun, et que la branche \$1 a été complètement fusionnée dans la branche destination \$2" function git_is_merged() { local b="$1" d="$2" b="$(git rev-parse --verify --quiet "$b")" || return 1 d="$(git rev-parse --verify --quiet "$d")" || return 1 [ -n "$(git merge-base "$b" "$d")" ] || return 1 [ -z "$(git rev-list "$d..$b")" ] } ################################################################################ # git annex NULIB_GIT_SSH_WRAPPER= function: git_annex_use_ssh_wrapper "" function git_annex_use_ssh_wrapper() { [ -n "$NULIB_GIT_SSH_WRAPPER" ] && return NULIB_GIT_FORCE_PATH="$PATH" NULIB_GIT_FORCE_SSH="${GIT_SSH:-ssh}" export NULIB_GIT_FORCE_PATH NULIB_GIT_FORCE_SSH base_delpath "$NULIBDIR/ssh-wrapper" NULIB_GIT_FORCE_PATH base_inspath "$NULIBDIR/ssh-wrapper" PATH NULIB_GIT_SSH_WRAPPER=1 } function: git_annex_initial "sur le dépôt \$1 fraichement cloné, vérifier s'il faut faire git annex init. Si oui, l'initialiser avec le nom d'hôte, et récupérer tous les fichiers annexés @return 1 si une erreur s'est produite" function git_annex_initial() { local repodir="${1:-.}" [ -d "$repodir" ] || return 1 repodir="$(abspath "$repodir")" local GIT_DIR GIT_WORK_TREE [ "$(cd "$repodir"; git rev-parse --is-bare-repository)" == false ] || return 0 [ -n "$(GIT_DIR="$repodir/.git" git config --get annex.uuid)" ] && return 0 # ici, on sait que git annex n'a pas encore été configuré # vérifier s'il existe des fichiers annexés local -a links base_array_splitl links "$(find "$repodir" -type l)" local link hasannex= for link in "${links[@]}"; do link="$(readlink "$link")" if [ "${link#.git/annex/}" != "$link" ]; then hasannex=1 break elif [[ "$link" == */.git/annex/* ]]; then hasannex=1 break fi done if [ -n "$hasannex" ]; then base_in_path git-annex || edie "Vous devez installer git-annex" || return local cwd; base_push_cwd "$repodir" && git annex init "$MYHOSTNAME" && git annex get && git annex sync && base_pop_cwd || base_pop_cwd 1 || return fi } ################################################################################ # Outils de haut niveau function: git_commit "" function git_commit() { local all=auto allnew push=auto nopush args setyesval nopush "$NULIB_GIT_OFFLINE" [ -n "$nopush" ] && push= parse_opts + "${PRETTYOPTS[@]}" \ -a,--all all=1 \ -A,--all-new allnew=1 \ -c,--cached all= \ -p,--push push=1 \ -l,--local push= \ @ args -- "$@" && set -- "${args[@]}" || { eerror "$args" return 1 } if [ -n "$allnew" ]; then git add -A all= fi local message="$1"; shift local -a cmd cmd=(git commit) [ -n "$message" ] && cmd=("${cmd[@]}" -m "$message") if [ "$all" == "auto" ]; then # Si des fichiers sont spécifiés, prendre ceux-là. if [ -z "$*" ]; then # Sinon, s'il y a des fichiers dans l'index, commiter uniquement ces # fichiers # Sinon, committer tous les fichiers modifiés # le code suivant retourne vrai si l'index contient au moins fichier git status --porcelain 2>/dev/null | lawk ' BEGIN { ec = 1 } substr($0, 1, 1) ~ /[^ ?]/ { ec = 0; exit } END { exit ec }' || cmd=("${cmd[@]}" -a) fi else [ -n "$all" ] && cmd=("${cmd[@]}" -a) fi if ! "${cmd[@]}" "$@"; then [ "$push" == auto ] && return 1 fi if [ "$push" == auto ]; then git_push --auto || return elif [ -n "$push" ]; then git_push --force || return fi return 0 } function: git_update "" function git_update() { local args autoff=1 parse_opts + "${PRETTYOPTS[@]}" \ -n,--no-autoff autoff= \ @ args -- "$@" && set -- "${args[@]}" || { eerror "$args" return 1 } if [ -z "$autoff" ]; then git pull "$@" return $? fi local branch orig_branch restore_branch remote rbranch pbranch local -a branches prbranches crbranches dbranches base_array_splitl prbranches "$(git_list_rbranches)" git fetch -p "$@" || return base_array_splitl crbranches "$(git_list_rbranches)" # vérifier s'il n'y a pas des branches distantes qui ont été supprimées for branch in "${prbranches[@]}"; do if ! base_array_contains crbranches "$branch"; then base_array_add dbranches "${branch#*/}" fi done if [ ${#dbranches[*]} -gt 0 ]; then eimportant "One or more distant branches where deleted" for branch in "${dbranches[@]}"; do if git_have_branch "$branch"; then if ! ask_yesno "Do you want to delete local branch $branch?" X; then base_array_del dbranches "$branch" fi fi done fi if [ ${#dbranches[*]} -gt 0 ]; then base_array_splitl branches "$(git_list_branches)" branch="$(git_get_branch)" if base_array_contains dbranches "$branch"; then # si la branche courante est l'une des branches à supprimer, il faut # basculer vers develop ou master local swto if [ -z "$swto" ] && base_array_contains branches develop && ! base_array_contains dbranches develop; then swto=develop fi if [ -z "$swto" ] && base_array_contains branches master && ! base_array_contains dbranches master; then swto=master fi if ! git_check_cleancheckout; then echo "* There are uncommitted local changes. However current branch is slated for removal. Make your verifications then delete the local branches: ${swto:+$(qvals git checkout "$swto") }$(qvals git branch -D "${dbranches[@]}")" return 1 fi if [ -n "$swto" ]; then git checkout -q "$swto" else echo "* Current branch is slated for removal but I don't know to which branch I should switch first. Make your choice then delete the local branches: $(qvals git branch -D "${dbranches[@]}")" return 1 fi fi for branch in "${dbranches[@]}"; do git branch -D "$branch" done fi # intégrer les modifications dans les branches locales if ! git_check_cleancheckout; then branch="$(git_get_branch)" remote="$(git_get_branch_remote "$branch")" rbranch="$(git_get_branch_rbranch "$branch" "$remote")" pbranch="${rbranch#refs/remotes/}" if git merge -q --ff-only "$rbranch"; then echo "* There are uncommitted local changes: only CURRENT branch were updated" fi return 0 fi orig_branch="$(git_get_branch)" base_array_splitl branches "$(git_list_branches)" for branch in "${branches[@]}"; do remote="$(git_get_branch_remote "$branch")" rbranch="$(git_get_branch_rbranch "$branch" "$remote")" pbranch="${rbranch#refs/remotes/}" [ -n "$remote" -a -n "$rbranch" ] || continue if git_is_ancestor "$branch" "$rbranch"; then if git_should_ff "$branch" "$rbranch"; then echo "* Fast-forwarding $branch -> $pbranch" git checkout -q "$branch" git merge -q --ff-only "$rbranch" restore_branch=1 fi else if [ "$branch" == "$orig_branch" ]; then echo "* Cannot fast-forward CURRENT branch $branch from $pbranch Try to merge manually with: git merge $pbranch" else echo "* Cannot fast-forward local branch $branch from $pbranch You can merge manually with: git checkout $branch; git merge $pbranch" fi fi done [ -n "$restore_branch" ] && git checkout -q "$orig_branch" return 0 } function: git_push "" function git_push() { local all all_branches all_tags auto force args no_annex parse_opts + "${PRETTYOPTS[@]}" \ -a,--all all=1 \ -b,--branches,--all-branches all_branches=1 \ -t,--tags,--all-tags all_tags=1 \ --auto auto=1 \ -f,--force force=1 \ -n,--no-annex no_annex=1 \ @ args -- "$@" && set -- "${args[@]}" || { eerror "$args" return 1 } if [ -n "$all" ]; then # On a demandé à pusher toutes les branches et tous les tags local r git push --all "$@"; r=$? if [ $r -eq 0 ]; then git push --tags "$@"; r=$? fi return $r elif [ -n "$all_branches" ]; then # On a demandé à pusher toutes les branches git push --all "$@" return $? elif [ -n "$all_tags" ]; then # On a demandé à pusher tous les tags git push --tags "$@" return $? elif [ $# -gt 0 ]; then # Sinon, si des arguments sont spécifiés, les passer à git sans # modification git push "$@" return $? elif git_have_annex; then # Si une annexe existe dans le dépôt, demander à git-annex de faire la # synchronisation, sauf si --no-annex est spécifié ou si on est en mode # automatique if [ -z "$no_annex" -a -z "$auto" ]; then git annex sync return $? fi fi # sinon on push vers origin. vérifier la présence du remote [ -n "$(git config --get remote.origin.url)" ] || { if [ -n "$auto" ]; then # en mode automatique, ignorer l'absence de remote return 0 else eerror "Aucun remote origin n'est défini" return 1 fi } # puis calculer la branche à pusher local branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" local origin="$(git config --get "branch.$branch.remote")" if [ -n "$branch" -a "$origin" == origin ]; then if [ -n "$auto" ]; then # en mode automatique, ne pousser que la branche courante git push "$origin" "$branch" || return else # utiliser la configuration par défaut, qui est sous debian squeeze # de pousser toutes les branches git push || return fi elif [ -n "$force" ]; then # utiliser la configuration par défaut, qui est sous debian squeeze de # pousser toutes les branches git push || return fi return 0 } function git__pclone() { estep "$1 --> $(ppath "$2")" mkdirof "$2" || return 1 git clone "$1" "$2" || return 1 if [ -z "$3" ]; then ( cd "$2" if git_have_rbranch develop; then git checkout develop || exit 1 fi ) || return 1 fi git_annex_initial "$2" || return 1 } function git__gitolite_info() { local mode="$1" urlbase="$2" pattern="$3" case "$mode" in http) curl -fs "$urlbase/info${pattern:+"?$pattern"}";; ssh) ssh -q "$urlbase" info ${pattern:+"$pattern"} 2>/dev/null;; esac } function git__filter_repos() { lawk -v prefix="$1" ' NR <= 2 { next } { # filtrer les projets qui ne sont pas encore créés if (substr($0, 5, 2) == " C") next repo = substr($0, 6) # filtrer les projets de type wildcard if (repo ~ /[\[\]\*]/) next # enlever le prefixe if (prefix != "" && substr(repo, 1, length(prefix)) != prefix) next print repo }' } function: git_clone "" function git_clone() { no_clone= update= nodevelop= recursive= parse_opts "${PRETTYOPTS[@]}" \ -n,--no-clone no_clone=1 \ -u,--update update=1 \ -m,--master nodevelop=1 \ -r,--recursive recursive=1 \ @ args -- "$@" && set -- "${args[@]}" || edie "$args" || return if [ -n "$recursive" ]; then repobase="$1" [ -n "$repobase" ] || edie "Vous devez spécifier l'url de base des dépôts à cloner" || return if [ "${repobase#http://}" != "$repobase" -o "${repobase#https://}" != "$repobase" ]; then # accès par http mode=http splitfsep "$repobase" :// scheme hostuserpath splitfsep "$hostuserpath" / host userpath splitfsep "$userpath" / user basepath [ -n "$host" -a -n "$user" ] || edie "Vous devez spécifier l'hôte e.g http://host/git/basepath" || return urlbase="$scheme://$host/$user" else # accès par ssh mode=ssh splitfsep "$repobase" : userhost basepath splituserhost "$userhost" user host [ -n "$user" ] || user=git [ -n "$host" ] || edie "Vous devez spécifier l'hôte" || return urlbase="$user@$host" fi basepath="${basepath%/}" destbase="${2:-.}" git_annex_use_ssh_wrapper prefix="${basepath:+$basepath/}" base_array_splitl repos "$(set -o pipefail; git__gitolite_info "$mode" "$urlbase" "$prefix" | git__filter_repos "$prefix")" || edie || return for repo in "${repos[@]}"; do case "$mode" in http) repourl="$urlbase/$repo";; ssh) repourl="$urlbase:$repo";; esac setx destdir=abspath "$destbase/${repo#$prefix}" if [ -d "$destdir" ]; then if [ -n "$update" ]; then ( ${no_clone:+qvals} cd "$destdir" ${no_clone:+qvals} git pull ) || edie || return else estepe "$(ppath2 "$destdir"): répertoire existant" fi elif [ -n "$no_clone" ]; then qvals git clone "$repourl" "$destdir" else git__pclone "$repourl" "$destdir" "$nodevelop" || edie || return fi done else repourl="${1%.git}" [ -n "$repourl" ] || edie "Vous devez spécifier l'url du dépôt git" || return destdir="$2" if [ -z "$destdir" ]; then splitfsep "$repourl" : userhost path setx destdir=basename -- "$path" destdir="${destdir%.git}" fi setx destdir=abspath "$destdir" git_annex_use_ssh_wrapper if [ -d "$destdir" ]; then if [ -n "$update" ]; then ( ${no_clone:+qvals} cd "$destdir" ${no_clone:+qvals} git pull ) || edie || return else estepe "$(ppath2 "$destdir"): répertoire existant" fi elif [ -n "$no_clone" ]; then qvals git clone "$repourl" "$destdir" else git__pclone "$repourl" "$destdir" "$nodevelop" || edie || return fi fi } function: git_crone "" function git_crone() { repourl="${1%.git}" [ -n "$repourl" ] || edie "Vous devez spécifier l'url du dépôt git" || return if [ "${repourl#http://}" != "$repourl" -o "${repourl#https://}" != "$repourl" ]; then # accès par http mode=http splitfsep "$repourl" :// scheme hostuserpath splitfsep "$hostuserpath" / host userpath splitfsep "$userpath" / user path [ -n "$host" -a -n "$user" ] || edie "Vous devez spécifier l'hôte e.g http://host/git/repo" || return hostuser="$scheme://$host/$user" else # accès par ssh mode=ssh splitfsep "$repourl" : userhost path splituserhost "$userhost" user host [ -n "$user" ] || user=git [ -n "$host" ] || edie "Vous devez spécifier l'hôte" || return userhost="$user@$host" fi [ -n "$path" ] || edie "Vous devez spécifier le chemin du dépôt git" || return destdir="$2" if [ -z "$destdir" ]; then setx destdir=basename -- "$path" destdir="${destdir%.git}" fi tmpdestdir= if [ -d "$destdir" ]; then [ -d "$destdir/.git" ] && edie "$(ppath2 "$destdir"): un dépôt existe déjà" || return ac_set_tmpdir tmpdestdir fi if [ "$mode" == http ]; then setx result=curl -fs "$hostuser/create?$path" || edie || return echo "$result" [[ "$result" == FATAL:* ]] && edie || return if [ -n "$tmpdestdir" ]; then setxx destname=abspath "$destdir" // basename git clone "$hostuser/$path" "$tmpdestdir/$destname" || edie || return mv "$tmpdestdir/$destname/.git" "$destdir" || edie || return ac_clean "$tmpdestdir" else git clone "$hostuser/$path" "$destdir" || edie || return fi elif [ "$mode" == ssh ]; then git_annex_use_ssh_wrapper ssh "$userhost" create "$path" || edie || return if [ -n "$tmpdestdir" ]; then setxx destname=abspath "$destdir" // basename git clone "$userhost:$path" "$tmpdestdir/$destname" || edie || return mv "$tmpdestdir/$destname/.git" "$destdir" || edie || return ac_clean "$tmpdestdir" else git clone "$userhost:$path" "$destdir" || edie || return fi else edie "$mode: mode non supporté" || return fi git_annex_initial "$destdir" || edie || return }