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

function display_help() {
    uecho "$scriptname: gestion d'une distribution upstream

Des fichiers de configuration (par exemple) sont distribués par un partenaire,
et il faut maintenir des modifications locales, tout en acceptant les mises à
jour provenant 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' ou 'file.default'
La liste des extensions reconnues est spécifiée avec l'option -s. Lors de leur
intégration dans le répertoire local, ces fichiers sont copiés sans cette
extension.

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'.

USAGE
    $scriptname cmd [options]

OPTIONS COMMUNES
    -s .EXT
        Ajouter une extension à la liste des extensions reconnues comme contenu
        original modifiable dans la distribution upstream. Par défaut, les
        extensions suivantes sont reconnues:
            ${DEFAULT_ORIGEXTS[*]}
        Cette option peut être utilisée autant de fois que nécessaire.
    --clear-origexts
        Supprimer la liste par défaut des extensions origines. Cette option doit
        être spécifiée avant l'option -s 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)
    -d WORKDIR
        Spécifier le répertoire de travail. Par défaut, la racine du répertoire
        de travail est cherchée à partir du répertoire courant.
    --help
        Afficher l'aide détaillée de la commande spécifiée

COMMANDES
    init [WORKDIR [ARCHIVE]]
        Initialiser un répertoire de travail pour contenir une distribution
        upstream

    upstream-new, new SRCDIR|ARCHIVE [WORKDIR]
        Intégrer une nouvelle distribution upstream.
        Les nouveaux fichiers sont copiés tout de suite dans le répertoire de
        travail. Par contre, les modifications ne sont intégrées qu'avec la
        commande patch
    upstream-clear, clear [WORKDIR]
        Supprimer tous les fichiers non modifiés de l'upstream.

    local-create, create FILE
        Créer et/ou identifier FILE comme une modification locale par rapport à
        l'upstream.
    local-edit, edit FILE
        S'assurer que local-create a été exécuté si nécessaire pour FILE, puis
        l'éditer avec $EDITOR

    local-copy, cp SRCFILE DESTFILE
    local-move, mv SRCFILE DESTFILE
    local-remove, rm FILE
        Frontend pour respectivement cp, mv et rm. Ces commandes agissent aussi
        sur les fichiers orig et de tag.

    local-tag, tag FILE TAG
        Faire une copie du fichier local avec le tag spécifié. Si le fichier de
        tag existe déjà, il est écrasé.
    local-switch, switch TAG FILE
        Sélectionner la copie avec le tag spécifié.

    local-put, put [WORKDIR] DESTDIR
        Copier tous les fichiers locaux dans DESTDIR, par exemple pour faire une
        sauvegarde. Si DESTDIR n'est pas spécifié, prendre la valeur de
        l'origine, affichée par local-list
    local-get, get [-l|-s] SRCDIR [WORKDIR]
        Opération inverse de local-put: intégrer tous les fichiers de SRCDIR
        comme fichiers locaux. Si SRCDIR n'est pas spécifié, prendre la valeur
        de l'origine, affichée par local-list
    local-list, list [WORKDIR]
        Lister tous les fichiers locaux. Les fichiers pour lesquels il faut
        intégrer une modification de l'upstream avec la commande local-patch
        sont identifiés visuellement.

    upstream-diff, udiff [FILE]
        Après intégration d'une nouvelle distribution upstream, afficher les
        modifications entre la nouvelle distribution upstream et l'ancienne pour
        tous les fichiers modifiables
    local-diff, ldiff [FILE]
        Afficher les modifications locales par rapport à l'upstream pour le(s)
        fichier(s) spécifié(s).
    local-patch, lpatch [FILE]
        Après intégration d'une nouvelle distribution upstream, appliquer au(x)
        le(s) fichier(s) spécifié(s) les modifications disponibles affichées par
        upstream-diff.
    local-forget, lforget [FILE]
        Après intégration d'une nouvelle distribution upstream, oublier les
        modifications disponibles pour le(s) fichier(s) spécifié(s)."
}

SCRIPT_ALIASES=(#alias:command
    udn:upstream-new
    udc:local-create
    ude:local-edit
    uds:local-switch
    udl:local-list
    udd:local-diff
    udp:local-patch
)
CMD_ALIASES=(
    unew:upstream-new new:upstream-new
    uclear:upstream-clear clear:upstream-clear
    udiff:upstream-diff
    lc:local-create lcreate:local-create c:local-create create:local-create
    le:local-edit ledit:local-edit e:local-edit edit:local-edit
    cp:local-copy copy:local-copy
    mv:local-move move:local-move
    rm:local-remove remove:local-remove delete:local-remove del:local-remove
    lt:local-tag ltag:local-tag t:local-tag tag:local-tag
    lswitch:local-switch s:local-switch sw:local-switch switch:local-switch
    lput:local-put put:local-put push:local-put
    lget:local-get get:local-get pull:local-get
    link:"local-get -l" ln:"local-get -l"
    ll:local-list llist:local-list list:local-list ls:local-list l:local-list
    ldiff:local-diff diff:local-diff
    lpatch:local-patch patch:local-patch
    lforget:local-forget forget:local-forget
)
DEFAULT_CMD=local-list

ORIGEXT=udist
DEFAULT_ORIGEXTS=(".$ORIGEXT" .origine .default)
ORIGEXTS=("${DEFAULT_ORIGEXTS[@]}")

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
fi

source "$(dirname "$0")/lib/ulib/ulib" || exit 1
urequire DEFAULTS conf install
INSTALL_VERBOSE=
INSTALL_USES_PPATH=1

# Traduire le nom du script
for script_alias in "${SCRIPT_ALIASES[@]}"; do
    splitpair "$script_alias" src dest
    if [ "$scriptname" == "$src" ]; then
        eval "set -- $dest \"\$@\""
        scriptname=udist
        break
    fi
done

[ -n "$EDITOR" ] || EDITOR=vi
UDISTOPTS=(
    -s:,--ext:,--origext: '$add@ ORIGEXTS'
    --clear-origexts '$ORIGEXTS=()'
    -d:,--workdir: workdir=
)

# Terminologie utilisée dans le code:
# - un fichier default est un fichier de la distribution upstream ayant une des
#   extensions du tableau ORIGEXTS, ce qui en fait un candidat automatique pour
#   avoir une copie locale. Les fichiers default sont de la forme 'NAME.origext'
# - un fichier local est un fichier auquel est associé un fichier orig. Les
#   fichier locaux sont de la forme 'NAME'. En général, un fichier local a un
#   fichier default correspondant dans la distribution upstream.
# - un fichier orig est la copie originale d'un fichier de la distribution
#   upstream. Ce fichier est associé à un fichier local qui contient ses
#   modifications locales. Les fichiers orig sont versionnés tant qu'ils ne sont
#   pas intégrés et sont de la forme '.--NAME--[version].udist'
# - un fichier de tag est un fichier associé à un fichier local et qui a un tag
#   associé. Ceci permet de maintenir plusieurs "versions" d'un même fichier
#   local, e.g. "dev", "prod", etc.

function udist_local() {
    utools_local
    echo "local workdir"
}

function __check_workdir() {
    [ -f "$1/.udist" ]
}

function find_workdir() {
    local workdir="$(abspath "${1:-.}")"
    while [ "$workdir" != "/" ]; do
        if __check_workdir "$workdir"; then
            echo "$workdir"
            return 0
        fi
        workdir="$(dirname "$workdir")"
    done
    return 1
}

function __after_parse() {
    array_fix_paths ORIGEXTS
    [ -n "${ORIGEXTS[*]}" ] || ORIGEXTS=(".$ORIGEXT")
    if [ "$1" != "initial" ]; then
        [ -z "$workdir" -o -d "$workdir" ] || die "$workdir: répertoire introuvable"
        workdir="$(find_workdir "$workdir")" || die "Impossible de trouver le répertoire de travail. Faut-il faire '$scriptname init'?"
        workdir="$(abspath "$workdir")"
    fi
}

function __udist_initial() {
    echo "# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
VERSION=
VERSIONS=()
TAGS=()
ORIGIN=
LOCALS=()"
}

function __udist_locals() {
    # afficher les variables définies dans le fichier .udist
    echo "local VERSION ORIGIN; local -a VERSIONS TAGS LOCALS"
}

function __load_udist_nolocal() { (
    # charger le fichier .udist ($1) et affiche les commandes pour définir les
    # variables PVERSION, PVERSIONS, PTAGS, PORIGIN, PLOCALS.
    # P ($2) est un préfixe éventuel pour le nom des variables
    VERSION=; VERSIONS=(); TAGS=(); ORIGIN=; LOCALS=()

    local udistf="${1:-$workdir}"
    [ -d "$udistf" ] && udistf="$udistf/.udist"
    source "$udistf"

    set_var_cmd "${2}VERSION" "$VERSION"
    set_array_cmd "${2}VERSIONS" VERSIONS
    set_array_cmd "${2}TAGS" TAGS
    set_var_cmd "${2}ORIGIN" "$ORIGIN"
    set_array_cmd "${2}LOCALS" LOCALS
); }

function __load_udist() {
    __udist_locals
    __load_udist_nolocal "$@"
}

function __save_udist() {
    # écrire dans le fichier .udist ($1) les variables spécifiées, parmi
    # VERSION, VERSIONS, TAGS, ORIGIN, LOCALS. Les arguments sont de la forme
    # DEST[:varname], e.g.
    #     __save_udist .udist VERSION:value ORIGIN
    # qui met à jour VERSION avec $value et ORIGIN avec $ORIGIN
    local __su_udistf="${1:-$workdir}"; shift
    [ -d "$__su_udistf" ] && __su_udistf="$__su_udistf/.udist"
    local __su_part __su_dest __su_varname
    local -a __su_cmd
    __su_cmd=(conf_enable "$__su_udistf")
    for __su_dest in "$@"; do
        splitpair "$__su_dest" __su_dest __su_varname
        [ -n "$__su_varname" ] || __su_varname="$__su_dest"
        case "$__su_dest" in
        VERSION) array_add __su_cmd "$(set_var_cmd VERSION "${!__su_varname}")";;
        VERSIONS) array_add __su_cmd "$(set_array_cmd VERSIONS "${__su_varname}")";;
        TAGS) array_add __su_cmd "$(set_array_cmd TAGS "${__su_varname}")";;
        ORIGIN) array_add __su_cmd "$(set_var_cmd ORIGIN "${!__su_varname}")";;
        LOCALS) array_add __su_cmd "$(set_array_cmd LOCALS "${__su_varname}")";;
        esac
    done
    "${__su_cmd[@]}"
}

function __is_default() {
    # tester si $1 est un fichier de la forme name.origext
    local origext ufile lfile
    for origext in "${ORIGEXTS[@]}"; do
        ufile="$1"
        lfile="${ufile%$origext}"
        [ "$ufile" != "$lfile" ] && return 0
    done
    return 1
}

function __get_local() {
    # tester si $1 est un fichier de la form 'NAME.origext'. Dans ce cas,
    # afficher 'NAME'. Sinon, afficher la valeur inchangée.
    # note: cette fonction fait la même chose que __is_default, sauf qu'elle
    # affiche le nom local correspondant au nom upstream
    local origext ufile lfile
    for origext in "${ORIGEXTS[@]}"; do
        ufile="$1"
        lfile="${ufile%$origext}"
        [ "$ufile" != "$lfile" ] && {
            echo "$lfile"
            return 0
        }
    done
    echo "$1"
    return 1
}

function __get_orig() {
    # obtenir le fichier orig correspondant à $1, i.e. afficher .--NAME--.udist
    # pour $1==NAME
    local dir="$(dirname "$1")" name="$(basename "$1")"
    echo "$dir/.--$name--.$ORIGEXT"
}

function __list_localfiles() {
    # lister les fichiers origs du répertoire $1 et en dériver une liste triée
    # de *noms* de fichiers locaux
    cd "$1"; find -name ".--*--*.$ORIGEXT" | filter_vcspath | while read path; do
        dir="$(dirname "$path")"
        file="$(basename "$path")"
        file="${file#.--}"
        file="${file%--*.$ORIGEXT}"
        path="$dir/$file"
        path="${path#./}"
        echo "$path"
    done | csort -u
}

function __list_tagfiles() {
    # lister les *noms* des fichiers de tag correspondant au fichier local $1
    local dir="$(dirname "$1")" name="$(basename "$1")"
    list_all "$dir" "$name.%*"
}

function __list_origfiles() {
    # afficher les *noms* des fichiers origs correspondant au fichier local $1
    local dir="$(dirname "$1")" name="$(basename "$1")"
    list_all "$dir" ".--$name--*.$ORIGEXT"
}

function __get_version() {
    # afficher la version correspondant au fichier orig $1
    local version="$(basename "$1")"
    version="${version##*--}"
    version="${version%.$ORIGEXT}"
    echo "$version"
}

function __norm_ctag() {
    # normaliser le tag composite $1: les doublons sont supprimés, et les tags
    # sont triés
    local -a tags
    array_split tags "$1" %
    array_from_lines tags "$(array_to_lines tags | csort -u)"
    array_join tags %
}

function __get_ctag() {
    # afficher le tag composite normalisé du fichier de tag $1. chaque tag est
    # séparé par %
    local tags="$(basename "$1")"
    tags="${tags##*.%}"
    tags="${tags%\%}"
    __norm_ctag "$tags"
}

function __init_tags() {
    # initialiser le tableau $1(=tags) avec la liste des tags du fichier de tag $2
    array_split "${1:-tags}" "$(__get_ctag "$2")" %
}

function __get_tagfile() {
    # afficher le nom du fichier de tag correspondant au fichier local $1 et au
    # tag $2 (qui doit avoir été normalisé). $3(=tagfiles) est le cas échéant le
    # nom du tableau contenant la liste des fichiers de tags.
    local localfile="$1" ctag="$2"
    local localdir="$(dirname "$localfile")"
    if [ -n "$3" -a "$3" != "tagfiles" ]; then
        local -a tagfiles
        array_copy tagfiles "$3"
    fi
    for tagfile in "${tagfiles[@]}"; do
        curtag="$(__get_ctag "$tagfile")"
        if [ "$curtag" == "$ctag" ]; then
            echo "$tagfile"
            return 0
        fi
    done
    return 1
}

function __get_curtagfile() {
    # afficher le nom du fichier de tag *courant* correspondant au fichier local
    # $1. $2(=tagfiles) est le cas échéant le nom du tableau contenant la liste
    # des fichiers de tags
    local localfile="$1"
    local localdir="$(dirname "$localfile")"
    if [ -n "$3" -a "$3" != "tagfiles" ]; then
        local -a tagfiles
        array_copy tagfiles "$3"
    fi
    for tagfile in "${tagfiles[@]}"; do
        if testsame "$localfile" "$localdir/$tagfile"; then
            echo "$tagfile"
            return 0
        fi
    done
    return 1
}

function is_local() {
    # tester si $1 est un fichier modifiable ayant une version originale dans
    # l'upstream
    local origext ufile lfile
    # tester d'abord si c'est un fichier de l'upstream de la forme
    # 'name.origext'
    for origext in "${ORIGEXTS[@]}"; do
        ufile="$1"
        lfile="${ufile%$origext}"
        if [ "$ufile" != "$lfile" -a -e "$lfile" ]; then
            return 0
        fi
    done
    # ensuite tester si c'est un fichier local de la forme 'name' tel que
    # 'name.origext' existe
    for origext in "${ORIGEXTS[@]}"; do
        lfile="$1"
        ufile="$lfile$origext"
        if [ -e "$lfile" -a -e "$ufile" ]; then
            return 0
        fi
    done
    return 1
}
function get_upstream() {
    # obtenir le fichier de l'upstream correspondant à $1
    local origext ufile lfile
    # tester d'abord si c'est un fichier de l'upstream de la forme
    # 'name.origext'. Afficher alors 'name.origext'
    for origext in "${ORIGEXTS[@]}"; do
        ufile="$1"
        lfile="${ufile%$origext}"
        if [ "$ufile" != "$lfile" -a -e "$lfile" ]; then
            echo "$ufile"
            return 0
        fi
    done
    # ensuite tester si c'est un fichier local de la forme 'name' tel que
    # 'name.origext' existe. Afficher alors 'name.origext'
    for origext in "${ORIGEXTS[@]}"; do
        lfile="$1"
        ufile="$lfile$origext"
        if [ -e "$lfile" -a -e "$ufile" ]; then
            echo "$ufile"
            return 0
        fi
    done
    # sinon afficher $1.origext
    echo "$1${ORIGEXTS[0]}"
    return 1
}
function get_local() {
    # obtenir le fichier local modifiable correspondant à $1
    local origext ufile lfile
    # tester d'abord si c'est un fichier de l'upstream de la forme
    # 'name.origext'. Afficher alors 'name'
    for origext in "${ORIGEXTS[@]}"; do
        ufile="$1"
        lfile="${ufile%$origext}"
        if [ "$ufile" != "$lfile" -a -e "$lfile" ]; then
            echo "$lfile"
            return 0
        fi
    done
    # ensuite tester si c'est un fichier local de la forme 'name' tel que
    # 'name.origext' existe. Afficher alors 'name'
    for origext in "${ORIGEXTS[@]}"; do
        lfile="$1"
        ufile="$lfile$origext"
        if [ -e "$lfile" -a -e "$ufile" ]; then
            echo "$lfile"
            return 0
        fi
    done
    # sinon afficher $1 inchangé
    echo "$1"
    return 1
}

function page_maybe() {
    if isatty; then
        less -XF
    else
        cat
    fi
}

################################################################################
# template à recopier et adapter pour chaque nouvelle opération

function template_help() {
    uecho "$scriptname template: 

USAGE
    $scriptname template [options]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant."
}

function template_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with template_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    #[ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    __after_parse
}

################################################################################

function init_help() {
    uecho "$scriptname init: initialiser un répertoire de travail

USAGE
    $scriptname init [WORKDIR [ARCHIVE|SRCDIR]]
    $scriptname init -d WORKDIR [ARCHIVE|SRCDIR]"
}

function init_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with init_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"
    array_fix_paths ORIGEXTS

    [ -n "$workdir" ] || { workdir="${1:-.}"; shift; }
    workdir="$(abspath "$workdir")"

    if [ ! -d "$workdir" ]; then
        ewarn "$(ppath "$workdir"): ce répertoire n'existe pas"
        ask_yesno "Voulez-vous le créer?" O || die
        mkdir -p "$workdir" || die
    fi

    if [ -f "$workdir/.udist" ]; then
        estepi "$(ppath "$workdir"): Ce répertoire est déjà initialisé pour udist"
    else
        touch "$workdir/.udist" || die
        __udist_initial >"$workdir/.udist"
        estepn "$(ppath "$workdir"): répertoire de travail initialisé avec succès"
    fi

    local archive="$1"; shift
    if [ -n "$archive" ]; then
        upstream_new_cmd -d "$workdir" "$archive"
    fi
}

################################################################################

function __unew_copy() {
    # copier les fichiers et répertoire de $1 dans $2 en identifiant les
    # fichiers originaux modifiés avec la version $3
    local srcdir="$1" destdir="$2" newversion="$3" osrcdir="$4" odestdir="$5"
    local -a srcs; local srcname src destname desto destl
    local s
    array_lsall srcs "$srcdir" "*" ".*"
    for src in "${srcs[@]}"; do
        srcname="$(basename "$src")"
        desto= # fichier orig
        destl= # fichier local
        if [ -f "$src" ]; then
            ####################################################################
            # copie d'un fichier
            if destname="$(__get_local "$srcname")"; then
                # c'est un fichier default. stratégie: ne jamais écraser le
                # fichier local & copier le fichier orig la première fois, ou
                # s'il est différent
                desto="$destdir/.--$destname--.$ORIGEXT"
                destl="$destdir/$destname"
                if [ ! -f "$desto" ]; then
                    mkdirof "$desto"
                    if [ -f "$desto" ]; then
                        if testdiff "$src" "$desto"; then
                            cat "$src" >"$desto"; edot $? "REPL ${src#$osrcdir/} --> ${desto#$odestdir/}"
                        fi
                    else
                        cp_a "$src" "$desto"; edot $? "NEW ${src#$osrcdir/} --> ${desto#$odestdir/}"
                    fi
                    [ -f "$destl" ] || {
                        cp_a "$src" "$destl"; edot $? "NEW ${src#$osrcdir/} --> ${destl#$odestdir/}"
                    }
                    continue
                fi
                # si on arrive ici, c'est qu'il y a déjà un fichier orig.
            else
                # ce n'est pas un fichier default. stratégie: n'écraser le
                # fichier local que s'il n'existe pas de fichier orig
                desto="$destdir/.--$destname--.$ORIGEXT"
                destl="$destdir/$destname"
                if [ ! -f "$desto" ]; then
                    mkdirof "$destl"
                    if [ -f "$destl" ]; then
                        if testdiff "$src" "$destl"; then
                            cat "$src" >"$destl"; edot $? "REPL ${src#$osrcdir/} --> ${destl#$odestdir/}"
                        fi
                    else
                        cp_a "$src" "$destl"; edot $? "NEW ${src#$osrcdir/} --> ${destl#$odestdir/}"
                    fi
                    continue
                fi
                # si on arrive ici, c'est qu'il y a maintenant un fichier
                # upstream pour un fichier local créé manuellement avec
                # local-create. Il faudra donc potentiellement intégrer des
                # modifications.
            fi

            # ici, le fichier upstream est potentiellement différent du fichier
            # orig déjà existant. il nous faut donc vérifier s'il y a eu
            # modification avant de copier cette nouvelle version.
            local -a versions; local destonv same version destov
            array_copy versions VERSIONS
            array_reverse versions
            destonv="$destdir/.--$destname--$newversion.$ORIGEXT"
            same=
            for version in "${versions[@]}"; do
                destov="$destdir/.--$destname--$version.$ORIGEXT"
                [ -f "$destov" ] || continue
                if testsame "$src" "$destov"; then
                    same=1
                    break
                fi
            done
            [ -n "$same" ] && continue
            testsame "$src" "$desto" && continue

            # nouveau fichier différent. il faut le versionner
            mkdirof "$destonv"
            if [ -f "$destonv" ]; then
                if testdiff "$src" "$destonv"; then
                    cat "$src" >"$destonv"; edot $? "REPL ${src#$osrcdir/} --> ${destonv#$odestdir/}"
                fi
            else
                cp_a "$src" "$destonv"; edot $? "NEW ${src#$osrcdir/} --> ${destonv#$odestdir/}"
            fi
        elif [ -d "$src" ]; then
            ####################################################################
            # copie d'un répertoire
            destname="$(__get_local "$srcname")"
            destl="$destdir/$destname"
            if [ ! -d "$destl" ]; then
                mkdir -p "$destl"; s=$?
                edot $s "DIR ${destl#$odestdir/}"
                [ $s == 0 ] || return 1
            fi
            __unew_copy "$src" "$destl" "$newversion" "$osrcdir" "$odestdir" || return 1
        else
            edotw 1 "$src: fichier ignoré"
        fi
    done
    return 0
}

function upstream_new_help() {
    uecho "$scriptname upstream-new: intégrer une nouvelle distribution upstream

USAGE
    $scriptname upstream-new <SRCDIR|ARCHIVE> [WORKDIR]

Les nouveaux fichiers sont copiés tout de suite dans le répertoire de travail.
Par contre, les modifications ne sont intégrées qu'avec la commande local-patch.

Pour chacun des fichiers orig, si le fichier correspondant de la distribution
upstream n'a pas été modifié, alors ce fichier n'est pas intégré.

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
    -V, --version VERSION
        Spécifier la version de l'archive qui est intégrée. 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.
    -O, --origin ORIGIN
        Spécifier un répertoire origine qui contient soit la distribution non
        modifiée, soit une autre environnement udist pour la même distribution.
        Si la source est un répertoire et que l'origine n'est pas déjà spécifiée
        dans la configuration, cette option est automatiquement activée.
    --no-origin
        Ne pas mettre à jour la valeur de l'origine
    --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. 
    --force
        Forcer l'intégration de la nouvelle version, même si elle a déjà été
        intégrée.
    -n
        Ne pas appeler '$scriptname local-patch' après l'intégration de la
        nouvelle version"
}

function upstream_new_cmd() {
    eval "$(udist_local)"
    local version=
    local origin=
    local noorigin= unwrap=auto force= nopatch=
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with upstream_new_help' \
        "${UDISTOPTS[@]}" \
        -V:,--version: version= \
        -O:,--origin: origin= \
        --no-origin noorigin=1 \
        --auto-unwrap unwrap=auto \
        --no-unwrap unwrap= \
        -e,--unwrap unwrap=1 \
        -k skiploneroot= \
        --force force=1 \
        -n nopatch=1 \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -d "$2" ] && workdir="$(find_workdir "$2")"
    __after_parse
    eval "$(__load_udist)"

    local srcdir archive
    archive="$1"; shift
    if [ -f "$archive" ]; then
        is_archive "$archive" || die "$archive: doit être un fichier archive"
        [ -n "$version" ] || version="$(get_archive_version "$archive")"
    elif [ -d "$archive" ]; then
        srcdir="$(abspath "$archive")"
        archive=
        if [ -z "$noorigin" -a -z "$ORIGIN" -a -z "$origin" ]; then
            # exprimer l'origine relativement à workdir
            origin="$(relpath "$srcdir" "$workdir")"
        fi
    else
        die "$archive: fichier introuvable"
    fi
    
    read_value "Entrez un identifiant pour cette version" version "$version" || die
    if [ -z "$force" ] && array_contains VERSIONS "$version"; then
        die "Vous avez déjà intégré la version $version"
    fi

    if [ -n "$archive" ]; then
        ac_set_tmpdir tmpd
        ebegin "Extraction de $(ppath "$archive")"
        extract_archive "$archive" "$tmpd" &
        ewait $!
        eend
        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_version "$archive")}"
                    local filename="$(basename "$file")"
                    [ "$filename" == "$banv" -o "$filename" == "$ban" ] || unwrap=
                fi
                [ -n "$unwrap" -a -d "$file" ] && srcdir="$file"
            fi
        fi
    fi

    ebegin "Copie des fichiers"
    __unew_copy "$srcdir" "$workdir" "$version" "$srcdir" "$workdir"
    eend

    estep "Maj de la configuration"
    array_addu VERSIONS "$version"
    __save_udist "" VERSION:version VERSIONS ${origin:+ORIGIN:origin}
    local_list_cmd -d "$workdir" --no-list

    [ -z "$nopatch" ] && local_patch_cmd -d "$workdir"
    ac_cleanall
}

################################################################################

function upstream_clear_help() {
    uecho "$scriptname upstream-clear: 

USAGE
    $scriptname upstream-clear [options] [WORKDIR]"
}

function upstream_clear_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with upstream_clear_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -d "$1" ] && workdir="$(find_workdir "$1")"
    __after_parse

    ewarn "upstream-clear: non implémenté"
}

################################################################################

function local_create_help() {
    uecho "$scriptname local-create: créer un fichier local

USAGE
    $scriptname local-create [options] [WORKDIR] <FILEs...>

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant.
    -u, --update
        Mettre à jour la liste des fichiers locaux après l'ajout. C'est l'option
        par défaut.
    -n, --no-update
        Ne pas mettre à jour la liste des fichiers avec locaux après
        l'ajout. Cette option ne devrait pas être utilisée sauf dans des cas
        très spécifiques."
}

function local_create_cmd() {
    eval "$(udist_local)"
    local update=1
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_create_help' \
        "${UDISTOPTS[@]}" \
        -u,--update update=1 \
        -n,--no-update update= \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    if [ -z "$workdir" -a -d "$1" ]; then
        workdir="$(find_workdir "$1")" && shift
        [ -n "$workdir" ] && estepn "Sélection du répertoire de travail $(ppath "$workdir")"
    else
        [ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
        [ -z "$workdir" -a ! -e "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    fi
    __after_parse
    [ -n "$*" ] || die "Vous devez spécifier les fichiers locaux à créer"

    if [ -n "$update" ]; then
        eval "$(__load_udist)"
    else
        local -a LOCALS
    fi

    local file relfile
    for file in "$@"; do
        if withinpath "$workdir" "$file"; then
            if [ ! -f "$file" ]; then
                ask_yesno "$file: fichier introuvable. Voulez-vous le créer?" O || continue
                touch "$file" || continue
            fi
            file="$(abspath "$file")"
            relfile="$(relpath "$file" "$workdir")"
            if [ -n "$(__list_origfiles "$file")" ]; then
                estep "$relfile: nop"
            else
                array_addu LOCALS "$relfile"
                ebegin "$relfile"
                cp "$file" "$(__get_orig "$file")"
                eend $?
            fi
        else
            eerror "$file: ne se trouve pas dans $workdir"
        fi
    done

    if [ -n "$update" ]; then
        estep "Maj de la configuration"
        __save_udist "" LOCALS
    fi
}

################################################################################

function local_edit_help() {
    uecho "$scriptname local-edit: editer un fichier local

USAGE
    $scriptname local-edit [options] <FILEs...>

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_edit_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_edit_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    __after_parse

    local_create_cmd -d "$workdir" -qy "$@"
    "$EDITOR" "$@"
}

################################################################################

function local_copy_help() {
    uecho "$scriptname local-copy: copier un fichier

USAGE
    $scriptname local-copy [options] SRCFILE DESTFILE

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier SRCFILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_copy_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_copy_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    __after_parse

    ewarn "local-copy: non implémenté"
}

################################################################################

function local_move_help() {
    uecho "$scriptname local-move: déplacer un fichier

USAGE
    $scriptname local-move [options] SRCFILE DESTFILE

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier SRCFILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_move_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_move_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    __after_parse

    ewarn "local-move: non implémenté"
}

################################################################################

function local_remove_help() {
    uecho "$scriptname local-remove: supprimer un fichier

USAGE
    $scriptname local-remove [options] FILE

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_remove_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_remove_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    __after_parse

    ewarn "local-remove: non implémenté"
}

################################################################################

function local_tag_help() {
    uecho "$scriptname local-tag: gérer les fichiers de tag

USAGE
    $scriptname local-tag [options] <FILE> <TAG[%TAG...]>

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_tag_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_tag_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -f "$1" ] && workdir="$(find_workdir "$(dirname "$1")")"
    __after_parse

    ewarn "local-tag: non implémenté"
}

################################################################################

function local_switch_help() {
    uecho "$scriptname local_switch: changer le tag courant

USAGE
    $scriptname local_switch [options] TOTAG [FILE]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant.
    --from FROMTAG
        Ne faire le changement que si le tag courant est FROMTAG. Si le tag est
        composite, les tags doivent être séparés par %, e.g. tag1%tag2"
}

function local_switch_cmd() {
    eval "$(udist_local)"
    local fromctag
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_switch_help' \
        "${UDISTOPTS[@]}" \
        --from: fromctag= \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -d "$2" ] && workdir="$(find_workdir "$2")"
    [ -z "$workdir" -a -f "$2" ] && workdir="$(find_workdir "$(dirname "$2")")"
    __after_parse

    local toctag="$1"; shift
    [ -n "$toctag" ] || die "Vous devez spécifier le tag de destination"

    fromctag="$(__norm_ctag "$fromctag")"
    toctag="$(__norm_ctag "$toctag")"

    [ -n "$*" ] || set -- "$workdir"
    local -a localfiles tagfiles
    local localfile localdir doswitch tagfile ctag oldtagfile oldctag
    for localfile in "$@"; do
        etitle "$localfile"
        if ! withinpath "$workdir" "$localfile"; then
            eerror "$localfile: ne se trouve pas dans $workdir"
            continue
        fi

        if [ -d "$localfile" ]; then
            # cas particulier, c'est un répertoire, il faut lister tous les
            # fichiers locaux
            local basedir="$localfile"
            array_from_lines localfiles "$(__list_localfiles "$basedir")"
            for localfile in "${localfiles[@]}"; do
                localfile="$basedir/$localfile"
                localdir="$(dirname "$localfile")"
                doswitch=1

                array_from_lines tagfiles "$(__list_tagfiles "$localfile")"
                oldtagfile="$(__get_curtagfile "$localfile")"
                oldctag=$(__get_ctag "$oldtagfile")
                if [ -z "$oldtagfile" ]; then
                    doswitch=
                    if [ -n "${tagfiles[*]}" ]; then
                        eerror "$(ppath "$localfile"): Modifications locales sur le fichier. Veuillez utiliser local-tag pour corriger ce problème."
                    fi
                elif [ "$oldctag" == "$toctag" ]; then
                    # le fichier a déjà le bon tag
                    doswitch=
                elif [ -z "${tagfiles[*]}" ]; then
                    doswitch=
                elif [ -n "$fromctag" ]; then
                    tagfile="$(__get_tagfile "$localfile" "$fromctag" tagfiles)"
                    if [ -z "$tagfile" ]; then
                        doswitch=
                    elif ! testsame "$localfile" "$localdir/$tagfile"; then
                        ctag="$(__get_ctag "$tagfile")"
                        ewarn "$(ppath "$localfile"): Fichier ignoré (tag courant $oldctag, attendu $fromctag)"
                        doswitch=
                    fi
                fi
                
                if [ -n "$doswitch" ]; then
                    tagfile="$(__get_tagfile "$localfile" "$toctag" tagfiles)"
                    if [ -n "$tagfile" ]; then
                        copy_update "$localdir/$tagfile" "$localfile" &&
                        estepn "$(ppath "$localfile"): $oldctag --> $toctag"
                    fi
                fi
            done
        elif [ -f "$localfile" ]; then
            # c'est un fichier
            localdir="$(dirname "$localfile")"
            doswitch=1

            array_from_lines tagfiles "$(__list_tagfiles "$localfile")"
            oldtagfile="$(__get_curtagfile "$localfile")"
            oldctag=$(__get_ctag "$oldtagfile")
            if [ -z "$oldtagfile" ]; then
                doswitch=
                if [ -n "${tagfiles[*]}" ]; then
                    eerror "Modifications locales sur le fichier. Veuillez utiliser local-tag pour corriger ce problème."
                fi
            elif [ "$oldctag" == "$toctag" ]; then
                # le fichier a déjà le bon tag
                doswitch=
            elif [ -z "${tagfiles[*]}" ]; then
                ewarn "Fichier ignoré (pas de tags)"
                doswitch=
            elif [ -n "$fromctag" ]; then
                tagfile="$(__get_tagfile "$localfile" "$fromctag" tagfiles)"
                if [ -z "$tagfile" ]; then
                    ewarn "Fichier ignoré (pas de tag $fromctag)"
                    doswitch=
                elif ! testsame "$localfile" "$localdir/$tagfile"; then
                    ctag="$(__get_ctag "$tagfile")"
                    ewarn "Fichier ignoré (tag courant $oldctag, attendu $fromctag)"
                    doswitch=
                fi
            fi

            if [ -n "$doswitch" ]; then
                tagfile="$(__get_tagfile "$localfile" "$toctag" tagfiles)"
                if [ -z "$tagfile" ]; then
                    ewarn "Fichier ignoré (pas de tag $toctag)"
                else
                    copy_update "$localdir/$tagfile" "$localfile" &&
                    estepn "$oldctag --> $toctag"
                fi
            fi
            
        else
            eerror "$localfile: fichier introuvable"
        fi
        eend
    done
}

################################################################################

function __lput_copyto() {
    local destdir="$1"
    for localfile in "${localfiles[@]}"; do
        copy_update "$workdir/$localfile" "$destdir/$localfile" &&
        edot 0 "$(ppath "$destdir/$localfile")"
    done
}

function local_put_help() {
    uecho "$scriptname local-put: copier les fichiers locaux dans un répertoire distant

USAGE
    $scriptname local-put [options] [WORKDIR] <[user@host:]destdir>

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
    -u, --update
        Mettre à jour d'abord la liste des fichiers locaux avec local-list.
    -n, --no-update
        Ne pas mettre à jour la liste des fichiers avec local-list. Utiliser
        simplement le cache. C'est l'option par défaut
    --copy-origs
        Copier les fichiers orig en plus des fichiers locaux
    --copy-tags
        Copier les fichiers de tag en plus des fichiers locaux
    -a, --all
        Copier le fichier .udist et les fichiers orig et de tag en plus des
        fichiers locaux
    -S, --ssh SSH
        Spécifier la commande ssh à utiliser pour la copie avec rsync.
    -R, --rsync RSYNC
        Spécifier la commande rsync à utiliser pour la copie.
    -O, --owner OWNER
        Spécifier le propriétaire des fichiers à l'arrivée. La modification se
        fait en se connectant avec ssh."
}

function local_put_cmd() {
    eval "$(udist_local)"
    local update= copyorigs= copytags= copyall=
    local ssh= rsync= owner=
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_put_help' \
        "${UDISTOPTS[@]}" \
        -u,--update update=1 \
        -n,--no-update update= \
        --copy-origs copyorigs=1 \
        --copy-tags copytags=1 \
        -a,--all,--copy-all copyall=1 \
        -S:,--ssh: ssh= \
        -R:,--rsync: rsync= \
        -O:,--owner: owner= \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    if [ -z "$workdir" -a $# -ge 2 -a -d "$1" ]; then
        workdir="$(find_workdir "$1")" && shift
        [ -n "$workdir" ] && estepn "Sélection du répertoire de travail $(ppath "$workdir")"
    fi
    __after_parse

    local remote="$1"
    if [ -z "$remote" ]; then
        # Si la destination n'est pas spécifiée, prendre la valeur de ORIGIN
        eval "$(__load_udist)"
        remote="$ORIGIN"
        [ -n "$remote" ] && enote "Sélection automatique de la destination $remote"
    fi
    [ -n "$remote" ] || die "Vous devez spécifier la destination"

    local userhost destdir
    if [[ "$remote" == *:* ]]; then
        # remote est de la forme userhost:destdir
        splitfsep "$remote" : userhost destdir
    else
        # remote est de la forme destdir
        userhost=
        destdir="$remote"
        [ -d "$destdir" ] || die "$destdir: répertoire introuvable"
    fi

    [ -n "$update" ] && local_list_cmd -d "$workdir" --no-list
    eval "$(__load_udist)"
    local -a localfiles; array_copy localfiles LOCALS

    if [ -n "$copyorigs" -o -n "$copytags" -o -n "$copyall" ]; then
        local -a origlocalfiles
        local local localdir origfile tagfile
        array_copy origlocalfiles localfiles
        localfiles=()
        [ -n "$copyall" ] && array_add localfiles .udist
        for localfile in "${origlocalfiles[@]}"; do
            array_add localfiles "$localfile"
            localdir="$(dirname "$localfile")"
            if [ -n "$copyorigs" -o -n "$copyall" ]; then
                array_from_lines origfiles "$(__list_origfiles "$workdir/$localfile")"
                for origfile in "${origfiles[@]}"; do
                    array_add localfiles "$localdir/$origfile"
                done
            fi
            if [ -n "$copytags" -o -n "$copyall" ]; then
                array_from_lines tagfiles "$(__list_tagfiles "$workdir/$localfile")"
                for tagfile in "${tagfiles[@]}"; do
                    array_add localfiles "$localdir/$tagfile"
                done
            fi
        done
    fi

    if [ -n "$userhost" ]; then
        # copie distante
        ac_set_tmpdir tmpdir
        ebegin "Préparation de la copie"
        __lput_copyto "$tmpdir"
        eend

        estep "Copie distante vers $userhost:$destdir"
        local -a cmd
        cmd=("${rsync:-rsync}")
        [ -n "$ssh" ] && cmd=("${cmd[@]}" -e "$ssh")
        "${cmd[@]}" -rv "$tmpdir/" "$userhost:$destdir"

        if [ -n "$owner" ]; then
            estep "Modification du propriétaire à $owner"
            local script=
            for localfile in "${localfiles[@]}"; do
                [ -n "$script" ] && script="$script; "
                script="$script$(quoted_args chown "$owner" "$destdir/$localfile")"
            done
            "${ssh:-ssh}" -qt "$userhost:$destdir" "$script"
        fi
    else
        # copie locale
        withinpath "$workdir" "$destdir" && die "Impossible de copier dans $(ppath "$destdir")"
        ebegin "Copie des fichiers"
        __lput_copyto "$destdir"
        eend

        if [ -n "$owner" ]; then
            ebegin "Modification du propriétaire à $owner"
            for localfile in "${localfiles[@]}"; do
                chown "$owner" "$destdir/$localfile"; edot $? "$(ppath "$destdir/$localfile")"
            done
            eend
        fi
    fi
}

################################################################################

function local_get_help() {
    uecho "$scriptname local-get: intégrer des fichiers d'un répertoire distant et en faire des fichiers locaux

USAGE
    $scriptname local-get [options] <[user@host:]destdir> [WORKDIR]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
    -l
        Faire des liens physiques au lieu de simplement copier les fichiers
    -s
        Faire des liens symboliques au lieu de liens physiques
    -S, --ssh SSH
        Spécifier la commande ssh à utiliser pour la copie avec rsync.
    -R, --rsync RSYNC
        Spécifier la commande rsync à utiliser pour la copie."
}

function local_get_cmd() {
    eval "$(udist_local)"
    local link= symbolic=
    local ssh= rsync=
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_get_help' \
        "${UDISTOPTS[@]}" \
        -l,--link link=1 \
        -s,--symbolic '$link=1; symbolic=1' \
        -S:,--ssh: ssh= \
        -R:,--rsync: rsync= \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -d "$2" ] && workdir="$(find_workdir "$2")"
    __after_parse

    local remote="$1"
    if [ -z "$remote" ]; then
        # Si la destination n'est pas spécifiée, prendre la valeur de ORIGIN
        eval "$(__load_udist)"
        remote="$ORIGIN"
        [ -n "$remote" ] && enote "Sélection automatique de la source $remote"
    fi
    [ -n "$remote" ] || die "Vous devez spécifier la source"

    local userhost srcdir
    if [[ "$remote" == *:* ]]; then
        # remote est de la forme userhost:srcdir
        splitfsep "$remote" : userhost srcdir
    else
        # remote est de la forme srcdir
        userhost=
        srcdir="$remote"
        [ -d "$srcdir" ] || die "$srcdir: répertoire introuvable"
        withinpath "$workdir" "$srcdir" && die "Impossible de copier depuis $(ppath "$srcdir")"
    fi

    if [ -n "$userhost" ]; then
        if [ -n "$link" ]; then
            ewarn "Impossible de faire des liens pour des fichiers distants"
            link=
        fi

        # copie distante
        etitle "Copie distante depuis $userhost:$srcdir"
        ac_set_tmpdir tmpdir

        local -a cmd
        cmd=("${rsync:-rsync}")
        [ -n "$ssh" ] && cmd=("${cmd[@]}" -e "$ssh")
        "${cmd[@]}" -rv "$userhost:$srcdir/" "$tmpdir"
        eend

        srcdir="$tmpdir"
    fi

    etitle "Intégration des fichiers"
    array_from_lines srcs "$(cd "$srcdir"; find -type f | filter_vcspath | sed 's/^.\///')"
    for src in "${srcs[@]}"; do
        mkdirof "$workdir/$src" || die
        local_create_cmd -d "$workdir" -qy "$src"
        if [ -n "$link" ]; then
            [ -e "$workdir/$src" ] && rm "$workdir/$src"
            ln ${symbolic:+-s} "$srcdir/$src" "$workdir/$src"
        else
            [ -L "$workdir/$src" ] && rm "$workdir/$src"
            copy_update "$srcdir/$src" "$workdir/$src" &&
            estep "$(ppath "$workdir/$src")"
        fi
    done
    eend
}

################################################################################

function local_list_help() {
    uecho "$scriptname local-list: lister les fichiers locaux

USAGE
    $scriptname local-list [options] [WORKDIR]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
    -n, --no-update
        Ne pas mettre à jour la liste des fichiers. Utiliser simplement le
        cache. Implique l'option --no-cache
    --no-list
        Ne pas lister les fichiers. Faire simplement la mise à jour du cache.
    --no-cache
        Ne pas mettre à jour le cache des fichiers locaux. Normalement, toutes
        les autres commandes utilisent les informations du cache."
}

function local_list_cmd() {
    eval "$(udist_local)"
    local update=1
    local list=1
    local cache=1
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_list_help' \
        "${UDISTOPTS[@]}" \
        -u,--update update=1 \
        -n,--no-update '$update=; cache=' \
        -l,--list list=1 \
        --no-list list= \
        -c,--cache cache=1 \
        --no-cache cache= \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    [ -z "$workdir" -a -d "$1" ] && workdir="$(find_workdir "$1")"
    __after_parse
    eval "$(__load_udist)"

    local -a localfiles tags tagfiles localtags
    local tagfile localtag
    if [ -n "$update" ]; then
        array_from_lines localfiles "$(__list_localfiles "$workdir")"
        for localfile in "${localfiles[@]}"; do
            array_from_lines tagfiles "$(__list_tagfiles "$workdir/$localfile")"
            for tagfile in "${tagfiles[@]}"; do
                __init_tags localtags "$tagfile"
                array_extendu tags localtags
            done
        done
        array_from_lines tags "$(array_to_lines tags | csort -u)"
    else
        array_copy localfiles LOCALS
        array_copy tags TAGS
    fi

    if [ -n "$cache" ]; then
        __save_udist "" LOCALS:localfiles TAGS:tags
    fi
    if [ -n "$list" ]; then
        [ -n "$ORIGIN" ] && echo "# origin: $(relpath "$ORIGIN" "" "$workdir")"
        echo "# version: $VERSION"

        local -a versions tagfiles; local localdir
        for localfile in "${localfiles[@]}"; do
            localfile="$workdir/$localfile"
            localdir="$(dirname "$localfile")"

            line="$(relpath "$localfile")"
            if [ ! -f "$localfile" ]; then
                line="$line ${COULEUR_ROUGE}!${COULEUR_NORMALE}"
            fi

            array_from_lines versions "$(__list_origfiles "$localfile")"
            array_map versions __get_version
            array_del versions "$version"
            if [ -n "${versions[*]}" ]; then
                line="$line ${COULEUR_JAUNE}pending${COULEUR_NORMALE}($(array_join versions ", "))"
            fi

            array_from_lines tagfiles "$(__list_tagfiles "$localfile")"
            if [ -n "${tagfiles[*]}" ]; then
                local tmpline= first=1 foundcur=
                for tagfile in "${tagfiles[@]}"; do
                    if [ -n "$first" ]; then first=; else tmpline="$tmpline, "; fi
                    if testsame "$localfile" "$localdir/$tagfile"; then
                        tmpline="$tmpline${COULEUR_BLEUE}$(__get_ctag "$tagfile")${COULEUR_NORMALE}"
                        foundcur=1
                    else
                        tmpline="$tmpline$(__get_ctag "$tagfile")"
                    fi
                done
                if [ -n "$foundcur" ]; then
                    line="$line tags($tmpline)"
                else
                    line="$line ${COULEUR_ROUGE}tags${COULEUR_NORMALE}($tmpline)"
                fi
            fi
            
            echo "$line"
        done
    fi
}

################################################################################

function upstream_diff_help() {
    uecho "$scriptname upstream-diff: 

USAGE
    $scriptname upstream-diff [options] WORKDIR
    $scriptname upstream-diff [options] [FILEs...]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function upstream_diff_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with upstream_diff_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    if [ -z "$workdir" -a -d "$1" ]; then
        workdir="$(find_workdir "$1")" && shift
    elif [ -z "$workdir" -a -f "$1" ]; then
        workdir="$(find_workdir "$(dirname "$1")")"
    fi
    __after_parse

    ewarn "upstream-diff: non implémenté"
}

################################################################################

function local_diff_help() {
    uecho "$scriptname local-diff: 

USAGE
    $scriptname local-diff [options] WORKDIR
    $scriptname local-diff [options] [FILEs...]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_diff_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_diff_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    if [ -z "$workdir" -a -d "$1" ]; then
        workdir="$(find_workdir "$1")" && shift
    elif [ -z "$workdir" -a -f "$1" ]; then
        workdir="$(find_workdir "$(dirname "$1")")"
    fi
    __after_parse

    ewarn "local-diff: non implémenté"
}

################################################################################

function local_patch_help() {
    uecho "$scriptname local-patch: 

USAGE
    $scriptname local-patch [options] WORKDIR
    $scriptname local-patch [options] [FILEs...]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_patch_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_patch_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    if [ -z "$workdir" -a -d "$1" ]; then
        workdir="$(find_workdir "$1")" && shift
    elif [ -z "$workdir" -a -f "$1" ]; then
        workdir="$(find_workdir "$(dirname "$1")")"
    fi
    __after_parse

    ewarn "local-patch: non implémenté"
}

################################################################################

function local_forget_help() {
    uecho "$scriptname local-forget: 

USAGE
    $scriptname local-forget [options] WORKDIR
    $scriptname local-forget [options] [FILEs...]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
        Si cette option n'est pas spécifiée et que le fichier FILE existe, le
        calcul est effectué à partir de son répertoire au lieu du répertoire
        courant."
}

function local_forget_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with local_forget_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    if [ -z "$workdir" -a -d "$1" ]; then
        workdir="$(find_workdir "$1")" && shift
    elif [ -z "$workdir" -a -f "$1" ]; then
        workdir="$(find_workdir "$(dirname "$1")")"
    fi
    __after_parse

    ewarn "local-forget: non implémenté"
}

################################################################################
# XXX à migrer puis supprimer

function diff_help() {
    uecho "$scriptname diff: afficher les différences entre les versions

USAGE
    $scriptname diff [options]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant.
    -o
        Afficher les différences entre la version originale actuelle et la
        version précédente (par défaut). Ces différences peuvent être intégrées
        dans le répertoire de travail avec la commande '$scriptname patch'
    -c
        Afficher les différences entre la copie de travail et la version
        originale."
}

function diff_cmd() {
    eval "$(udist_local)"
    local mode=orig
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with diff_help' \
        "${UDISTOPTS[@]}" \
        -o,--orig mode=orig \
        -c,--work mode=work \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"
    __after_parse

    local cwd="$(pwd)"
    cd "$workdir"
    if [ "$mode" == "orig" ]; then
        if [ -d ".__prev" -a -d ".__orig" ]; then
            cdiff -urN .__prev .__orig | page_maybe
        else
            einfo "Pas de version précédente à comparer"
        fi
    elif [ "$mode" == "work" ]; then
        if [ -d ".__orig" ]; then
            cdiff -urN -x .svn -x .git -x .__prev -x .__orig -x .udist .__orig . | page_maybe
        else
            einfo "Pas de version originale à comparer"
        fi
    fi
    cd "$cwd"
}

################################################################################
# XXX à migrer puis supprimer

function patch_help() {
    uecho "$scriptname patch: intégrer les modifications dans le répertoire de travail

USAGE
    $scriptname patch [options]

OPTIONS
    -d WORKDIR
        Spécifier le répertoire de travail au lieu du répertoire courant."
}

function patch_cmd() {
    eval "$(udist_local)"
    parse_opts "${PRETTYOPTS[@]}" \
        --help '$exit_with patch_help' \
        "${UDISTOPTS[@]}" \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"
    __after_parse

    local patch
    ac_set_tmpfile patch
    diff_cmd -qo >"$patch"

    if [ -s "$patch" ]; then
        page_maybe <"$patch"
        ask_yesno "Voulez-vous appliquer ces modifications à la copie de travail?" X || die

        local cwd="$(pwd)"
        cd "$workdir"
        patch -p1 <"$patch" || die
        rm -rf ".__prev" || die
        cd "$cwd"
    else
        einfo "Pas de modifications à intégrer"
    fi
}

################################################################################

workdir=
parse_opts + "${PRETTYOPTS[@]}" \
    --help '$exit_with display_help' \
    "${UDISTOPTS[@]}" \
    @ args -- "$@" && set -- "${args[@]}" || die "$args"
__after_parse initial

# Traduire la commande
[ -n "$*" ] || set -- "$DEFAULT_CMD"
cmd=
found_cmd=
while [ -z "$found_cmd" ]; do
    cmd="$1"; shift; found_cmd=1
    [ -n "$cmd" ] || break

    for cmd_alias in "${CMD_ALIASES[@]}"; do
        splitpair "$cmd_alias" src dest
        if [ "$cmd" == "$src" ]; then
            eval "set -- $dest \"\$@\""
            found_cmd=
            break
        fi
    done
done

case "$cmd" in
"") exit_with display_help;;
init) init_cmd "$@";;
upstream-new) upstream_new_cmd "$@";;
upstream-clear) upstream_clear_cmd "$@";;
local-create) local_create_cmd "$@";;
local-edit) local_edit_cmd "$@";;
local-copy) local_copy_cmd "$@";;
local-move) local_move_cmd "$@";;
local-remove) local_remove_cmd "$@";;
local-tag) local_tag_cmd "$@";;
local-switch) local_switch_cmd "$@";;
local-put) local_put_cmd "$@";;
local-get) local_get_cmd "$@";;
local-list) local_list_cmd "$@";;
upstream-diff) upstream_diff_cmd "$@";;
local-diff) local_diff_cmd "$@";;
local-patch) local_patch_cmd "$@";;
local-forget) local_forget_cmd "$@";;
*) die "$cmd: commande incorrecte";;
esac