#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

SCRIPT_ALIASES=(
    pf0:-0
    pfn:-N
    pfg:-g
    pfb:-b
    pfs:-s
    pfa:-a
    pfe:-e
    pfd:-d
)

ORIGEXT=pff
DEFAULT_ORIGEXTS=(".$ORIGEXT" .origine .default)
PFFCONF=.pff.conf # ne pas modifier
DEFAULT_PROTECTS=(/.git/ .svn/ /pff/ "/$PFFCONF")

PFFCONFVARS=(
    "VERSION//Version actuellement installée"
    -a
    "PVERSIONS//Versions en attente d'intégration"
    "PROFILES//Profils définis"
    -s
    "DISTTYPE=auto//Type de distribution upstream: full ou patch"
    -a
    "ORIGEXTS=//Extensions origines"
    "PROTECTS=//Fichiers locaux à protéger lors de l'intégration e.g /dir/, /file, etc."
    "MKDIRS//Répertoires qui doivent toujours exister"
    "FILTERS//Filtres appliqués aux fichiers lors de l'intégration"
    "NOMERGES=//Fichiers qu'il ne faut pas chercher à fusionner"
)

if [ "$#" -eq 1 -a "$1" == --nutools-makelinks ]; then
    # créer les liens
    scriptname="$(basename -- "$0")"
    for alias in "${SCRIPT_ALIASES[@]}"; do
        alias="${alias%:*}"
        ln -s "$scriptname" "$alias"
    done
    exit 0
elif [ $# -eq 1 -a "$1" == --nutools-completion ]; then
    echo '
function __pff_profiles() {
  local cwd="$(pwd)"
  local pffdir="$cwd"
  while true; do
    if [ -f "$pffdir/'"$PFFCONF"'" -a -d "$pffdir/pff" ]; then
      cd "$pffdir/pff"
      /bin/ls -1d * | while read f; do
        [ -d "$f" -a "$f" != Current ] || continue
        f="$1$f"
        if [[ "${f:0:2}" == -[eEn] ]]; then
          echo -n -
          echo "${f:1}"
        else
          echo "$f"
        fi
      done
      cd "$cwd"
      break
    fi
    [ "$pffdir" == / -o "$pffdir" == "$HOME" ] && break
    pffdir="$(dirname -- "$pffdir")"
  done
}
function __pfe_profiles() {
  echo ALL
  __pff_profiles "$@" | grep -vxF Base
}
function __pfs_completion() {
  local cur
  _get_comp_words_by_ref cur
  COMPREPLY=($(compgen -W "$(__pff_profiles)" "$cur"))
}
complete -F __pfs_completion -o bashdefault -o default pfs
function __pfe_completion() {
  local cur prev opt comp
  _get_comp_words_by_ref cur prev
  if [[ "$prev" == -*p ]]; then
    COMPREPLY=($(compgen -W "$(__pfe_profiles)" -- "$cur"))
  elif [ "$prev" == --profile ]; then
    COMPREPLY=($(compgen -W "$(__pfe_profiles)" -- "$cur"))
  elif [[ "$cur" == -*p* ]]; then
    comp="${cur#-*p}"; opt="${cur:0:$((${#cur}-${#comp}))}"
    COMPREPLY=($(compgen -W "$(__pfe_profiles "$opt")" -- "$cur"))
  fi
}
complete -F __pfe_completion -o bashdefault -o default pfe
function __pff_completion() {
  local cur prev opt comp
  _get_comp_words_by_ref cur prev
  if [ "${COMP_WORDS[1]}" == -e -o "${COMP_WORDS[1]}" == --edit ]; then
    # ne compléter -p que si on est en mode --edit
    if [[ "$prev" == -*p ]]; then
      COMPREPLY=($(compgen -W "$(__pfe_profiles)" -- "$cur"))
    elif [ "$prev" == --profile ]; then
      COMPREPLY=($(compgen -W "$(__pfe_profiles)" -- "$cur"))
    elif [[ "$cur" == -*p* ]]; then
      comp="${cur#-*p}"; opt="${cur:0:$((${#cur}-${#comp}))}"
      COMPREPLY=($(compgen -W "$(__pfe_profiles "$opt")" -- "$cur"))
    fi
  elif [[ "$prev" == -*s ]]; then
    COMPREPLY=($(compgen -W "$(__pff_profiles)" -- "$cur"))
  elif [ "$prev" == --switch ]; then
    COMPREPLY=($(compgen -W "$(__pff_profiles)" -- "$cur"))
  elif [[ "$cur" == -*s* ]]; then
    comp="${cur#-*s}"; opt="${cur:0:$((${#cur}-${#comp}))}"
    COMPREPLY=($(compgen -W "$(__pff_profiles "$opt")" -- "$cur"))
  fi
}
complete -F __pff_completion -o bashdefault -o default pff
'
    exit 0
fi

source "$(dirname "$0")/lib/ulib/ulib" || exit 1
urequire DEFAULTS multiconf vcs javaproperties

function display_help() {
    uecho "$scriptname: gestion de modifications locales

Un produit est distribué par un partenaire, et il faut maintenir certaines
modifications locales tout en continuant d'accepter des modififications de
l'upstream. Ce script aide à maintenir un tel scénario.

En général, la distribution upstream identifie les fichiers modifiables en leur
donnant une extension particulière, par exemple 'file.origine'. On peut aussi
décider de modifier des fichiers qui n'ont pas été prévus comme tels par
l'upstream. On peut aussi avoir plusieurs ensembles de modifications, rassemblés
en profils, e.g prod ou test pour les modifications à déployer en prod ou en
test.

Terminologie: Les fichiers pour lesquels il faut maintenir une version locale
sont appelés 'fichiers locaux', qu'ils viennent de la distribution upstream ou
non. Les autres fichiers qui proviennent de la distribution sont appelés
'fichiers upstream'. Les fichiers livrés dans la distribution upstream avec une
extension particulière pour indiquer qu'ils sont modifiables sont appelés
'fichiers origine'.

Les fichiers sont classés dans des profils spécifiques. Les profils reconnus
sont:
- Base est le profil des fichiers upstream non modifiés. Les fichiers origine
  sont intégrés dans ce profil.
- Common est le profil des fichiers upstream modifiés. Tous les fichiers
  devraient être modifiés dans ce profil.
- Les autres profils sont basés sur Common en priorité puis sur Base en cas de
  non existence dans Common. Il peut s'agir de profils comme prod ou test, qui
  contiennent des modifications spécifiques à différents cas d'utilisation.

USAGE
    $scriptname [options]

COMMANDES / OPTIONS
Les arguments du script dépendent de la commande utilisée. Les commandes
supportées sont:
    -0, --init [WORKDIR [ARCHIVE]]
        Initialiser un répertoire pour le suivi des distributions upstream. Le
        fichier $PFFCONF contient toutes les informations paramétrables sur la
        gestion du projet.
    --s, --origext .EXT
        Ajouter une extension à la liste des extensions origines, c'est à dire
        les fichiers identifiés dans la distribution upstream comme contenus
        modifiables. Par défaut, les extensions suivantes sont reconnues:
            ${DEFAULT_ORIGEXTS[*]}
        Cette option peut être utilisée autant de fois que nécessaire.
    --k, --clear-origexts
        Supprimer la liste par défaut des extensions origines. Cette option doit
        être spécifiée avant l'option --origext pour construire une nouvelle
        liste. La liste des extensions ne doit pas être vide. Si c'est le cas,
        elle est modifiée pour contenir l'unique élément (.$ORIGEXT)

    -N, --new ARCHIVE [WORKDIR]
    -M, --new-only ARCHIVE [WORKDIR]
        Intégrer une nouvelle distribution upstream, sous forme d'une archive ou
        d'un répertoire. Les fichiers origine et les fichiers locaux sont placés
        en attente d'intégration. Les autres sont intégrés sans modification.
        La variante --new appelle automatiquement --patch après l'intégration de
        l'archive.
    -V, --version VERSION
        Spécifier la version de l'archive qui est intégrée avec l'option --new.
        Normalement, cette information est calculée automatiquement à partir du
        nom de l'archive. Si la source est un répertoire ou une archive sans
        numéro de version, cette option est requise.
    -F, --full-archive
        Intégrer une distribution complète. Par défaut, les archives avec
        l'extension .war sont considérées commes des livraisons complètes. Si
        la source est un répertoire ou une archive sans extension, l'une des
        options -F ou -H est requise.
    -H, --patch-archive
        Intégrer un patch. Par défaut, les archives avec l'extension .zip sont
        considérées comme des patches. Si la source est un répertoire ou une
        archive sans extension, l'une des options -F ou -H est requise.
        Avec une distribution de type patch, on considère que les fichiers
        livrés ne sont pas des fichiers origines. Il n'y a donc aucun traitement
        particulier: l'archive est simplement intégrée telle quelle.
    --auto-unwrap
        Si l'archive ne contient qu'un seul répertoire à la racine nommé d'après
        le nom de base de l'archive, c'est le contenu de ce répertoire qui est
        considéré. C'est l'option par défaut.
        Le test est effectué avec et sans la version. Par exemple, si l'archive
        est product-x.y.zip, et qu'elle contient un unique répertoire nommé
        'product-x.y' ou 'product' alors intégrer le contenu de ce répertoire.
    -E, --unwrap
        Si l'archive ne contient qu'un seul répertoire à la racine, intégrer
        inconditionellement le contenu de se répertoire.
    --no-unwrap
        Intégrer tel quel le contenu de l'archive.

    -g, --patch [WORKDIR]
        Intégrer les modifications entrantes sur les fichiers nouvellement
        arrivés via l'option --new
    -o, --ours
        Utiliser la stratégie '-s recursive -X ours' en cas de conflit lors de
        l'intégration de la version.
    --ask-commit
    -c, --commit
    --no-commit
        Après l'intégration avec succès d'un patch, demander à l'utilisateur
        s'il veut faire commit & push dans git (--ask-commit, la valeur par
        défaut), le faire sans confirmation (--commit), ou ne jamais le faire
        (--no-commit)

    -b, --add-global FILES...
        Ajouter/Identifier un fichier comme un fichier local pour tous les
        profils. Le fichier est copié dans le profil Base.

    --locals [WORKDIR]
        Lister les fichiers locaux

    --profiles [WORKDIR]
        Lister les profils valides

    -s, --switch PROFILE [WORKDIR]
        Basculer le profil courant

    -a, --add-local FILES...
        Ajouter/Identifier un fichier comme un fichier local et le rendre
        spécifique au profil courant.
    -e, --edit FILES...
        Editer des fichiers, implique --add-local
    -p, --profile PROFILE
        Pour l'option --edit, sélectionner le profil spécifié comme celui
        concerné pour les fichier mentionnés après cette option. Par exemple:
            $scriptname -e A -pprod B C -ptest D
        Edite le fichier A du profil courant, les fichiers B et C du profil prod
        et le fichier D du profil test.
        Le profil ALL est spécial et permet d'éditer le fichier dans tous les
        profils *où il existe*, excepté Base parce que ce profil contient des
        fichiers qui ne devraient pas être modifiés.
        Pour tous les autres profils, si le fichier n'existe pas dans le profil
        spécifié il est rajouté dans le profil avant son édition.
        Attention: pour que l'option -p/--profile soit correctement reconnue
        avec l'option -e/--edit, il faut que cette dernière option soit
        mentionnée en premier sur la ligne de commande.
    -P, --prod
    -T, --test
    -A, --all-profiles
        Raccourcis pour respectivement -pprod, -ptest et -pALL

    -d, --diff [DESTP [WORKDIR]]
    -d, --diff [SRCP DESTP [WORKDIR]]
        Afficher la différence entre entre deux profils. Avec la première
        syntaxe, comparer le profil DESTP au profil courant. Avec la deuxième
        syntaxe, comparer le profil DESTP au profil SRCP.
    -l, --list-names
        N'afficher que les noms des fichiers qui sont différents

    --infos [WORKDIR]
        Afficher des informations sur le projet courant: profils, fichiers
        locaux, profil courant, etc. C'est la commande par défaut.
        Les fichiers locaux sont taggés avec les valeurs suivantes
          ${COULEUR_ROUGE}P${COULEUR_NORMALE} il existe un patch pour ce fichier dans le profil courant
          ${COULEUR_BLEUE}*${COULEUR_NORMALE} ce fichier local est spécifique à ce profil
          $(get_color YELLOW)C${COULEUR_NORMALE} ce fichier local est spécifique au profil Common
    -l, --show-all
        Afficher tous les fichiers locaux au lieu de se contenter des fichiers
        modifiés dans le profil courant."
}

# Nomenclature pour le nommage des fichiers traités:
# pfile: le chemin absolu du fichier dans le projet
# rfile: le chemin relatif du fichier dans le projet
# bfile: le chemin absolu du fichier dans pff/Base/
# Cfile: le chemin absolu du fichier dans pff/Common/
# cfile: le chemin absolu du fichier dans pff/Current/
# Pfile: le chemin absolu du fichier dans pff/ANYPROFILE/
# plink: la destination du lien pfile
# clink: la destination du lien cfile
# Plink: la destination du lien Pfile

function flexists() {
    [ -e "$1" -o -L "$1" ]
}

function find_pffdir() {
    # trouver le répertoire du projet pff à partir du répertoire $2(=.) et
    # mettre le chemin absolu dans la variable $1(=pffdir)
    # si le répertoire n'est pas trouvé, retourner 1
    local destvar="${1:-pffdir}" pffdir
    setx pffdir=abspath "${2:-.}"
    while true; do
        if [ -f "$pffdir/$PFFCONF" -a -d "$pffdir/pff" ]; then
            local "$destvar"
            upvar "$destvar" "$pffdir"
            return 0
        fi
        [ "$pffdir" == / -o "$pffdir" == "$HOME" ] && break
        setx pffdir=dirname -- "$pffdir"
    done
    return 1
}

function ensure_pffdir() {
    # trouver le répertoire du projet pff à partir du répertoire $2(=.) et
    # mettre le chemin absolu dans la variable $1(=pffdir)
    # si le répertoire n'est pas trouvé, arrêter le script avec un code d'erreur
    local destvar="${1:-pffdir}" pffdir
    if find_pffdir pffdir "$2"; then
        conf_init "${PFFCONFVARS[@]}"
        source "$pffdir/$PFFCONF"
        local "$destvar"; upvar "$destvar" "$pffdir"
        return
    fi
    local msg="Projet pff introuvable (utiliser --init ?)"
    [ -n "$2" ] && die "$2: $msg" || die "$msg"
}

function get_current_profile() {
    # afficher le profil courant du projet pff $1, s'il est défini
    local pffdir="$1"
    [ -L "$pffdir/pff/.Current" ] && readlink "$pffdir/pff/.Current"
}

function get_profiles() {
    # afficher tous les profils valides du projet pff $1
    local pffdir="$1"
    (for profile in "${PROFILES[@]}"; do echo "$profile"; done
     list_dirs "$pffdir/pff") | sort -u | grep -vxF Current
}

function get_user_profiles() {
    # afficher tous les profils modifiables du projet pff $1 (c'est à dire tous
    # les profils valides excepté Base)
    get_profiles "$@" | grep -vxF Base
}

function get_first_profile() {
    # afficher le premier profil autre que Base du projet pff $1
    local profile
    profile="${PROFILES[0]}"
    if [ -z "$profile" -o "$profile" == Base ]; then
        get_user_profiles "$@" | head -n1
    else
        echo "$profile"
    fi
}

function get_local_files() {
    # afficher tous les fichiers locaux exprimés relativement au répertoire du
    # projet pff $1
    local pffdir="$1"
    find "$pffdir/pff/Base" -type f | sed "s|^$pffdir/pff/Base/||" | grep -v '__pv-.*__$'
}

function multiups() {
    # afficher un chemin vers le haut e.g ../../.. avec autant d'éléments que
    # les répertoires du chemin relatif $1.
    # méthode: commencer avec la valeur de départ $2 et préfixer avec autant de
    # ../ que nécessaire. puis afficher le résultat.
    local tmp="$1" link="$2"
    setx tmp=dirname -- "$tmp"
    while [ "$tmp" != . ]; do
        [ -n "$link" ] && link="/$link"
        link="..$link"
        setx tmp=dirname -- "$tmp"
    done
    echo "$link"
}

function get_rfile() {
    # obtenir le chemin relatif du fichier $1 exprimé par rapport au répertoire
    # du projet pff $2. Si c'est un fichier d'un répertoire de profil,
    # l'exprimer comme un chemin du répertoire de projet, e.g pff/Profile/path
    # devient path
    # retourner 1 si le chemin est invalide (est situé en dehors de pffdir ou
    # pas dans un répertoire de profil)
    local rfile="$1" pffdir="$2"
    setx rfile=abspath "$rfile"
    [ "${rfile#$pffdir/}" != "$rfile" ] || return 1
    rfile="${rfile#$pffdir/}"
    if [[ "$rfile" == pff/*/* ]]; then
        rfile="${rfile#pff/*/}"
    elif [[ "$rfile" == pff/* ]]; then
        return 1
    fi
    echo "$rfile"
}
function get_pfile() {
    # obtenir le chemin du fichier $1 exprimé par rapport au répertoire du
    # profil $2 dans le répertoire de projet $3
    # retourner 1 si le chemin est invalide (est situé en dehors de pffdir ou
    # pas dans un répertoire de profil)
    local pfile="$1" profile="$2" pffdir="$3"
    setx pfile=abspath "$pfile"
    [ "${pfile#$pffdir/}" != "$pfile" ] || return 1
    pfile="${pfile#$pffdir/}"
    if [[ "$pfile" == pff/*/* ]]; then
        pfile="${pfile#pff/*/}"
    elif [[ "$pfile" == pff/* ]]; then
        return 1
    fi
    echo "$pffdir/pff/$profile/$pfile"
}
function get_bfile() { get_pfile "$1" Base "$2"; }
function get_Cfile() { get_pfile "$1" Common "$2"; }
function get_cfile() { get_pfile "$1" Current "$2"; }

function get_vlfiles_nostrip() {
    # afficher tous les fichiers de version
    local pffdir="$1" rfile="$2" profile="${3:-Base}" version="$4"
    [ -d "$pffdir/pff/$profile" ] || return
    if [ -n "$version" ]; then
        if [ -n "$rfile" ]; then
            find "$pffdir/pff/$profile" \
                 -type f -path "$pffdir/pff/$profile/${rfile}__pv-${version}__" -o \
                 -type l -path "$pffdir/pff/$profile/${rfile}__pv-${version}__"
        else
            find "$pffdir/pff/$profile" \
                 -type f -name "*__pv-${version}__" -o \
                 -type l -name "*__pv-${version}__"
        fi
    else
        if [ -n "$rfile" ]; then
            find "$pffdir/pff/$profile" \
                 -type f -path "$pffdir/pff/$profile/${rfile}__pv-*__" -o \
                 -type l -path "$pffdir/pff/$profile/${rfile}__pv-*__"
        else
            find "$pffdir/pff/$profile" \
                 -type f -name "*__pv-*__" -o \
                 -type l -name "*__pv-*__"
        fi
    fi
}
function get_vlfiles() {
    local pffdir="$1" rfile="$2" profile="${3:-Base}" version="$4"
    get_vlfiles_nostrip "$@" | sed "s|^$pffdir/pff/$profile/||"
}

function is_nomerge() {
    local file="$1" pffdir="$2"
    local nomerge rfile
    setx rfile=get_rfile "$file" "$pffdir"
    setx file=basename -- "$rfile" # utilisé pour le match sur le nom du fichier
    for nomerge in "${NOMERGES[@]}"; do
        if [[ "$nomerge" == */* ]]; then
            # matcher sur le chemin relatif
            if eval "[[ $(qval "$rfile") == $(qwc "$nomerge") ]]"; then
                return 0
            fi
        else
            # matcher uniquement sur le nom du fichier
            if eval "[[ $(qval "$file") == $(qwc "$nomerge") ]]"; then
                return 0
            fi
        fi
    done
    return 1
}

function sync_vlfiles() {
    # synchroniser les fichiers de version $3..@ dans tous les répertoires de
    # profil, ou seulement le répertoire de profil $2 si la valeur n'est pas
    # vide.
    local pffdir="$1"; shift
    local profile="$1"; shift
    local -a profiles
    if [ -n "$profile" ]; then
        profiles=("$profile")
    else
        array_from_lines profiles "$(get_user_profiles "$pffdir")"
    fi
    local vlfile rfile prefix pfile plink tmp
    for vlfile in "$@"; do
        rfile="${vlfile%__pv-*__}"
        for profile in "${profiles[@]}"; do
            prefix="$pffdir/pff/$profile"
            flexists "$prefix/$rfile" || continue
            pfile="$prefix/$vlfile"
            setx plink=multiups "$profile/$vlfile" "Base/$vlfile"
            if [ -L "$pfile" ]; then
                # correction éventuelle du lien existant
                setx tmp=readlink "$pfile"
                [ "$tmp" == "$plink" ] || ln -sfT "$plink" "$pfile"
            else
                ln -sf "$plink" "$pfile" || return
            fi
        done
    done
}

function select_profile() {
    # sélectionner le profil $1 dans le projet pff $2. créer d'abord le profil
    # s'il n'existe pas.
    local profile="$1" pffdir="$2"
    # créer le répertoire de profil si nécessaire
    mkdir -p "$pffdir/pff/$profile" || return 1
    # mettre à jour les liens
    local -a lfiles; local lfile src dest
    setx -a lfiles=get_local_files "$pffdir"
    for lfile in "${lfiles[@]}"; do
        src="$pffdir/pff/Current/$lfile"
        if [ -f "$pffdir/pff/$profile/$lfile" ]; then
            dest="$profile/$lfile"
        elif [ "$profile" != Common -a -f "$pffdir/pff/Common/$lfile" ]; then
            dest="Common/$lfile"
        else
            dest="Base/$lfile"
        fi
        setx dest=multiups "Current/$lfile" "$dest"
        [ -L "$src" ] || mkdirof "$src"
        ln -sfT "$dest" "$src"
    done
    # maj du lien "profil courant"
    ln -sfT "$profile" "$pffdir/pff/.Current"
}

function autoinit() {
    # vérifications automatiques: créer les répertoires de base nécessaire au
    # fonctionnement de pff dans le projet pff $1
    local pffdir="$1" profile mkdir
    [ -d "$pffdir/pff/Current" ] || mkdir -p "$pffdir/pff/Current"
    [ -d "$pffdir/pff/Base" ] || mkdir -p "$pffdir/pff/Base"
    # tous les fichiers du profil Base doivent être en lecture seule
    find "$pffdir/pff/Base" -type f -perm /222 -exec chmod a-w '{}' +
    # Créer les répertoires de MKDIRS
    for mkdir in "${MKDIRS[@]}"; do
        mkdir -p "$pffdir/$mkdir"
    done
    return 0
}

function autoselect() {
    # vérification automatiques: sélectionner le premier profil défini si aucun
    # profil n'est sélectionné dans le projet pff $1
    local pffdir="$1" profile
    if [ ! -L "$pffdir/pff/.Current" ]; then
        setx profile=get_first_profile "$pffdir"
        [ -n "$profile" ] || profile=Base
        enote "Autosélection du profil $profile"
        select_profile "$profile" "$pffdir"
    fi
}

function autofix() {
    autoinit "$1"
    autoselect "$1"
}

################################################################################
# Filtres

function pff_filter_normalize_properties() {
    if [ $# -eq 0 ]; then
        __norm_properties
    else
        local mode r
        mode="$(fix_mode "$1")"
        norm_properties "$1"; r=$?
        unfix_mode "$1" "$mode"
        return $r
    fi
}

function apply_filter() {
    # Appliquer les filtres définis au fichier $1 dans le projet pff $2
    # $3 est le nom du fichier sans préfixe, pour la sélection du filtre
    # retourner 0 si un filtre a été appliqué avec succès, 1 si une erreur s'est
    # produite, 2 si aucun filtre n'existe pour ce fichier
    local pfile="$1" pffdir="$2"
    local realfile="${3:-$pfile}"
    local rfile ffile filter r=2
    setx rfile=get_rfile "$realfile" "$pffdir"
    for filter in "${FILTERS[@]}"; do
        splitpair "$filter" ffile filter
        if [ "$ffile" == "$rfile" ]; then
            "pff_filter_$filter" "$pfile" && r=0 || r=1
        fi
    done
    return $r
}

################################################################################
# Commandes

#===========================================================
# pff --init

function init_cmd() {
    local workdir="$1" archive="$2"
    local pffdir

    if [ -z "$workdir" ]; then
        estep "Initialisation d'un nouveau projet pff"
        read_value "Veuillez entrer le répertoire du projet" workdir
    fi
    if find_pffdir pffdir "$workdir"; then
        enote "$(ppath "$pffdir"): est déjà un projet pff"
        if ask_yesno "Voulez-vous vérifier la configuration et la mettre à jour?" O; then
            conf_init "${PFFCONFVARS[@]}"
            source "$pffdir/$PFFCONF"
            conf_upgrade "$workdir/$PFFCONF"
            conf_update -n "$workdir/$PFFCONF" ORIGEXTS
        fi
        return 0
    fi
    if [ ! -d "$workdir" ]; then
        ewarn "$workdir: ce répertoire n'existe pas"
        ask_yesno "Voulez-vous le créer?" O || return 1
        mkdir -p "$workdir" || return 1
    fi
    enote "Initialisation d'un nouveau projet pff dans $workdir"
    # fichier de configuration
    conf_upgrade "$workdir/$PFFCONF" "${PFFCONFVARS[@]}"
    if [ "${ORIGEXTS[*]}" != "${DEFAULT_ORIGEXTS[*]}" ]; then
        conf_update "$workdir/$PFFCONF" ORIGEXTS
    fi
    array_addu PROFILES Common
    conf_update "$workdir/$PFFCONF" PROFILES
    # répertoire pff
    mkdir -p "$workdir/pff"
    [ -f "$workdir/pff/.gitignore" ] || echo >"$workdir/pff/.gitignore" "\
/Current/
/.Current"
    autofix "$workdir"
    return 0
}

#===========================================================
# pff --new

function new__prepare_archive() {
    # préparer l'archive fournie en la copiant dans un répertoire temporaire
    # initialiser la variable $1(=srcdir) avec le chemin du répertoire résultat
    local destvar="${1:-srcdir}"; shift
    local destver="${1:-version}"; shift
    local archive="$1" version="$2" unwrap="$3"

    local srcdir
    if [ -f "$archive" ]; then
        is_archive "$archive" || die "$archive: doit être un fichier archive"
        [ -n "$version" ] || version="$(get_archive_version "$archive")"
        archive="$(abspath "$archive")"
    elif [ -d "$archive" ]; then
        srcdir="$(abspath "$archive")"
        archive=
    else
        die "$archive: fichier introuvable"
    fi

    read_value "Entrez un identifiant pour cette version" version "$version" || die

    local tmpd
    if [ -n "$archive" ]; then
        if [ "$DISTTYPE" == auto ]; then
            case "$archive" in
            *.war)
                enote "Auto-sélection du type distribution complète sur la base de l'extension .war"
                DISTTYPE=full
                ;;
            *.zip)
                enote "Auto-sélection du type distribution de patch sur la base de l'extension .zip"
                DISTTYPE=patch
                ;;
            *)
                die "L'extension de l'archive n'est pas reconnue. Vous devez spécifier l'une des options -F ou -H"
                ;;
            esac
        fi

        ac_set_tmpdir tmpd
        estep "Extraction de $(ppath "$archive" "$cwd")"
        extract_archive "$archive" "$tmpd" || die
        srcdir="$tmpd"

        if [ -n "$unwrap" ]; then
            local -a files
            array_lsall files "$srcdir" "*" ".*"
            if [ "${#files[*]}" -eq 1 ]; then
                local file="${files[0]}"
                if [ "$unwrap" == auto ]; then
                    # nom de l'archive avec la version
                    local banv="$(get_archive_basename "$archive")"
                    # nom de l'archive sans la version
                    local ban="${banv%$(get_archive_versionsuffix "$archive")}"
                    local filename="$(basename "$file")"
                    [ "$filename" == "$banv" -o "$filename" == "$ban" ] || unwrap=
                fi
                [ -n "$unwrap" -a -d "$file" ] && srcdir="$file"
            fi
        fi
    elif [ -d "$srcdir" ]; then
        if [ "$DISTTYPE" == auto ]; then
            die "La source est un répertoire. Vous devez spécifier l'une des option -F ou -H"
        fi

        ac_set_tmpdir tmpd
        estep "Création d'une copie de travail de $(ppath "$srcdir" "$cwd")"
        rsync ${QUIET:+-q} -a "$srcdir/" "$tmpd" || die
        srcdir="$tmpd"
    fi

    local "$destvar"; upvar "$destvar" "$srcdir"
    local "$destver"; upvar "$destver" "$version"
}

function new_cmd() {
    local autopatch="$1" version="$2" disttype="$3" unwrap="$4" merge_strategy="$5" commit_policy="$6"; shift; shift; shift; shift; shift; shift
    local archive="$1" pffdir="$2"

    ensure_pffdir pffdir "$pffdir"
    [ -n "$disttype" ] && DISTTYPE="$disttype"

    ## préparer la source: les fichiers entrants
    local srcdir full
    new__prepare_archive srcdir version "$archive" "$version" "$unwrap"
    [ "$DISTTYPE" == full ] && full=1

    ## lister les fichiers dans la source et la destination
    local workdir
    ac_set_tmpdir workdir

    # fichiers sources
    local fnsrc0="$workdir/nsrc0" fosrc0="$workdir/osrc0"
    >"$fnsrc0"
    >"$fosrc0"
    find "$srcdir" -type f | awkrun ORIGEXTS[@] prefix="$srcdir/" fnsrc="$fnsrc0" fosrc="$fosrc0" '{
  found = 0
  for (i = 1; i <= ORIGEXTS_count; i++) {
    sub("^" prefix, "")
    if ($0 ~ ORIGEXTS[i] "(/|$)") {
      print >fosrc
      found = 1
      break
    }
  }
  if (!found) {
    print >fnsrc
  }
}'
    # fichiers destination
    local -a find; local p
    local fndest0="$workdir/ndest0" fldest0="$workdir/ldest0"
    get_local_files "$pffdir" >"$fldest0"
    for p in "${PROTECTS[@]}" "${DEFAULT_PROTECTS[@]}"; do
        [ ${#find[*]} -gt 0 ] && find=("${find[@]}" -o)
        if [ "${p#/}" != "$p" ]; then
            # les fichiers à la racine
            p="${p#/}"
            if [ "${p%/}" != "$p" ]; then
                find=("${find[@]}" -type d -path "$pffdir/${p%/}" -prune)
            else
                find=("${find[@]}" -type f -path "$pffdir/$p" -o -type l -path "$pffdir/$p")
            fi
        else
            # fichiers n'importe où dans l'arborescence
            if [ "${p%/}" != "$p" ]; then
                find=("${find[@]}" -type d -name "${p%/}" -prune)
            else
                find=("${find[@]}" -type f -name "$p" -o -type l -name "$p")
            fi
        fi
    done
    [ ${#find[*]} -gt 0 ] && find=("${find[@]}" -o)
    find=(find "$pffdir" "${find[@]}" -type f -print -o -type l -print)
    "${find[@]}" | sed "s|^$pffdir/||" | grep -vxf "$fldest0" >"$fndest0"

    ## Traiter les fichiers normaux

    local fnsrc="$workdir/nsrc" fndest="$workdir/ndest"
    grep -vxf "$fldest0" "$fnsrc0" | csort >"$fnsrc"
    csort "$fndest0" >"$fndest"

    local fcreates="$workdir/creates" fdeletes="$workdir/deletes" fcds="$workdir/cds" fupdates="$workdir/updates"
    >"$fcreates"
    >"$fdeletes"
    diff "$fnsrc" "$fndest" | awkrun fcreates="$fcreates" fupdates="$fupdates" fdeletes="$fdeletes" '{
  c = substr($0, 1, 1)
  f = substr($0, 3)
  if (c == "<") print f >fcreates
  else if (c == ">") print f >fdeletes
}'
    cat "$fcreates" "$fdeletes" >"$fcds"
    grep -vxf "$fcds" "$fnsrc" >"$fupdates"
    local -a creates updates deletes r i have_creates have_updates have_deletes src dest
    array_from_lines creates "$(<"$fcreates")"; [ ${#creates[*]} -gt 0 ] && have_creates=1 || have_creates=
    array_from_lines updates "$(<"$fupdates")"; [ ${#updates[*]} -gt 0 ] && have_updates=1 || have_updates=
    array_from_lines deletes "$(<"$fdeletes")"; [ ${#deletes[*]} -gt 0 ] && have_deletes=1 || have_deletes=
    enote "Fichiers normaux: $((${#creates[*]} + ${#updates[*]})) au total, ${#creates[*]} nouveau(x)${full:+", ${#deletes[*]} à supprimer"}"
    ask_any -i "Voulez-vous continuer?" +Od; r=$?
    if [ $r == 2 ]; then
        for i in "${updates[@]}"; do
            echo " $i"
        done
        for i in "${creates[@]}"; do
            echo "+$i"
        done
        if [ -n "$full" ]; then
            for i in "${deletes[@]}"; do
                echo "-$i"
            done
        fi
        ask_yesno "Voulez-vous continuer?" O || return 1
    elif [ $r == 1 ]; then
        return 1
    fi

    if [ -n "$have_creates" -o -n "$have_updates" -o -n "$have_deletes" ]; then
        ebegin "Copie des fichiers"
        r=0
        for i in "${creates[@]}"; do
            src="$srcdir/$i"
            dest="$pffdir/$i"
            mkdirof "$dest"; r=$?
            edot $r "mkdirof $i"; [ $r -eq 0 ] || break
            cp "$src" "$dest"; r=$?
            edot $r "create $i"; [ $r -eq 0 ] || break
            apply_filter "$dest" "$pffdir"; r=$?
            [ $r -eq 0 -o $r -eq 1 ] && edot $r "filter $i"
            [ $r -eq 2 ] && r=0; [ $r -eq 0 ] || break
        done
        [ $r -eq 0 ] || { eend $r; return 1; }
        for i in "${updates[@]}"; do
            src="$srcdir/$i"
            dest="$pffdir/$i"
            if diff -q "$src" "$dest" >&/dev/null; then
                # pas la peine de copier si les fichiers sont identiques
                show_debug && edot 0 "$i: non modifié"
                continue
            fi
            cat "$src" >"$dest"; r=$?
            edot $r "update $i"; [ $r -eq 0 ] || break
            apply_filter "$dest" "$pffdir"; r=$?
            [ $r -eq 0 -o $r -eq 1 ] && edot $r "filter $i"
            [ $r -eq 2 ] && r=0; [ $r -eq 0 ] || break
        done
        [ $r -eq 0 ] || { eend $r; return 1; }
        if [ -n "$full" ]; then
            # ne faire les suppression qu'en mode full
            for i in "${deletes[@]}"; do
                rm "$pffdir/$i"; r=$?
                edot $r "delete $i"; [ $r -eq 0 ] || break
            done
        fi
        eend $r
    fi

    ## Traiter les fichiers origines

    local ftmp="$workdir/tmp"
    local fosrc="$workdir/osrc" flosrc="$workdir/losrc" flsrc="$workdir/lsrc" fldest="$workdir/ldest"
    csort "$fosrc0" >"$fosrc"
    >"$flsrc"
    >"$flosrc"
    awkrun <"$fosrc0" ORIGEXTS[@] flsrc="$flsrc" flosrc="$flosrc" '{
  for (i = 1; i <= ORIGEXTS_count; i++) {
    if ($0 ~ ORIGEXTS[i] "(/|$)") {
      orig = $0
      local = gensub(ORIGEXTS[i] "(/|$)", "\\1", 1, $0)
      print local ":" orig >flosrc
      print local >flsrc
      break
    }
  }
}'
    csort "$flsrc" >"$ftmp"; cat "$ftmp" >"$flsrc"
    csort "$fldest0" >"$fldest"

    local losrc lsrc osrc; local -a tmpa; declare -A losrcs
    array_from_lines tmpa "$(<"$flosrc")"
    for losrc in "${tmpa[@]}"; do
        splitpair "$losrc" lsrc osrc
        losrcs["$lsrc"]="$osrc"
    done

    >"$fcreates"
    >"$fdeletes"
    diff "$flsrc" "$fldest" | awkrun fcreates="$fcreates" fupdates="$fupdates" fdeletes="$fdeletes" '{
  c = substr($0, 1, 1)
  f = substr($0, 3)
  if (c == "<") print f >fcreates
  else if (c == ">") print f >fdeletes
}'
    # contrairement aux fichiers normaux, ajouter le contenu de fdeletes à fupdates
    # les fichiers de fdeletes sont des fichiers locaux non identifiés comme tels dans l'origine
    { grep -vxf "$fcreates" "$flsrc"; cat "$fdeletes"; } >"$fupdates"
    array_from_lines creates "$(<"$fcreates")"; [ ${#creates[*]} -gt 0 ] && have_creates=1 || have_creates=
    array_from_lines updates "$(<"$fupdates")"; [ ${#updates[*]} -gt 0 ] && have_updates=1 || have_updates=
    enote "Fichiers origines: $((${#creates[*]} + ${#updates[*]})) au total, ${#creates[*]} nouveau(x)"
    ask_any -i "Voulez-vous continuer?" +Od; r=$?
    if [ $r == 2 ]; then
        for i in "${updates[@]}"; do
            echo " $i"
        done
        for i in "${creates[@]}"; do
            echo "+$i"
        done
        ask_yesno "Voulez-vous continuer?" O || return 1
    elif [ $r == 1 ]; then
        return 1
    fi

    local vlfile; local -a vlfiles
    if [ -n "$have_creates" -o -n "$have_updates" ]; then
        ebegin "Copie des fichiers"
        r=0
        for i in "${creates[@]}"; do
            src="$srcdir/${losrcs[$i]}"
            dest="$pffdir/$i"
            mkdirof "$dest"; r=$?
            edot $r "mkdirof $i"; [ $r -eq 0 ] || break
            cp "$src" "$dest"; r=$?
            edot $r "create $i"; [ $r -eq 0 ] || break
            apply_filter "$dest" "$pffdir"; r=$?
            [ $r -eq 0 -o $r -eq 1 ] && edot $r "filter $i"
            [ $r -eq 2 ] && r=0; [ $r -eq 0 ] || break
            add_global__link "$dest" "$pffdir"
            edot $r "add_global $i"; [ $r -eq 0 ] || break
        done
        [ $r -eq 0 ] || { eend $r; return 1; }
        for i in "${updates[@]}"; do
            # du fait qu'on intègre fdeletes dans fupdates, il est possible que
            # le fichier n'existe pas dans la source. de plus, l'association
            # dans losrcs n'existe pas non plus. dans ce cas, il suffit de
            # d'ignorer le fichier
            if [ -n "${losrcs[$i]+set}" ]; then
                src="$srcdir/${losrcs[$i]}"
            else
                src="$srcdir/$i"
            fi
            vlfile="${i}__pv-${version}__"
            dest="$pffdir/pff/Base/$vlfile"
            if flexists "$src"; then
                mkdirof "$dest"; r=$?
                edot $r "mkdirof $vlfile"; [ $r -eq 0 ] || break
                cp "$src" "$dest"; r=$?
                edot $r "create $vlfile"; [ $r -eq 0 ] || break
                apply_filter "$dest" "$pffdir" "$pffdir/pff/Base/$i"; r=$?
                [ $r -eq 0 -o $r -eq 1 ] && edot $r "filter $vlfile"
                [ $r -eq 2 ] && r=0; [ $r -eq 0 ] || break
                array_add vlfiles "$vlfile"
            fi
        done
        [ $r -eq 0 ] || { eend $r; return 1; }
        eend $r
    fi

    sync_vlfiles "$pffdir" "" "${vlfiles[@]}"
    array_addu PVERSIONS "$version"
    conf_update "$pffdir/$PFFCONF" PVERSIONS

    ## Fin du traitement
    ac_clean "$srcdir" "$workdir"

    if [ -n "$autopatch" ]; then
        # lancer la commande patch pour intégrer les modifications
        ask_yesno "Voulez-vous passer à l'intégration de cette version?" O &&
            patch_cmd "$merge_strategy" "$commit_policy" "$pffdir"
    fi
}

#===========================================================
# pff --patch

function patch_cmd() {
    local merge_strategy="$1" commit_policy="$2"; shift; shift
    local pffdir="$1"
    local gitproject was_patched eop_version
    local version profile rcfile curdir workdir controlfile control tmpfile
    local -a profiles vlfiles tmpfiles

    ensure_pffdir pffdir "$pffdir"

    # est-ce un projet suivi dans git?
    (cd "$pffdir"; git_check_gitvcs) && gitproject=1 || gitproject=

    array_from_lines profiles "$(get_user_profiles "$pffdir")"
    if array_contains profiles Common; then
        # toujours traiter Common en dernier
        array_del profiles Common
        array_add profiles Common
    fi

    case "$merge_strategy" in
    ours) merge_strategy=(-s recursive -X ours);;
    theirs) merge_strategy=(-s recursive -X theirs);;
    *) merge_strategy=();;
    esac

    while true; do
        # si pas de version en cours, il n'y a rien à patcher
        [ ${#PVERSIONS[*]} -gt 0 ] || break

        eop_version=
        for version in "${PVERSIONS[@]}"; do
            have_profile_vlfiles=
            have_base_vlfiles=
            for profile in "${profiles[@]}"; do
                array_from_lines vlfiles "$(get_vlfiles_nostrip "$pffdir" "" "$profile" "$version")"
                # filtrer les fichiers de NOMERGES
                mergefiles=()
                for vlfile in "${vlfiles[@]}"; do
                    setx rfile=get_rfile "${vlfile%__pv-${version}__}" "$pffdir"
                    is_nomerge "$rfile" "$pffdir" && continue
                    array_add mergefiles "$vlfile"
                done
                vlfiles=("${mergefiles[@]}")
                # on en a trouvé
                if [ ${#vlfiles[*]} -gt 0 ]; then
                    have_profile_vlfiles=1
                    break
                fi
            done
            [ -n "$have_profile_vlfiles" ] && break
            array_from_lines vlfiles "$(get_vlfiles_nostrip "$pffdir" "" Base "$version")"
            # filtrer les fichiers de NOMERGES
            mergefiles=()
            for vlfile in "${vlfiles[@]}"; do
                setx rfile=get_rfile "${vlfile%__pv-${version}__}" "$pffdir"
                is_nomerge "$rfile" "$pffdir" && continue
                array_add mergefiles "$vlfile"
            done
            vlfiles=("${mergefiles[@]}")
            have_base_vlfiles=1
            break
        done
        if [ -n "$have_profile_vlfiles" ]; then
            # il faut patcher les fichiers du profil
            etitle "Intégration de la version $version dans le profil $profile"
            [ -n "$rcfile" ] || ac_set_tmpfile rcfile
            [ -n "$controlfile" ] || ac_set_tmpfile controlfile
            [ -n "$workdir" ] && ac_clean "$workdir"
            ac_set_tmpdir workdir
            curdir="$(pwd)"
            cd "$workdir"
            git init -q .
            git checkout -q --orphan upstream
            # rajouter les fichiers de Base dans la branche upstream
            for vlfile in "${vlfiles[@]}"; do
                setx bfile=get_bfile "${vlfile%__pv-${version}__}" "$pffdir"
                setx rfile=get_rfile "$bfile" "$pffdir"
                mkdirof "$rfile"
                cp -a "$bfile" "$rfile"
                chmod +w "$rfile"
            done
            git add -A
            [ -n "$(git status --porcelain)" ] && git commit -qm "Base"
            # créer la branche v$version depuis upstream
            git checkout -qb "v$version"
            # rajouter les fichiers de Common s'ils existent (sauf si la branche c'est Common ^^)
            if [ "$profile" != Common ]; then
                for vlfile in "${vlfiles[@]}"; do
                    setx Cfile=get_Cfile "${vlfile%__pv-${version}__}" "$pffdir"
                    if flexists "$Cfile"; then
                        setx rfile=get_rfile "$Cfile" "$pffdir"
                        mkdirof "$rfile"
                        cp -a "$Cfile" "$rfile"
                    fi
                done
                git add -A
                [ -n "$(git status --porcelain)" ] && git commit -qm "Common"
            fi
            # rajouter les fichiers du profil
            for vlfile in "${vlfiles[@]}"; do
                setx pfile=get_pfile "${vlfile%__pv-${version}__}" "$profile" "$pffdir"
                setx rfile=get_rfile "$pfile" "$pffdir"
                mkdirof "$rfile"
                cp -a "$pfile" "$rfile"
            done
            git add -A
            [ -n "$(git status --porcelain)" ] && git commit -qm "$profile"
            # rebasculer vers upstream et rajouter les fichiers de patch
            git checkout -q upstream
            for vlfile in "${vlfiles[@]}"; do
                setx rfile=get_rfile "${vlfile%__pv-${version}__}" "$pffdir"
                mkdirof "$rfile"
                cp -L "$vlfile" "$rfile"
            done
            git add -A
            [ -n "$(git status --porcelain)" ] && git commit -qm "$version"
            # basculer vers la branche de version et tenter de merger upstream dans version
            git checkout -q "v$version"
            if git merge -q "${merge_strategy[@]}" --no-commit upstream; then
                # tout s'est bien passé
                [ -n "$(git status --porcelain)" ] && git commit -qm "v$version --> $profile"
            else
                # il y a eu une erreur. laisser l'utilisateur décider quoi faire
                echo >"$rcfile" "#
[ -f /etc/bash.bashrc ] && . /etc/bash.bashrc
[ -f ~/.bashrc ] && . ~/.bashrc
$(qvals source "$ULIBDIR/ulib")
urequire DEFAULTS
function Commit() { echo COMMIT >$(qvals "$controlfile"); exit; }
# raccourcis pour Commit
function C() { Commit \"\$@\"; }; function c() { Commit \"\$@\"; }; function commit() { Commit \"\$@\"; }
function Abort() { echo ABORT >$(qvals "$controlfile"); exit; }
# raccourcis pour Abort
function A() { Abort \"\$@\"; }; function a() { Abort \"\$@\"; }; function abort() { Abort \"\$@\"; }
$(qvals cd "$workdir")
$(qvals eerror "Une erreur s'est produite: consultez les fichiers concernés et faites les corrections nécessaires. En cas de conflit, vous devez les résoudre en examinant les lignes marquées
    <<<<<<< HEAD
    =======
    >>>>>>> upstream
puis en gardant la partie HEAD pour les modifications locales et upstream pour les modifications entrantes")
$(qvals eimportant "Puis tapez ${COULEUR_VERTE}Commit${COULEUR_NORMALE} pour valider l'intégration de cette version")
$(qvals eimportant "Sinon, tapez ${COULEUR_ROUGE}Abort${COULEUR_NORMALE} pour arrêter l'intégration de cette version")
"
                >"$controlfile"
                "${SHELL:-bash}" --rcfile "$rcfile"
                control="$(<"$controlfile")"
                if [ -z "$control" ]; then
                    # demander à l'utilisateur sa décision
                    eerror "Vous n'avez pas terminé votre session par ${COULEUR_VERTE}Commit${COULEUR_NORMALE} ou ${COULEUR_ROUGE}Abort${COULEUR_NORMALE}"
                    ask_yesno "Voulez-vous valider l'intégration de la version?" N && control=COMMIT || control=ABORT
                fi
                if [ "$control" == COMMIT ]; then
                    array_from_lines tmpfiles "$(git status --porcelain | awk '{print substr($0, 4)}')"
                    for tmpfile in "${tmpfiles[@]}"; do
                        if grep -qE '^(<<<<<<<|=======|>>>>>>>)' "$tmpfile"; then
                            ewarn "Vous avez choisi de valider l'intégration des modifications mais..."
                            ewarn "$tmpfile: ce fichier semble encore contenir des marques de conflit"
                            if ! ask_yesno -y "Etes-vous sûr d'avoir correctement édité le fichier?" N; then
                                eecho "Annulation de l'intégration de la version"
                                control=ABORT
                                break
                            fi
                        fi
                    done
                fi
                if [ "$control" == ABORT ]; then
                    enote "Vous pouvez reprendre l'intégration de la version $version avec la commande    pff -g"
                    break # abort
                fi
            fi
            # récupérer les versions modifiées et supprimer les fichiers de patch
            for vlfile in "${vlfiles[@]}"; do
                setx pfile=get_pfile "${vlfile%__pv-${version}__}" "$profile" "$pffdir"
                setx rfile=get_rfile "$pfile" "$pffdir"
                cp -a "$rfile" "$pfile"
                rm "$vlfile"
            done
            cd "$curdir"
            eend
        elif [ -n "$have_base_vlfiles" ]; then
            # il faut intégrer la nouvelle version dans Base
            etitle "Finaliser intégration de la version $version"
            for vlfile in "${vlfiles[@]}"; do
                bfile="${vlfile%__pv-${version}__}"
                chmod +w "$bfile"
                mv "$vlfile" "$bfile"
                chmod a-w "$bfile"
            done
            eop_version=1
            VERSION="$version"
            array_del PVERSIONS "$VERSION"
            conf_update "$pffdir/$PFFCONF" VERSION PVERSIONS
            eend
        fi

        if [ -n "$gitproject" ]; then
            local commit default
            if [ -n "$eop_version" ]; then
                msg="Intégration de la version $version"
            else
                msg="Correction du profil $profile pour la version $version"
            fi
            commit="$commit_policy"
            if [ "$commit" == ask ]; then
                if [ -n "$eop_version" ]; then
                    enote "Vous avez terminé l'intégration des patches de la version $version"
                    default=O
                else
                    einfo "Vous avez intégré les patches de la version $version pour le profil $profile"
                    default=N
                fi
                ask_yesno "Voulez-vous enregistrer les modifications dans git?" $default || commit=
            fi
            if [ -n "$commit" ]; then
                git add -A && git commit -m "$msg" || return
                if [ -z "$UTOOLS_VCS_OFFLINE" -a -n "$(git_get_branch_remote)" ]; then
                    git push
                fi
            fi
        fi
    done
}

#===========================================================
# pff --add-global

function add_global__link() {
    local pfile="$1" pffdir="$2"
    local rfile bfile cfile plink clink tmp

    setx rfile=get_rfile "$pfile" "$pffdir" || return
    flexists "$pffdir/$rfile" || touch "$pffdir/$rfile"

    setx bfile=get_bfile "$pfile" "$pffdir"
    setx cfile=get_cfile "$pfile" "$pffdir"
    setx plink=multiups "$rfile" "pff/Current/$rfile"
    setx clink=multiups "Current/$rfile" "Base/$rfile"
    if flexists "$bfile" && [ -L "$pfile" -a -L "$cfile" ]; then
        : # ok pour les liens
    else
        # Création des liens pour $rfile
        mkdirof "$bfile" || return
        mv "$pfile" "$bfile" || return
        chmod a-w "$bfile" || return
        [ -L "$pfile" ] || ln -sf "$plink" "$pfile" || return
        mkdirof "$cfile" || return
        [ -L "$cfile" ] || ln -sf "$clink" "$cfile" || return
    fi
    # correction éventuelle du lien existant
    setx tmp=readlink "$pfile"
    [ "$tmp" == "$plink" ] || ln -sfT "$plink" "$pfile"
    return 0
}

function add_global_cmd() {
    local pffdir file pfile

    ensure_pffdir pffdir
    for file in "$@"; do
        if [ -d "$file" ]; then
            ewarn "$file: est un répertoire. argument ignoré"
            continue
        elif flexists "$file"; then
            :
        else
            ewarn "$file: fichier introuvable"
            ask_yesno "Voulez-vous le créer?" O || continue
        fi
        setx pfile=abspath "$file"
        setx rfile=get_rfile "$pfile" || {
            eerror "$file: chemin invalide. argument ignoré"
            continue
        }
        add_global__link "$pfile" "$pffdir" || return
    done
}

#===========================================================
# pff --locals

function list_locals_cmd() {
    local pffdir="$1"

    ensure_pffdir pffdir "$pffdir"
    get_local_files "$pffdir"
}

#===========================================================
# pff --profiles

function list_profiles_cmd() {
    local pffdir="$1"

    ensure_pffdir pffdir "$pffdir"
    get_profiles "$pffdir"
}

#===========================================================
# pff --switch

function switch_cmd() {
    local profile="$1" pffdir="$2"
    local -a profiles

    ensure_pffdir pffdir "$pffdir"
    autoinit "$pffdir"
    [ -n "$profile" ] || setx profile=get_first_profile "$pffdir"

    setx -a profiles=get_profiles "$pffdir"
    if ! array_contains profiles "$profile"; then
        if ! array_contains PROFILES "$profile"; then
            ewarn "$profile: ce profil n'existe pas"
            ask_yesno "Voulez-vous le créer?" O || return 1

            array_addu PROFILES "$profile"
            conf_update "$pffdir/$PFFCONF" PROFILES
        fi
    fi
    enote "Sélection du profil $profile"
    select_profile "$profile" "$pffdir"
}

#===========================================================
# pff --add-local

function add_local__link() {
    local pfile="$1" profile="$2" pffdir="$3"
    local rfile bfile pfile Pfile plink Plink
    # vérifier validité du chemin
    setx rfile=get_rfile "$pfile" "$pffdir" || return
    flexists "$pffdir/$rfile" || touch "$pffdir/$rfile"
    # vérifier la présence du lien global
    setx bfile=get_bfile "$pfile" "$pffdir"
    if ! flexists "$bfile"; then
        add_global__link "$pfile" "$pffdir" || return
    fi
    # vérifier la présence du lien local
    setx Pfile=get_pfile "$pfile" "$profile" "$pffdir"
    flexists "$Pfile" && return 0
    # créer le fichier local
    setx Cfile=get_Cfile "$pfile" "$pffdir"
    mkdirof "$Pfile"
    if flexists "$Cfile"; then
        # copier depuis le profil Common par défaut
        cp "$Cfile" "$Pfile"
    else
        cp "$bfile" "$Pfile"
        chmod +w "$Pfile"
    fi
    # mettre à jour le profil courant
    setx cfile=get_cfile "$pfile" "$pffdir"
    setx clink=multiups "Current/$rfile" "$profile/$rfile"
    ln -sf "$clink" "$cfile"
    # intégrer les fichiers de version
    local -a vlfiles
    array_from_lines vlfiles "$(get_vlfiles "$pffdir" "$rfile")"
    sync_vlfiles "$pffdir" "$profile" "${vlfiles[@]}"
    return 0
}

function add_local_cmd() {
    local pffdir file rfile pfile
    local profile r

    ensure_pffdir pffdir
    setx profile=get_current_profile "$pffdir"
    r=0
    for file in "$@"; do
        if [ -d "$file" ]; then
            ewarn "$file: est un répertoire. argument ignoré"
            continue
        elif flexists "$file"; then
            :
        else
            ewarn "$file: fichier introuvable"
            ask_yesno "Voulez-vous le créer?" O || continue
        fi
        setx pfile=abspath "$file"
        setx rfile=get_rfile "$pfile" || {
            eerror "$file: chemin invalide. argument ignoré"
            r=1
            continue
        }
        add_local__link "$pfile" "$profile" "$pffdir" || {
            ewarn "$file: erreur lors de la création du lien local"
            r=1
            continue
        }
    done
    return $r
}

#===========================================================
# pff --edit

function edit_cmd() {
    local profile="$1"; shift
    local pffdir file rfile pfile Pfile r
    local -a profiles edits args

    ensure_pffdir pffdir
    array_from_lines profiles "$(get_user_profiles "$pffdir")"
    [ -n "$profile" ] || setx profile=get_current_profile "$pffdir"
    enote "Dans le profil $profile:"

    r=0
    while [ $# -gt 0 ]; do
        if [[ "$1" == -* ]]; then
            # nouvelle option
            local prev_profile="$profile"
            args=(+ -p:,--profile: profile=)
            parse_args "$@"; set -- "${args[@]}"
            [ "$profile" != "$prev_profile" ] && enote "Dans le profil $profile:"
            continue
        fi
        file="$1"; shift

        if [ -d "$file" ]; then
            ewarn "$file: est un répertoire. argument ignoré"
            continue
        fi
        setx pfile=abspath "$file"
        setx rfile=get_rfile "$pfile" || {
            eerror "$file: chemin invalide. argument ignoré"
            r=1
            continue
        }
        if [ "$profile" == ALL ]; then
            local tmpp
            for tmpp in "${profiles[@]}"; do
                setx Pfile=get_pfile "$pfile" "$tmpp" "$pffdir"
                [ -e "$Pfile" ] || continue
                estep "Edition de $(ppath "$Pfile")"
                array_add edits "$Pfile"
            done
        else
            add_local__link "$pfile" "$profile" "$pffdir" || {
                ewarn "$file: erreur lors de la création du lien local"
                r=1
                continue
            }
            setx Pfile=get_pfile "$pfile" "$profile" "$pffdir"
            estep "Edition de $(ppath "$Pfile")"
            array_add edits "$Pfile"
        fi
    done

    if [ ${#edits[*]} -gt 0 ]; then
        "${EDITOR:-vi}" "${edits[@]}" || r=$?
    else
        r=2
    fi
    return $r
}

#===========================================================
# pff --diff

function diff_cmd() {
    local list_names="$1"; shift

    local pffdir profile srcp destp desc
    case $# in
    0)
        ensure_pffdir pffdir
        setx srcp=get_current_profile "$pffdir"
        destp=
        ;;
    1)
        if withpath "$1"; then
            ensure_pffdir pffdir "$1"
            setx srcp=get_current_profile "$pffdir"
            destp=
        else
            ensure_pffdir pffdir
            setx srcp=get_current_profile "$pffdir"
            destp="$1"
        fi
        ;;
    2)
        if withpath "$2"; then
            ensure_pffdir pffdir "$2"
            setx srcp=get_current_profile "$pffdir"
            destp="$1"
        else
            ensure_pffdir pffdir
            srcp="$1"
            destp="$2"
        fi
        ;;
    *)
        ensure_pffdir pffdir "$3"
        srcp="$1"
        destp="$2"
        ;;
    esac
    [ "$destp" == "Common|Base" ] && destp=
    if [ -z "$destp" ]; then
        if [ "$srcp" == Base -o "$srcp" == Common ]; then
            desc="pff --diff $srcp Base"
        else
            desc="pff --diff $srcp ${destp:-"Common|Base"}"
        fi
    else
        desc="pff --diff $srcp $destp"
    fi

    local -a lfiles; local rfile bfile Cfile srcfile destfile
    setx -a lfiles=get_local_files "$pffdir"

    local -a diffcolor
    isatty && diffcolor=(--color) || diffcolor=(--no-color)
    for rfile in "${lfiles[@]}"; do
        setx srcfile=get_pfile "$pffdir/$rfile" "$srcp" "$pffdir"
        flexists "$srcfile" || continue
        if [ -n "$destp" ]; then
            setx destfile=get_pfile "$pffdir/$rfile" "$destp" "$pffdir"
        else
            setx Cfile=get_Cfile "$pffdir/$rfile" "$pffdir"
            setx bfile=get_bfile "$pffdir/$rfile" "$pffdir"
            # si on précise pas le profil destination, sélectionner dans l'ordre
            # Common ou Base
            if [ "$srcp" == Base -o "$srcp" == Common ]; then
                destfile="$bfile"
            elif flexists "$Cfile"; then
                destfile="$Cfile"
            else
                destfile="$bfile"
            fi
        fi
        if [ -n "$list_names" ]; then
            diff -q "$destfile" "$srcfile" >&/dev/null || echo "$srcfile"
        else
            [ -n "$desc" ] && echo "$desc"
            desc=
            if [ -n "$PFF_USE_REGULAR_DIFF" ]; then
                # au cas où git n'est pas disponible
                diff -ur "$destfile" "$srcfile"
            else
                git diff "${diffcolor[@]}" --no-index "$destfile" "$srcfile"
            fi
        fi
    done | page_maybe
}

#===========================================================
# pff --infos

function infos_cmd() {
    local show_all="$1"; shift
    local pffdir="$1"
    local -a profiles vlfiles
    local rfile Pfile flag

    if find_pffdir pffdir "$pffdir"; then
        ensure_pffdir pffdir "$pffdir"
        autofix "$pffdir"
        setx -a lfiles=get_local_files "$pffdir"
        setx profile=get_current_profile "$pffdir"
        setx -a profiles=get_profiles "$pffdir"

        if [ ${#lfiles[*]} -gt 0 ]; then
            [ ${#lfiles[*]} -gt 1 ] && estep "${#lfiles[*]} fichiers locaux" || estep "1 fichier local"
            for rfile in "${lfiles[@]}"; do
                setx -a vlfiles=get_vlfiles "$pffdir" "$rfile" "$profile"
                setx Pfile=get_pfile "$pffdir/$rfile" "$profile" "$pffdir"
                setx Cfile=get_Cfile "$pffdir/$rfile" "$pffdir"
                if [ ${#vlfiles[*]} -gt 0 ]; then
                    flag="${COULEUR_ROUGE}P${COULEUR_NORMALE} "
                elif [ -f "$Pfile" ]; then
                    flag="${COULEUR_BLEUE}*${COULEUR_NORMALE} "
                elif [ "$profile" != Common -a -f "$Cfile" ]; then
                    flag="$(get_color YELLOW)C${COULEUR_NORMALE} "
                elif [ -z "$show_all" ]; then
                    continue
                else
                    flag="  "
                fi
                uecho "  $flag$rfile"
            done
        else
            estep "Pas de fichiers locaux définis"
        fi
        estep "Version courante: ${COULEUR_BLEUE}$VERSION${COULEUR_NORMALE}"
        estep "Versions en attente: ${COULEUR_ROUGE}${PVERSIONS[*]}${COULEUR_NORMALE}"
        [ -n "$profile" ] && estep "Profil courant: ${COULEUR_BLEUE}$profile${COULEUR_NORMALE}" || estep "${COULEUR_JAUNE}Pas de profil courant${COULEUR_NORMALE}"
        [ ${#profiles[*]} -gt 0 ] && estep "Profils valides: ${profiles[*]}" || estep "Pas de profils définis"
    elif [ -f "${pffdir:-.}/$PFFCONF" ]; then
        : "${pffdir:=.}"
        conf_init "${PFFCONFVARS[@]}"
        source "$pffdir/$PFFCONF"
        estep "[Projet pff déployé] Version courante: ${COULEUR_BLEUE}$VERSION${COULEUR_NORMALE}"
    fi
}

################################################################################
# Programme principal

for script_alias in "${SCRIPT_ALIASES[@]}"; do
    splitpair "$script_alias" src option
    if [ "$scriptname" == "$src" ]; then
        eval "set -- $option \"\$@\""
        break
    fi
done

QUIET=1 # masquer les messages de git et rsync?
VERYQUIET=1 # masquer les messages de git commit?

parse_mode=
if [ "$1" == -e -o "$1" == --edit ]; then
    parse_mode=+
fi

action=infos
autopatch=1
version=
disttype=
ORIGEXTS=("${DEFAULT_ORIGEXTS[@]}")
unwrap=auto
merge_strategy=
commit_policy=ask
profile=
alternate=
args=($parse_mode
    --help '$exit_with display_help'
    -0,--init action=init
    --s:,--origext: '$add@ ORIGEXTS'
    --k,--clear-origexts '$ORIGEXTS=()'
    -N,--new action=new
    -M,--new-only '$action=new; autopatch='
    -V:,--version: version=
    --auto-archive disttype=auto
    -F,--full-archive disttype=full
    -H,--patch-archive disttype=patch
    --auto-unwrap unwrap=auto
    --no-unwrap unwrap=
    -E,--unwrap unwrap=1
    -g,--patch action=patch
    -o,--ours merge_strategy=ours
    --theirs merge_strategy=theirs
    --ask-commit commit_policy=ask
    -c,--comit commit_policy=1
    --no-commit commit_policy=
    -b,--add-global action=add-global
    --locals action=list-locals
    --profiles action=list-profiles
    -s,--switch action=switch
    -a,--add-local action=add-local
    -e,--edit action=edit
    -p:,--profile: profile=
    -P,--prod profile=prod
    -T,--test profile=test
    -A,--all-profiles profile=ALL
    -d,--diff action=diff
    --infos action=infos
    -l,--list-names,--show-all alternate=1
)
parse_args "$@"; set -- "${args[@]}"

array_fix_paths ORIGEXTS

case "$action" in
init) init_cmd "$@";;
new) new_cmd "$autopatch" "$version" "$disttype" "$unwrap" "$merge_strategy" "$commit_policy" "$@";;
patch) patch_cmd "$merge_strategy" "$commit_policy" "$@";;
add-global) add_global_cmd "$@";;
list-locals) list_locals_cmd "$@";;
list-profiles) list_profiles_cmd "$@";;
switch) switch_cmd "$@";;
add-local) add_local_cmd "$@";;
edit) edit_cmd "$profile" "$@";;
diff) diff_cmd "$alternate" "$@";;
infos) infos_cmd "$alternate" "$@";;
esac