Compare commits

..

1 Commits

Author SHA1 Message Date
c457152c7f réorganiser capacitors 2025-10-16 17:16:16 +04:00
22 changed files with 1402 additions and 1925 deletions

View File

@ -28,152 +28,6 @@ CONFIG_VARS=(
UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO
) )
################################################################################
PMAN_TOOL_PUPS=UPSTREAM
PMAN_TOOL_PDEV=DEVELOP
PMAN_TOOL_PWIP=FEATURE
PMAN_TOOL_PMAIN=MAIN
PMAN_TOOL_PDIST=DIST
UPSTREAM_BASE= ; UPSTREAM_MERGE_FROM= ; UPSTREAM_MERGE_TO=DEVELOP ; UPSTREAM_PREL= ; UPSTREAM_DELETE=
DEVELOP_BASE=MAIN ; DEVELOP_MERGE_FROM=FEATURE ; DEVELOP_MERGE_TO=MAIN ; DEVELOP_PREL=to ; DEVELOP_DELETE=from
MAIN_BASE= ; MAIN_MERGE_FROM=DEVELOP ; MAIN_MERGE_TO=DIST ; MAIN_PREL=from ; MAIN_DELETE=
DIST_BASE=MAIN ; DIST_MERGE_FROM=MAIN ; DIST_MERGE_TO= ; DIST_PREL= ; DIST_DELETE=
FEATURE_BASE=DEVELOP ; FEATURE_MERGE_FROM= ; FEATURE_MERGE_TO=DEVELOP ; FEATURE_PREL= ; FEATURE_DELETE=to
function get_base_branch() {
# afficher la branche depuis laquelle créer la branche $1
# retourner 1 en cas d'erreur (pas de branche source)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_BASE"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function get_merge_from_branch() {
# afficher la branche depuis laquelle la branche $1 doit merger
# retourner 1 en cas d'erreur (pas de branche source)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_MERGE_FROM"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function get_merge_to_branch() {
# afficher la branche dans laquelle la branche $1 doit merger
# retourner 1 en cas d'erreur (pas de branche destination)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_MERGE_TO"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function should_prel_merge() {
# tester si la branche $1 doit être mergée avec prel dans la direction
# $2(=to)
local branch="$1" dir="${2:-to}" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_PREL"
[ "${!infos}" == "$dir" ]
}
function should_delete_merged() {
# tester si la branche $1 doit être supprimée après avoir été mergée dans la
# direction $2(=to)
local branch="$1" dir="${2:-to}" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_DELETE"
[ "${!infos}" == "$dir" ]
}
: "
# description des variables #
* PMAN_TOOL -- nom de l'outil, e.g pdev, pmain, pdist
* PMAN_REF_BRANCH -- code de la branche de référence basé sur le nom de l'outil
* PmanRefBranch -- nom effectif de la branche si elle est définie dans
.pman.conf, vide sinon
* IfRefBranch -- nom effectif de la branche *si elle existe*, vide sinon
* PMAN_UNIQUE -- si la branche de référence est unique. est vide pour les
codes de branches multiples, telle que FEATURE
* PMAN_BASE_BRANCH -- branche de base à partir de laquelle créer la branche
de référence
* PmanBaseBranch -- nom effectif de la branche de base si elle est définie
dans .pman.conf, vide sinon
* IfBaseBranch -- nom effectif de la branche de base *si elle existe*, vide
sinon
* PMAN_MERGE_FROM -- code de la branche source à partir de laquelle la fusion
est faite dans PMAN_REF_BRANCH. vide si la branche n'a pas de source
* PMAN_MERGE_TO -- code de la branche destination dans laquelle la fusion est
faite depuis PMAN_REF_BRANCH. vide si la branche n'a pas de destination
* PMAN_DIR -- direction de la fusion:
'from' si on fait PMAN_MERGE_FROM --> PMAN_REF_BRANCH
'to' si on fait PMAN_REF_BRANCH --> PMAN_MERGE_TO
* PMAN_PREL_MERGE -- si la fusion devrait se faire avec prel
* PMAN_DELETE_MERGED -- s'il faut supprimer la branche source après la fusion
* PMAN_MERGE_SRC -- code de la branche source pour la fusion, ou vide si la
fusion n'est pas possible
* PmanMergeSrc -- nom effectif de la branche source si elle est définie
dans .pman.conf
* IfMergeSrc -- nom effectif de la branche source *si elle existe*, vide
sinon
* PMAN_MERGE_DEST -- code de la branche destination pour la fusion, ou vide si
la fusion n'est pas possible
* PmanMergeDest -- nom effectif de la branche destination si elle est
définie dans .pman.conf
* IfMergeDest -- nom effectif de la branche source *si elle existe*, vide
sinon
* PMAN_CAN_MERGE -- indique si la fusion est théoriquement possible, c'est à
dire que $PMAN_MERGE_SRC et $PMAN_MERGE_DEST sont tous les deux non vides
* IfCanMerge -- indique si la fusion est effectivement possible, c'est à dire
que $IfMergeSrc et $IfMergeDest sont tous les deux non vides"
[ -n "$PMAN_TOOL" ] || PMAN_TOOL="$MYNAME"
PMAN_REF_BRANCH="PMAN_TOOL_${PMAN_TOOL^^}"; PMAN_REF_BRANCH="${!PMAN_REF_BRANCH}"
function set_pman_vars() {
PmanRefBranch="${!PMAN_REF_BRANCH}"
case "$PMAN_REF_BRANCH" in
FEATURE|RELEASE|HOTFIX) PMAN_UNIQUE=;;
*) PMAN_UNIQUE=1;;
esac
PMAN_BASE_BRANCH=$(get_base_branch "$PMAN_REF_BRANCH")
[ -n "$PMAN_BASE_BRANCH" ] && PmanBaseBranch="${!PMAN_BASE_BRANCH}" || PmanBaseBranch=
PMAN_MERGE_FROM=$(get_merge_from_branch "$PMAN_REF_BRANCH")
PMAN_MERGE_TO=$(get_merge_to_branch "$PMAN_REF_BRANCH")
if [ -n "$1" ]; then PMAN_DIR="$1"
else PMAN_DIR=to
#elif [ -n "$PMAN_MERGE_TO" ]; then PMAN_DIR=to
#else PMAN_DIR=from
fi
PMAN_PREL_MERGE=$(should_prel_merge "$PMAN_REF_BRANCH" "$PMAN_DIR" && echo 1)
PMAN_DELETE_MERGED=$(should_delete_merged "$PMAN_REF_BRANCH" "$PMAN_DIR" && echo 1)
case "$PMAN_DIR" in
to)
PMAN_MERGE_SRC="$PMAN_REF_BRANCH"
PMAN_MERGE_DEST="$PMAN_MERGE_TO"
;;
from)
PMAN_MERGE_SRC="$PMAN_MERGE_FROM"
PMAN_MERGE_DEST="$PMAN_REF_BRANCH"
;;
esac
[ -n "$PMAN_MERGE_SRC" -a -n "$PMAN_MERGE_DEST" ] && PMAN_CAN_MERGE=1 || PMAN_CAN_MERGE=
[ -n "$PMAN_MERGE_SRC" ] && PmanMergeSrc="${!PMAN_MERGE_SRC}" || PmanMergeSrc=
[ -n "$PMAN_MERGE_DEST" ] && PmanMergeDest="${!PMAN_MERGE_DEST}" || PmanMergeDest=
}
################################################################################
function _init_changelog() { function _init_changelog() {
setx date=date +%d/%m/%Y-%H:%M setx date=date +%d/%m/%Y-%H:%M
ac_set_tmpfile changelog ac_set_tmpfile changelog
@ -302,14 +156,14 @@ function check_gitdir() {
# se mettre à la racine du dépôt git # se mettre à la racine du dépôt git
local gitdir local gitdir
git_check_gitvcs || return 1 git_ensure_gitvcs
setx gitdir=git_get_toplevel setx gitdir=git_get_toplevel
cd "$gitdir" || return 1 cd "$gitdir" || return 1
} }
function ensure_gitdir() { function ensure_gitdir() {
# commencer dans le répertoire indiqué # commencer dans le répertoire indiqué
check_gitdir "$@" || die || return 1 check_gitdir "$@" || die || return
} }
function load_branches() { function load_branches() {
@ -354,32 +208,23 @@ function load_branches() {
HotfixBranch= HotfixBranch=
MainBranch= MainBranch=
DistBranch= DistBranch=
IfRefBranch=
IfBaseBranch=
IfMergeSrc=
IfMergeDest=
for branch in "${LocalBranches[@]}"; do for branch in "${LocalBranches[@]}"; do
if [ "$branch" == "$UPSTREAM" ]; then if [ "$branch" == "$UPSTREAM" ]; then
UpstreamBranch="$branch" UpstreamBranch="$branch"
elif [ -n "$FEATURE" ] && [[ "$branch" == "$FEATURE"* ]]; then elif [[ "$branch" == "$FEATURE"* ]]; then
FeatureBranches+=("$branch") FeatureBranches+=("$branch")
elif [ -n "$DEVELOP" -a "$branch" == "$DEVELOP" ]; then elif [ "$branch" == "$DEVELOP" ]; then
DevelopBranch="$branch" DevelopBranch="$branch"
elif [ -n "$RELEASE" ] && [[ "$branch" == "$RELEASE"* ]]; then elif [[ "$branch" == "$RELEASE"* ]]; then
ReleaseBranch="$branch" ReleaseBranch="$branch"
elif [ -n "$HOTFIX" ] && [[ "$branch" == "$HOTFIX"* ]]; then elif [[ "$branch" == "$HOTFIX"* ]]; then
HotfixBranch="$branch" HotfixBranch="$branch"
elif [ -n "$MAIN" -a "$branch" == "$MAIN" ]; then elif [ "$branch" == "$MAIN" ]; then
MainBranch="$branch" MainBranch="$branch"
elif [ -n "$DIST" -a "$branch" == "$DIST" ]; then elif [ "$branch" == "$DIST" ]; then
DistBranch="$branch" DistBranch="$branch"
fi fi
[ -n "$PmanRefBranch" -a "$branch" == "$PmanRefBranch" ] && IfRefBranch="$branch"
[ -n "$PmanBaseBranch" -a "$branch" == "$PmanBaseBranch" ] && IfBaseBranch="$branch"
[ -n "$PmanMergeSrc" -a "$branch" == "$PmanMergeSrc" ] && IfMergeSrc="$branch"
[ -n "$PmanMergeDest" -a "$branch" == "$PmanMergeDest" ] && IfMergeDest="$branch"
done done
[ -n "$IfMergeSrc" -a "$IfMergeDest" ] && IfCanMerge=1 || IfCanMerge=
;; ;;
esac esac
} }
@ -404,6 +249,12 @@ function load_config() {
elif [ -f .pman.conf ]; then elif [ -f .pman.conf ]; then
ConfigFile="$(pwd)/.pman.conf" ConfigFile="$(pwd)/.pman.conf"
source "$ConfigFile" source "$ConfigFile"
elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then
# $1 est le nom de base de l'outil e.g "pdev", et le suffixe est la
# configuration à charger par défaut. i.e pdev74 chargera par défaut la
# configuration pman74.conf
ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh"
source "$ConfigFile"
else else
ConfigFile="$NULIBDIR/bash/src/pman.conf.sh" ConfigFile="$NULIBDIR/bash/src/pman.conf.sh"
fi fi

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
PMAN_TOOLS=pdev
SRC_TYPE=DEVELOP
SRC_BRANCH="${SRC_TYPE,,}"; SRC_BRANCH="${SRC_BRANCH^}Branch"
DEST_TYPE=MAIN
DEST_BRANCH="${DEST_TYPE,,}"; DEST_BRANCH="${DEST_BRANCH^}Branch"
ALLOW_MERGE=1
MERGE_PREL=1
ALLOW_DELETE=

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
PMAN_TOOL=pdist
SRC_TYPE=DIST
SRC_BRANCH="${SRC_TYPE,,}"; SRC_BRANCH="${SRC_BRANCH^}Branch"
DEST_TYPE=
DEST_BRANCH=
ALLOW_MERGE=
MERGE_PREL=
ALLOW_DELETE=

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
PMAN_TOOL=pmain
SRC_TYPE=MAIN
SRC_BRANCH="${SRC_TYPE,,}"; SRC_BRANCH="${SRC_BRANCH^}Branch"
DEST_TYPE=DIST
DEST_BRANCH="${DEST_TYPE,,}"; DEST_BRANCH="${DEST_BRANCH^}Branch"
ALLOW_MERGE=1
MERGE_PREL=
ALLOW_DELETE=

View File

@ -1,299 +0,0 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de fusionner la branche"
function resolve_unique_branch() {
if [ "$PMAN_REF_BRANCH" == FEATURE ]; then
if [ $# -gt 0 ]; then
UniqueBranch="$FEATURE${1#$FEATURE}"
elif [[ "$CurrentBranch" == "$FEATURE"* ]]; then
UniqueBranch="$CurrentBranch"
elif [ ${#FeatureBranches[*]} -eq 0 ]; then
die "Vous devez spécifier la branche de feature"
elif [ ${#FeatureBranches[*]} -eq 1 ]; then
UniqueBranch="${FeatureBranches[0]}"
else
simple_menu \
UniqueBranch FeatureBranches \
-t "Branches de feature" \
-m "Veuillez choisir la branche de feature" \
-d "${FeatureBranches[0]}"
fi
PMAN_REF_BRANCH=UniqueBranch
PMAN_MERGE_SRC=UniqueBranch
else
die "resolve_unique_branch: $PMAN_REF_BRANCH: non implémenté"
fi
}
function dump_action() {
enote "Valeurs des variables:
PMAN_TOOL=$PMAN_TOOL
PMAN_REF_BRANCH=$PMAN_REF_BRANCH${PmanRefBranch:+ PmanRefBranch=$PmanRefBranch IfRefBranch=$IfRefBranch}
PMAN_BASE_BRANCH=$PMAN_BASE_BRANCH${PmanBaseBranch:+ PmanBaseBranch=$PmanBaseBranch IfCreateBase=$IfCreateBase}
PMAN_MERGE_FROM=$PMAN_MERGE_FROM
PMAN_MERGE_TO=$PMAN_MERGE_TO
PMAN_DIR=$PMAN_DIR
PMAN_PREL_MERGE=$PMAN_PREL_MERGE
PMAN_DELETE_MERGED=$PMAN_DELETE_MERGED
PMAN_MERGE_SRC=$PMAN_MERGE_SRC${PmanMergeSrc:+ PmanMergeSrc=$PmanMergeSrc IfMergeSrc=$IfMergeSrc}
PMAN_MERGE_DEST=$PMAN_MERGE_DEST${PmanMergeDest:+ PmanMergeDest=$PmanMergeDest IfMergeDest=$IfMergeDest}
PMAN_CAN_MERGE=$PMAN_CAN_MERGE IfCanMerge=$IfCanMerge
CurrentBranch=$CurrentBranch
LocalBranches=${LocalBranches[*]}
RemoteBranches=${RemoteBranches[*]}
AllBranches=${AllBranches[*]}
UpstreamBranch=$UpstreamBranch
FeatureBranches=${FeatureBranches[*]}
DevelopBranch=$DevelopBranch
ReleaseBranch=$ReleaseBranch
HotfixBranch=$HotfixBranch
MainBranch=$MainBranch
DistBranch=$DistBranch
"
}
function _ensure_branch() {
[ -n "$PmanRefBranch" ] || die "\
La branche $PMAN_REF_BRANCH n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfRefBranch" ] || die "$PmanRefBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _ensure_base_branch() {
[ -n "${!PMAN_BASE_BRANCH}" ] || die "\
La branche $PMAN_BASE_BRANCH n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfCreateBase" ] || die "${!PMAN_BASE_BRANCH}: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _ensure_merge_src() {
[ -n "$PmanMergeSrc" ] || die "\
La branche $PMAN_MERGE_SRC n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfMergeSrc" ] || die "$PmanMergeSrc: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _ensure_merge_dest() {
[ -n "$PmanMergeDest" ] || die "\
La branche $PMAN_MERGE_DEST n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfMergeDest" ] || die "$PmanMergeDest: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function checkout_action() {
local -a push_branches
[ -n "$PMAN_UNIQUE" ] || resolve_unique_branch "$@" || die
_ensure_branch init
if [ -n "$IfRefBranch" ]; then
git checkout "$IfRefBranch"
elif array_contains LocalBranches "$PmanRefBranch"; then
git checkout "$PmanRefBranch"
elif array_contains AllBranches "$PmanRefBranch"; then
enote "$PmanRefBranch: une branche du même nom existe dans l'origine"
ask_yesno "Voulez-vous basculer sur cette branche?" O || die
git checkout "$PmanRefBranch"
elif [ -n "$PMAN_BASE_BRANCH" ]; then
_ensure_base_branch
resolve_should_push
local SrcBranch="${!PMAN_BASE_BRANCH}" DestBranch="$PmanRefBranch"
enote "Vous allez créer la branche ${COULEUR_BLEUE}$DestBranch${COULEUR_NORMALE} <-- ${COULEUR_ROUGE}$SrcBranch${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
einfo "Création de la branche $DestBranch"
git checkout -b "$DestBranch" "$SrcBranch" || die
push_branches+=("$DestBranch")
_push_branches
fi
}
#XXXX
#[ -n "$PmanMergeSrc" ] ||
# die "Aucune branche définie pour $PMAN_MERGE_SRC. Veuillez éditer le fichier .pman.conf"
function ensure_merge_branches() {
[ -n "$PMAN_CAN_MERGE" ] ||
die "$PmanRefBranch: Aucune configuration de fusion trouvée pour cette branche"
local branches
[ "$1" == -a ] && branches=AllBranches || branches=LocalBranches
SrcBranch="$PmanMergeSrc"
array_contains "$branches" "$SrcBranch" || die "$SrcBranch: branche source introuvable"
DestBranch="$PmanMergeDest"
array_contains "$branches" "$DestBranch" || die "$DestBranch: branche destination introuvable"
}
function _show_action() {
local commits
setx commits=_list_commits
if [ -n "$commits" ]; then
if [ $ShowLevel -ge 2 ]; then
{
echo "\
# Commits à fusionner $SrcBranch --> $DestBranch
$commits
"
_sd_COLOR=always _show_diff
} | less -eRF
else
einfo "Commits à fusionner $SrcBranch --> $DestBranch"
eecho "$commits"
fi
fi
}
function show_action() {
git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY"
ensure_merge_branches
_show_action "$@"
}
function _merge_action() {
enote "\
Ce script va
- fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}"
ask_yesno "Voulez-vous continuer?" O || die
local script=".git/pman-merge.sh"
local -a push_branches delete_branches
local hook
local comment=
local or_die=" || exit 1"
_mscript_start
_scripta <<EOF
################################################################################
# merge
if [ -n "\$merge" ]; then
esection "Fusionner la branche"
EOF
hook="BEFORE_MERGE_$PMAN_MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_mscript_merge_branch
hook="AFTER_MERGE_$PMAN_MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
if [ -n "$ShouldDelete" ]; then
_scripta <<EOF
################################################################################
# delete
if [ -n "\$delete" ]; then
esection "Supprimer la branche"
EOF
_mscript_delete_branch
hook="AFTER_DELETE_$PMAN_MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
fi
_scripta <<EOF
################################################################################
# push
if [ -n "\$push" ]; then
esection "Pousser les branches"
EOF
hook="BEFORE_PUSH_$PMAN_MERGE_DEST"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_script_push_branches
if [ ${#delete_branches[*]} -gt 0 ]; then
_scripta <<<"if [ -n \"\$delete\" ]; then"
push_branches=("${delete_branches[@]}")
_script_push_branches
_scripta <<<fi
fi
hook="AFTER_PUSH_$PMAN_MERGE_DEST"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
[ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
[ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
[ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then
eimportant "\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
git merge --continue
Sinon, veuillez consulter le script et/ou le relancer
./$script${Delete:+ delete}${Push:+ push}"
die
elif [ -n "$Deleted" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script"
[ -n "$AfterMerge" ] && eval "$AfterMerge"
else
local msg="\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
Vous pouvez consulter le script et/ou le relancer
./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
[ -n "$AfterMerge" ] && msg="$msg
Il y a aussi les commandes supplémentaires suivantes:
${AfterMerge//
/
}"
einfo "$msg"
fi
}
function merge_action() {
[ -n "$PMAN_UNIQUE" ] || resolve_unique_branch "$@" || die
ensure_merge_branches -a
if [ -n "$PMAN_PREL_MERGE" ]; then
[ -n "$ForceMerge" ] || die "$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
fi
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$SrcBranch"
[ -n "$PMAN_DELETE_MERGED" ] || Delete=
[ -z "$_Fake" ] && git_ensure_cleancheckout
if ! array_contains LocalBranches "$SrcBranch" && array_contains AllBranches "$SrcBranch"; then
enote "$SrcBranch: une branche du même nom existe dans l'origine"
fi
if ! array_contains LocalBranches "$DestBranch" && array_contains AllBranches "$DestBranch"; then
enote "$DestBranch: une branche du même nom existe dans l'origine"
fi
array_contains LocalBranches "$SrcBranch" || die "$SrcBranch: branche locale introuvable"
array_contains LocalBranches "$DestBranch" || die "$DestBranch: branche locale introuvable"
resolve_should_push
set -x #XXX
_merge_action "$@"
}
function rebase_action() {
die "non implémenté"
}

View File

@ -2,215 +2,727 @@
namespace nulib\db; namespace nulib\db;
use nulib\cl; use nulib\cl;
use nulib\exceptions; use nulib\cv;
use nulib\db\_private\_migration;
use nulib\php\func; use nulib\php\func;
use nulib\ValueException;
use Traversable; use Traversable;
/** /**
* Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une * Class Capacitor: objet permettant d'accumuler des données pour les
* instance de {@link CapacitorStorage} * réutiliser plus tard
*/ */
class Capacitor implements ITransactor { abstract class Capacitor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) { abstract function db(): IDatabase;
$this->storage = $storage;
$this->channel = $channel;
$this->channel->setCapacitor($this);
if ($ensureExists) $this->ensureExists();
}
/** @var CapacitorStorage */
protected $storage;
function getStorage(): CapacitorStorage {
return $this->storage;
}
function db(): IDatabase {
return $this->getStorage()->db();
}
function ensureLive(): self { function ensureLive(): self {
$this->getStorage()->ensureLive(); $this->db()->ensureLive();
return $this; return $this;
} }
/** @var CapacitorChannel */ function newChannel($channel): CapacitorChannel {
protected $channel; if (!($channel instanceof CapacitorChannel)) {
if (!is_array($channel)) $channel = ["name" => $channel];
function getChannel(): CapacitorChannel { $channel = new CapacitorChannel($channel);
return $this->channel; }
return $channel->initCapacitor($this);
} }
function getTableName(): string { const CDATA_DEFINITION = null;
return $this->getChannel()->getTableName(); const CSUM_DEFINITION = null;
} const CTIMESTAMP_DEFINITION = null;
const GSERIAL_DEFINITION = null;
const GLIC_DEFINITION = null;
const GLIB_DEFINITION = null;
const GTEXT_DEFINITION = null;
const GBOOL_DEFINITION = null;
const GUUID_DEFINITION = null;
function getCreateSql(): string { protected static function verifix_col($def): string {
$channel = $this->channel; if (!is_string($def)) $def = strval($def);
return $this->storage->_getMigration($channel)->getSql(get_class($channel), $this->db()); $def = trim($def);
} $parts = preg_split('/\s+/', $def, 2);
if (count($parts) == 2) {
/** @var CapacitorChannel[] */ $def = $parts[0];
protected ?array $subChannels = null; $rest = " $parts[1]";
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this->channel;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof Capacitor) $channel = $channel->getChannel();
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else { } else {
throw exceptions::invalid_type($channel, "channel", CapacitorChannel::class); $rest = null;
} }
switch ($def) {
case "serdata":
case "Cdata": $def = static::CDATA_DEFINITION; break;
case "sersum":
case "Csum": $def = static::CSUM_DEFINITION; break;
case "serts":
case "Ctimestamp": $def = static::CTIMESTAMP_DEFINITION; break;
case "genserial":
case "Gserial": $def = static::GSERIAL_DEFINITION; break;
case "genlic":
case "Glic": $def = static::GLIC_DEFINITION; break;
case "genlib":
case "Glib": $def = static::GLIB_DEFINITION; break;
case "gentext":
case "Gtext": $def = static::GTEXT_DEFINITION; break;
case "genbool":
case "Gbool": $def = static::GBOOL_DEFINITION; break;
case "genuuid":
case "Guuid": $def = static::GUUID_DEFINITION; break;
} }
} return "$def$rest";
return $this;
} }
function inTransaction(): bool { const PRIMARY_KEY_DEFINITION = [
return $this->db()->inTransaction(); "id_" => "Gserial",
];
const COLUMN_DEFINITIONS = [
"item__" => "Cdata",
"item__sum_" => "Csum",
"created_" => "Ctimestamp",
"modified_" => "Ctimestamp",
];
protected function getColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
if (is_array($def)) {
if (!$ignoreMigrations) {
$mdefs = $def;
$mindex = 0;
foreach ($mdefs as $mcol => $mdef) {
if ($mcol === $mindex) {
$mindex++;
} else {
if ($mdef) {
$definitions[$mcol] = self::verifix_col($mdef);
} else {
unset($definitions[$mcol]);
}
}
}
}
} else {
$constraints[] = $def;
}
} else {
$definitions[$col] = self::verifix_col($def);
}
}
return cl::merge($definitions, $constraints);
} }
function beginTransaction(?callable $func=null, bool $commit=true): void { /** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$colDefs = $this->getColumnDefinitions($channel);
$index = 0;
$raw = [];
foreach (array_keys($colDefs) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $row)) {
$sum = $channel->getSum($key, $row[$key]);
$raw[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $colDefs)) {
$raw[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $row)) {
$raw[$col] = $row[$key];
}
}
return $raw;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $raw): ?array {
if ($raw === null) return null;
$colDefs = $this->getColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($colDefs) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) {
$value = $raw[$col];
if ($value !== null) $value = $channel->unserialize($value);
$row[$key] = $value;
} else {
$row[$key] = $raw[$col];
}
}
return $row;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
#############################################################################
# Migration et metadata
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
const METADATA_COLS = [
"name" => "varchar not null primary key",
"value" => "varchar",
];
protected function prepareMetadata(): void {
if (!$this->tableExists(static::METADATA_TABLE)) {
$db = $this->db(); $db = $this->db();
if ($this->subChannels !== null) { $db->exec([
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait "create table",
if ($this->subManageTransactions === null) { "table" => static::METADATA_TABLE,
foreach ($this->subChannels as $channel) { "cols" => static::METADATA_COLS,
]);
$db->exec([
"insert",
"into" => static::METADATA_TABLE,
"values" => [
"name" => "version",
"value" => "1",
],
]);
}
}
protected function getCreateChannelSql(CapacitorChannel $channel): array {
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $this->getColumnDefinitions($channel, true),
];
}
abstract function getMigration(CapacitorChannel $channel): _migration;
#############################################################################
# Catalogue
const CATALOG_TABLE = "_channels";
const CATALOG_COLS = [
"name" => "varchar not null primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
protected function getCreateCatalogSql(): array {
return [
"create table if not exists",
"table" => static::CATALOG_TABLE,
"cols" => static::CATALOG_COLS,
];
}
protected function addToCatalogSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CATALOG_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
function getCatalog(): iterable {
return $this->db()->all([
"select",
"from" => static::CATALOG_TABLE,
]);
}
function isInCatalog(array $filter, ?array &$raw=null): bool {
$raw = $this->db()->one([
"select",
"from" => static::CATALOG_TABLE,
"where" => $filter,
]);
return $raw !== null;
}
#############################################################################
protected function afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->getCreateCatalogSql());
$db->exec($this->addToCatalogSql($channel));
}
function create(CapacitorChannel $channel): void {
$this->prepareMetadata();
$this->getMigration($channel)->migrate($this->db());
$this->afterCreate($channel);
}
function autocreate(CapacitorChannel $channel, bool $force=false): void {
if ($force || !$channel->isCreated()) {
$channel->ensureSetup();
$this->create($channel);
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */
function exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
protected function beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName(); $name = $channel->getName();
$this->subManageTransactions ??= []; $db->exec([
if (!array_key_exists($name, $this->subManageTransactions)) { "delete",
$this->subManageTransactions[$name] = $channel->isManageTransactions(); "from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CATALOG_TABLE,
"where" => [
"name" => $name,
],
]);
} }
$channel->setManageTransactions(false);
/** supprimer le canal spécifié */
function reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->beforeReset($channel);
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->autocreate($channel);
} }
if (!$db->inTransaction()) $db->beginTransaction();
/**
* charger une valeur dans le canal
*
* Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* est appelée en fonction du type d'opération: création ou mise à jour
*
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
*
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal
*/
function charge(CapacitorChannel $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
$channel->initCapacitor($this);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($row === [false]) return 0;
if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
} }
} elseif (!$db->inTransaction()) {
$raw = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $row));
$praw = null;
$rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) {
# modification
$praw = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
}
$now = date("Y-m-d H:i:s");
$insert = null;
if ($praw === null) {
# création
$raw = cl::merge($raw, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = func::with([$channel, "onCreate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = null;
} else {
# modification
# intégrer autant que possible les valeurs de praw dans raw, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false;
$raw = cl::merge($praw, $raw, [
"modified_" => $now,
]);
} else {
$raw = cl::merge($praw, $raw);
}
$initFunc = func::with([$channel, "onUpdate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = $this->unserialize($channel, $praw);
}
$updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
if ($func !== null) {
$updates = func::with($func, $args)
->prependArgs([$item, $row, $prow])
->bind($channel)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction(); $db->beginTransaction();
} }
if ($func !== null) { $nbModified = 0;
$commited = false;
try { try {
func::call($func, $this); if ($insert) {
if ($commit) { $id = $db->exec([
$this->commit(); "insert",
"into" => $tableName,
"values" => $raw,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$row[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
if (!cv::equals($value, $praw[$col] ?? null)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
}
}
if ($manageTransactions) {
$db->commit();
$commited = true; $commited = true;
} }
return $nbModified;
} finally { } finally {
if ($commit && !$commited) $this->rollback(); if ($manageTransactions && !$commited) $db->rollback();
}
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
}
$this->subManageTransactions = null;
} }
} }
function commit(): void { /**
$this->beforeEndTransaction(); * décharger les données du canal spécifié. seul la valeur de $item est
* fournie
*/
function discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$channel->initCapacitor($this);
$raws = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($raws as $raw) {
yield unserialize($raw['item__']);
}
if ($reset) $this->reset($channel);
}
protected function convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
}
}
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->getColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->convertValue2row($channel, $filter, $cols);
}
}
/** indiquer le nombre d'éléments du canal spécifié */
function count(CapacitorChannel $channel, $filter): int {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
/**
* obtenir la ligne correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
$raw = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $raw);
}
/**
* obtenir les lignes correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
$raws = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
foreach ($raws as $key => $raw) {
yield $key => $this->unserialize($channel, $raw);
}
}
/**
* appeler une fonction pour chaque élément du canal spécifié.
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onEach()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function each(CapacitorChannel $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$channel->initCapacitor($this);
if ($func === null) $func = CapacitorChannel::onEach;
$onEach = func::with($func)->bind($channel);
$db = $this->db(); $db = $this->db();
if ($db->inTransaction()) $db->commit(); # si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
} }
function rollback(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->rollback();
}
function exists(): bool {
return $this->storage->_exists($this->channel);
}
function ensureExists(): void {
$this->storage->_ensureExists($this->channel);
}
function reset(bool $recreate=false): void {
$this->storage->_reset($this->channel, $recreate);
}
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $row);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
$count = 0; $count = 0;
if ($items !== null) { $nbUpdated = 0;
if ($func !== null) { $tableName = $channel->getTableName();
$func = func::with($func, $args)->bind($this->channel); try {
$args ??= [];
$rows = $this->all($channel, $filter, $mergeQuery);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$row, ...$args]);
if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
} }
foreach ($items as $item) { $nbUpdated += $db->exec([
$count += $this->charge($item, $func); "update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
} }
} }
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count; return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
} }
function discharge(bool $reset=true): Traversable { /**
return $this->storage->_discharge($this->channel, $reset); * supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @return int le nombre de lignes parcourues
*/
function delete(CapacitorChannel $channel, $filter, $func=null, ?array $args=null): int {
$channel->initCapacitor($this);
if ($func === null) $func = CapacitorChannel::onDelete;
$onDelete = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->all($channel, $filter);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
} }
function count($filter=null): int { abstract function close(): void;
return $this->storage->_count($this->channel, $filter);
}
function one($filter, ?array $mergeQuery=null): ?array {
return $this->storage->_one($this->channel, $filter, $mergeQuery);
}
function all($filter, ?array $mergeQuery=null): Traversable {
return $this->storage->_all($this->channel, $filter, $mergeQuery);
}
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_each($this->channel, $filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_delete($this->channel, $filter, $func, $args);
}
function dbAll(array $query, ?array $params=null): iterable {
$primaryKeys = $this->channel->getPrimaryKeys();
return $this->storage->db()->all(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params, $primaryKeys);
}
function dbOne(array $query, ?array $params=null): ?array {
return $this->storage->db()->one(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params);
}
/** @return int|false */
function dbUpdate(array $query, ?array $params=null) {
return $this->storage->db()->exec(cl::merge([
"update",
"table" => $this->getTableName(),
], $query), $params);
}
function close(): void {
$this->storage->close();
}
} }

View File

@ -1,18 +1,24 @@
<?php <?php
namespace nulib\db; namespace nulib\db;
use nulib\app\app;
use nulib\cl; use nulib\cl;
use nulib\exceptions;
use nulib\php\func;
use nulib\str; use nulib\str;
use nulib\ValueException;
use Traversable; use Traversable;
/** /**
* Class CapacitorChannel: un canal d'une instance de {@link ICapacitor} * Class CapacitorChannel: un canal de données
*/ */
class CapacitorChannel implements ITransactor { class CapacitorChannel implements ITransactor {
const NAME = null; const NAME = null;
const TABLE_NAME = null; const TABLE_NAME = null;
const AUTOCREATE = null;
protected function COLUMN_DEFINITIONS(): ?array { protected function COLUMN_DEFINITIONS(): ?array {
return static::COLUMN_DEFINITIONS; return static::COLUMN_DEFINITIONS;
} const COLUMN_DEFINITIONS = null; } const COLUMN_DEFINITIONS = null;
@ -50,16 +56,20 @@ class CapacitorChannel implements ITransactor {
return $eachCommitThreshold; return $eachCommitThreshold;
} }
function __construct(?string $name=null, ?int $eachCommitThreshold=null, ?bool $manageTransactions=null) { function __construct(?array $params=null) {
$name ??= static::NAME; $this->capacitor = null;
$tableName ??= static::TABLE_NAME;
$name = $params["name"] ?? static::NAME;
$tableName = $params["tableName"] ?? static::TABLE_NAME;
self::verifix_name($name, $tableName); self::verifix_name($name, $tableName);
$this->name = $name; $this->name = $name;
$this->tableName = $tableName; $this->tableName = $tableName;
$this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold); $autocreate = $params["autocreate"] ?? null;
$autocreate ??= !app::get()->isProductionMode();
$this->created = !$autocreate;
$this->setup = false; $this->setup = false;
$this->created = false;
$columnDefinitions = $this->COLUMN_DEFINITIONS(); $columnDefinitions = $this->COLUMN_DEFINITIONS();
$primaryKeys = cl::withn(static::PRIMARY_KEYS); $primaryKeys = cl::withn(static::PRIMARY_KEYS);
$migration = cl::withn(static::MIGRATION); $migration = cl::withn(static::MIGRATION);
@ -117,6 +127,13 @@ class CapacitorChannel implements ITransactor {
$this->columnDefinitions = $columnDefinitions; $this->columnDefinitions = $columnDefinitions;
$this->primaryKeys = $primaryKeys; $this->primaryKeys = $primaryKeys;
$this->migration = $migration; $this->migration = $migration;
$manageTransactions = $params["manageTransactions"] ?? static::MANAGE_TRANSACTIONS;
$this->manageTransactions = $manageTransactions;
$eachCommitThreshold = $params["eachCommitThreshold"] ?? null;
$eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$this->eachCommitThreshold = $eachCommitThreshold;
} }
protected string $name; protected string $name;
@ -131,40 +148,6 @@ class CapacitorChannel implements ITransactor {
return $this->tableName; return $this->tableName;
} }
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/
protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
}
/** /**
* initialiser ce channel avant sa première utilisation. * initialiser ce channel avant sa première utilisation.
*/ */
@ -319,6 +302,40 @@ class CapacitorChannel implements ITransactor {
return $sum !== $psum; return $sum !== $psum;
} }
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/
protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
}
/** /**
* méthode appelée lors du chargement avec {@link Capacitor::charge()} pour * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
* créer un nouvel élément * créer un nouvel élément
@ -400,24 +417,20 @@ class CapacitorChannel implements ITransactor {
############################################################################# #############################################################################
# Méthodes déléguées pour des workflows centrés sur le channel # Méthodes déléguées pour des workflows centrés sur le channel
/**
* @var Capacitor|null instance de Capacitor par laquelle cette instance est
* utilisée
*/
protected ?Capacitor $capacitor; protected ?Capacitor $capacitor;
function getCapacitor(): ?Capacitor { function getCapacitor(): ?Capacitor {
return $this->capacitor; return $this->capacitor;
} }
function setCapacitor(Capacitor $capacitor): self { function initCapacitor(Capacitor $capacitor, bool $autocreate=true): self {
$this->capacitor = $capacitor; if ($this->capacitor === null) $this->capacitor = $capacitor;
if ($autocreate) $this->capacitor->autocreate($this);
return $this; return $this;
} }
function initStorage(CapacitorStorage $storage): self { function db(): IDatabase {
new Capacitor($storage, $this); return $this->capacitor->db();
return $this;
} }
function ensureLive(): self { function ensureLive(): self {
@ -425,52 +438,117 @@ class CapacitorChannel implements ITransactor {
return $this; return $this;
} }
function willUpdate(...$transactors): ITransactor { function getCreateSql(): string {
return $this->capacitor->willUpdate(...$transactors); return $this->capacitor->getMigration($this)->getSql(get_class($this), $this->db());
}
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw exceptions::invalid_type($channel, "channel", CapacitorChannel::class);
}
}
}
return $this;
} }
function inTransaction(): bool { function inTransaction(): bool {
return $this->capacitor->inTransaction(); return $this->db()->inTransaction();
} }
function beginTransaction(?callable $func=null, bool $commit=true): void { function beginTransaction(?callable $func=null, bool $commit=true): void {
$this->capacitor->beginTransaction($func, $commit); $db = $this->db();
if ($this->subChannels !== null) {
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait
if ($this->subManageTransactions === null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$this->subManageTransactions ??= [];
if (!array_key_exists($name, $this->subManageTransactions)) {
$this->subManageTransactions[$name] = $channel->isManageTransactions();
}
$channel->setManageTransactions(false);
}
if (!$db->inTransaction()) $db->beginTransaction();
}
} elseif (!$db->inTransaction()) {
$db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
}
$this->subManageTransactions = null;
}
} }
function commit(): void { function commit(): void {
$this->capacitor->commit(); $this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->commit();
} }
function rollback(): void { function rollback(): void {
$this->capacitor->rollback(); $this->beforeEndTransaction();
} $db = $this->db();
if ($db->inTransaction()) $db->rollback();
function db(): IDatabase {
return $this->capacitor->getStorage()->db();
} }
function exists(): bool { function exists(): bool {
return $this->capacitor->exists(); return $this->capacitor->exists($this);
}
function ensureExists(): void {
$this->capacitor->ensureExists();
} }
function reset(bool $recreate=false): void { function reset(bool $recreate=false): void {
$this->capacitor->reset($recreate); $this->capacitor->reset($this, $recreate);
} }
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int { function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->capacitor->charge($item, $func, $args, $row); return $this->capacitor->charge($this, $item, $func, $args, $row);
} }
function chargeAll(?iterable $items, $func=null, ?array $args=null): int { function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
return $this->capacitor->chargeAll($items, $func, $args); $count = 0;
if ($items !== null) {
if ($func !== null) {
$func = func::with($func, $args)->bind($this);
}
foreach ($items as $item) {
$count += $this->charge($item, $func);
}
}
return $count;
} }
function discharge(bool $reset=true): Traversable { function discharge(bool $reset=true): Traversable {
return $this->capacitor->discharge($reset); return $this->capacitor->discharge($this, $reset);
} }
/** /**
@ -496,40 +574,50 @@ class CapacitorChannel implements ITransactor {
function count($filter=null): int { function count($filter=null): int {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->count($filter); return $this->capacitor->count($this, $filter);
} }
function one($filter, ?array $mergeQuery=null): ?array { function one($filter, ?array $mergeQuery=null): ?array {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->one($filter, $mergeQuery); return $this->capacitor->one($this, $filter, $mergeQuery);
} }
function all($filter, ?array $mergeQuery=null): Traversable { function all($filter, ?array $mergeQuery=null): Traversable {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->all($filter, $mergeQuery); return $this->capacitor->all($this, $filter, $mergeQuery);
} }
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated); return $this->capacitor->each($this, $filter, $func, $args, $mergeQuery, $nbUpdated);
} }
function delete($filter, $func=null, ?array $args=null): int { function delete($filter, $func=null, ?array $args=null): int {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->delete($filter, $func, $args); return $this->capacitor->delete($this, $filter, $func, $args);
} }
function dbAll(array $query, ?array $params=null): iterable { function dbAll(array $query, ?array $params=null): iterable {
return $this->capacitor->dbAll($query, $params); $primaryKeys = $this->getPrimaryKeys();
return $this->capacitor->db()->all(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params, $primaryKeys);
} }
function dbOne(array $query, ?array $params=null): ?array { function dbOne(array $query, ?array $params=null): ?array {
return $this->capacitor->dbOne($query, $params); return $this->capacitor->db()->one(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params);
} }
/** @return int|false */ /** @return int|false */
function dbUpdate(array $query, ?array $params=null) { function dbUpdate(array $query, ?array $params=null) {
return $this->capacitor->dbUpdate($query, $params); return $this->capacitor->db()->exec(cl::merge([
"update",
"table" => $this->getTableName(),
], $query), $params);
} }
function close(): void { function close(): void {

View File

@ -1,770 +0,0 @@
<?php
namespace nulib\db;
use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\db\_private\_migration;
use nulib\exceptions;
use nulib\php\func;
use Traversable;
/**
* Class CapacitorStorage: objet permettant d'accumuler des données pour les
* réutiliser plus tard
*/
abstract class CapacitorStorage {
abstract function db(): IDatabase;
function ensureLive(): self {
$this->db()->ensureLive();
return $this;
}
/** @var CapacitorChannel[] */
protected $channels;
function addChannel(CapacitorChannel $channel): CapacitorChannel {
$this->_create($channel);
$this->channels[$channel->getName()] = $channel;
return $channel;
}
protected function getChannel(?string $name): CapacitorChannel {
CapacitorChannel::verifix_name($name);
$channel = $this->channels[$name] ?? null;
if ($channel === null) {
$channel = $this->addChannel(new CapacitorChannel($name));
}
return $channel;
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "genserial",
];
# les définitions sont par défaut pour MariaDB/MySQL
const SERDATA_DEFINITION = "mediumtext";
const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "datetime";
const GENSERIAL_DEFINITION = "integer primary key auto_increment";
const GENLIC_DEFINITION = "varchar(80)";
const GENLIB_DEFINITION = "varchar(255)";
const GENTEXT_DEFINITION = "mediumtext";
const GENBOOL_DEFINITION = "integer(1) default 0";
const GENUUID_DEFINITION = "varchar(36)";
protected static function gencol($def): string {
if (!is_string($def)) $def = strval($def);
$def = trim($def);
$parts = preg_split('/\s+/', $def, 2);
if (count($parts) == 2) {
$def = $parts[0];
$rest = " $parts[1]";
} else {
$rest = null;
}
switch ($def) {
case "serdata": $def = static::SERDATA_DEFINITION; break;
case "sersum": $def = static::SERSUM_DEFINITION; break;
case "serts": $def = static::SERTS_DEFINITION; break;
case "genserial": $def = static::GENSERIAL_DEFINITION; break;
case "genlic": $def = static::GENLIC_DEFINITION; break;
case "genlib": $def = static::GENLIB_DEFINITION; break;
case "gentext": $def = static::GENTEXT_DEFINITION; break;
case "genbool": $def = static::GENBOOL_DEFINITION; break;
case "genuuid": $def = static::GENUUID_DEFINITION; break;
}
return "$def$rest";
}
const COLUMN_DEFINITIONS = [
"item__" => "serdata",
"item__sum_" => "sersum",
"created_" => "serts",
"modified_" => "serts",
];
protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
if (is_array($def)) {
if (!$ignoreMigrations) {
$mdefs = $def;
$mindex = 0;
foreach ($mdefs as $mcol => $mdef) {
if ($mcol === $mindex) {
$mindex++;
} else {
if ($mdef) {
$definitions[$mcol] = self::gencol($mdef);
} else {
unset($definitions[$mcol]);
}
}
}
}
} else {
$constraints[] = $def;
}
} else {
$definitions[$col] = self::gencol($def);
}
}
return cl::merge($definitions, $constraints);
}
protected function getMigration(CapacitorChannel $channel): ?array {
return $channel->getMigration($this->db()->getPrefix());
}
/** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$raw = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $row)) {
$sum = $channel->getSum($key, $row[$key]);
$raw[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $cols)) {
$raw[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $row)) {
$raw[$col] = $row[$key];
}
}
return $raw;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $raw): ?array {
if ($raw === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) {
$value = $raw[$col];
if ($value !== null) $value = $channel->unserialize($value);
$row[$key] = $value;
} else {
$row[$key] = $raw[$col];
}
}
return $row;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
protected function _createSql(CapacitorChannel $channel): array {
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $this->ColumnDefinitions($channel, true),
];
}
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
const METADATA_COLS = [
"name" => "varchar not null primary key",
"value" => "varchar",
];
protected function _prepareMetadata(): void {
if (!$this->tableExists(static::METADATA_TABLE)) {
$db = $this->db();
$db->exec([
"drop table if exists",
"table" => self::CHANNELS_TABLE,
]);
$db->exec([
"drop table if exists",
"table" => _migration::MIGRATION_TABLE,
]);
$db->exec([
"create table",
"table" => static::METADATA_TABLE,
"cols" => static::METADATA_COLS,
]);
$db->exec([
"insert",
"into" => static::METADATA_TABLE,
"values" => [
"name" => "version",
"value" => "1",
],
]);
}
}
abstract function _getMigration(CapacitorChannel $channel): _migration;
const CHANNELS_TABLE = "_channels";
const CHANNELS_COLS = [
"name" => "varchar not null primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
function channelExists(string $name, ?array &$raw=null): bool {
$raw = $this->db()->one([
"select",
"from" => static::CHANNELS_TABLE,
"where" => ["name" => $name],
]);
return $raw !== null;
}
function getChannels(): iterable {
return $this->db()->all([
"select",
"from" => static::CHANNELS_TABLE,
]);
}
protected function _createChannelsSql(): array {
return [
"create table if not exists",
"table" => static::CHANNELS_TABLE,
"cols" => static::CHANNELS_COLS,
];
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CHANNELS_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->_createChannelsSql());
$db->exec($this->_addToChannelsSql($channel));
}
protected function _create(CapacitorChannel $channel): void {
$channel->ensureSetup();
if (!$channel->isCreated()) {
$this->_prepareMetadata();
$this->_getMigration($channel)->migrate($this->db());
$this->_afterCreate($channel);
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */
function _exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
function exists(?string $channel): bool {
return $this->_exists($this->getChannel($channel));
}
/** s'assurer que le canal spécifié existe */
function _ensureExists(CapacitorChannel $channel): void {
$this->_create($channel);
}
function ensureExists(?string $channel): void {
$this->_ensureExists($this->getChannel($channel));
}
protected function _beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName();
$db->exec([
"delete",
"from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CHANNELS_TABLE,
"where" => [
"name" => $name,
],
]);
}
/** supprimer le canal spécifié */
function _reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->_beforeReset($channel);
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->_ensureExists($channel);
}
function reset(?string $channel, bool $recreate=false): void {
$this->_reset($this->getChannel($channel), $recreate);
}
/**
* charger une valeur dans le canal
*
* Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* est appelée en fonction du type d'opération: création ou mise à jour
*
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
*
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal
*/
function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$row=null): int {
$this->_create($channel);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($row === [false]) return 0;
if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
}
$raw = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $row));
$praw = null;
$rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) {
# modification
$praw = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
}
$now = date("Y-m-d H:i:s");
$insert = null;
if ($praw === null) {
# création
$raw = cl::merge($raw, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = func::with([$channel, "onCreate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = null;
} else {
# modification
# intégrer autant que possible les valeurs de praw dans raw, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false;
$raw = cl::merge($praw, $raw, [
"modified_" => $now,
]);
} else {
$raw = cl::merge($praw, $raw);
}
$initFunc = func::with([$channel, "onUpdate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = $this->unserialize($channel, $praw);
}
$updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
if ($func !== null) {
$updates = func::with($func, $args)
->prependArgs([$item, $row, $prow])
->bind($channel)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
$nbModified = 0;
try {
if ($insert) {
$id = $db->exec([
"insert",
"into" => $tableName,
"values" => $raw,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$row[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
if (!cv::equals($value, $praw[$col] ?? null)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
}
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $nbModified;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args, $row);
}
/**
* décharger les données du canal spécifié. seul la valeur de $item est
* fournie
*/
function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$this->_create($channel);
$raws = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($raws as $raw) {
yield unserialize($raw['item__']);
}
if ($reset) $this->_reset($channel);
}
function discharge(?string $channel, bool $reset=true): Traversable {
return $this->_discharge($this->getChannel($channel), $reset);
}
protected function _convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->_convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
}
}
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->ColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->_convertValue2row($channel, $filter, $cols);
}
}
/** indiquer le nombre d'éléments du canal spécifié */
function _count(CapacitorChannel $channel, $filter): int {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
function count(?string $channel, $filter=null): int {
return $this->_count($this->getChannel($channel), $filter);
}
/**
* obtenir la ligne correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function _one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
if ($filter === null) throw exceptions::null_value("filter");
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$raw = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $raw);
}
function one(?string $channel, $filter, ?array $mergeQuery=null): ?array {
return $this->_one($this->getChannel($channel), $filter, $mergeQuery);
}
/**
* obtenir les lignes correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$raws = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
foreach ($raws as $key => $raw) {
yield $key => $this->unserialize($channel, $raw);
}
}
function all(?string $channel, $filter, $mergeQuery=null): Traversable {
return $this->_all($this->getChannel($channel), $filter, $mergeQuery);
}
/**
* appeler une fonction pour chaque élément du canal spécifié.
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onEach()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onEach;
$onEach = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$nbUpdated = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->_all($channel, $filter, $mergeQuery);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$row, ...$args]);
if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
$nbUpdated += $db->exec([
"update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function each(?string $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
return $this->_each($this->getChannel($channel), $filter, $func, $args, $mergeQuery, $nbUpdated);
}
/**
* supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @return int le nombre de lignes parcourues
*/
function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onDelete;
$onDelete = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->_all($channel, $filter);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function delete(?string $channel, $filter, $func=null, ?array $args=null): int {
return $this->_delete($this->getChannel($channel), $filter, $func, $args);
}
abstract function close(): void;
}

View File

@ -1,5 +1,11 @@
# db
# db/Capacitor # db/Capacitor
La source peut être un iterable
---
charge() permet de spécifier la clé associée avec la valeur chargée, et charge() permet de spécifier la clé associée avec la valeur chargée, et
discharge() retourne les valeurs avec la clé primaire discharge() retourne les valeurs avec la clé primaire

View File

@ -25,14 +25,11 @@ class conds {
/** /**
* retourner une condition "like" si la valeur s'y prête * retourner une condition "like" si la valeur s'y prête
*
* - si la valeur fait moins de $likeThreshold caractères, faire une recherche * - si la valeur fait moins de $likeThreshold caractères, faire une recherche
* exacte en retournant ["=", $value] * exacte en retournant ["=", $value]
*
*
* - les espaces sont remplacés par % * - les espaces sont remplacés par %
* * - si $partial et que $value ne contient pas d'espaces, rajouter un % à la
* si $partial * fin
*/ */
static function like($value, bool $partial=false, ?int $likeThreshold=null) { static function like($value, bool $partial=false, ?int $likeThreshold=null) {
if ($value === false || $value === null) return $value; if ($value === false || $value === null) return $value;

View File

@ -3,12 +3,22 @@ namespace nulib\db\mysql;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\Capacitor;
/** /**
* Class MysqlStorage * Class MysqlStorage
*/ */
class MysqlStorage extends CapacitorStorage { class MysqlCapacitor extends Capacitor {
const CDATA_DEFINITION = "mediumtext";
const CSUM_DEFINITION = "varchar(40)";
const CTIMESTAMP_DEFINITION = "datetime";
const GSERIAL_DEFINITION = "integer primary key auto_increment";
const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "mediumtext";
const GBOOL_DEFINITION = "integer(1) default 0";
const GUUID_DEFINITION = "varchar(36)";
function __construct($mysql) { function __construct($mysql) {
$this->db = Mysql::with($mysql); $this->db = Mysql::with($mysql);
} }
@ -36,21 +46,21 @@ class MysqlStorage extends CapacitorStorage {
"value" => "varchar(255)", "value" => "varchar(255)",
]; ];
function _getMigration(CapacitorChannel $channel): _mysqlMigration { function getMigration(CapacitorChannel $channel): _mysqlMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _mysqlMigration($migrations, $channel->getName()); return new _mysqlMigration($migrations, $channel->getName());
} }
const CHANNELS_COLS = [ const CATALOG_COLS = [
"name" => "varchar(255) not null primary key", "name" => "varchar(255) not null primary key",
"table_name" => "varchar(64)", "table_name" => "varchar(64)",
"class_name" => "varchar(255)", "class_name" => "varchar(255)",
]; ];
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function addToCatalogSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [ return cl::merge(parent::addToCatalogSql($channel), [
"suffix" => "on duplicate key update name = name", "suffix" => "on duplicate key update name = name",
]); ]);
} }

View File

@ -3,16 +3,18 @@ namespace nulib\db\pgsql;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\Capacitor;
class PgsqlStorage extends CapacitorStorage { class PgsqlCapacitor extends Capacitor {
const SERDATA_DEFINITION = "text"; const CDATA_DEFINITION = "text";
const SERSUM_DEFINITION = "varchar(40)"; const CSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "timestamp"; const CTIMESTAMP_DEFINITION = "timestamp";
const GENSERIAL_DEFINITION = "serial primary key"; const GSERIAL_DEFINITION = "serial primary key";
const GENTEXT_DEFINITION = "text"; const GLIC_DEFINITION = "varchar(80)";
const GENBOOL_DEFINITION = "boolean default false"; const GLIB_DEFINITION = "varchar(255)";
const GENUUID_DEFINITION = "uuid"; const GTEXT_DEFINITION = "text";
const GBOOL_DEFINITION = "boolean default false";
const GUUID_DEFINITION = "uuid";
function __construct($pgsql) { function __construct($pgsql) {
$this->db = Pgsql::with($pgsql); $this->db = Pgsql::with($pgsql);
@ -41,15 +43,15 @@ class PgsqlStorage extends CapacitorStorage {
return $found !== null; return $found !== null;
} }
function _getMigration(CapacitorChannel $channel): _pgsqlMigration { function getMigration(CapacitorChannel $channel): _pgsqlMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _pgsqlMigration($migrations, $channel->getName()); return new _pgsqlMigration($migrations, $channel->getName());
} }
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function addToCatalogSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [ return cl::merge(parent::addToCatalogSql($channel), [
"suffix" => "on conflict (name) do nothing", "suffix" => "on conflict (name) do nothing",
]); ]);
} }

View File

@ -3,13 +3,21 @@ namespace nulib\db\sqlite;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\Capacitor;
/** /**
* Class SqliteStorage * Class SqliteStorage
*/ */
class SqliteStorage extends CapacitorStorage { class SqliteCapacitor extends Capacitor {
const GENSERIAL_DEFINITION = "integer primary key autoincrement"; const CDATA_DEFINITION = "mediumtext";
const CSUM_DEFINITION = "varchar(40)";
const CTIMESTAMP_DEFINITION = "datetime";
const GSERIAL_DEFINITION = "integer primary key autoincrement";
const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "mediumtext";
const GBOOL_DEFINITION = "integer(1) default 0";
const GUUID_DEFINITION = "varchar(36)";
function __construct($sqlite) { function __construct($sqlite) {
$this->db = Sqlite::with($sqlite); $this->db = Sqlite::with($sqlite);
@ -31,30 +39,30 @@ class SqliteStorage extends CapacitorStorage {
return $found !== null; return $found !== null;
} }
function _getMigration(CapacitorChannel $channel): _sqliteMigration { function getMigration(CapacitorChannel $channel): _sqliteMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _sqliteMigration($migrations, $channel->getName()); return new _sqliteMigration($migrations, $channel->getName());
} }
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function addToCatalogSql(CapacitorChannel $channel): array {
$sql = parent::_addToChannelsSql($channel); $sql = parent::addToCatalogSql($channel);
$sql[0] = "insert or ignore"; $sql[0] = "insert or ignore";
return $sql; return $sql;
} }
protected function _afterCreate(CapacitorChannel $channel): void { protected function afterCreate(CapacitorChannel $channel): void {
$db = $this->db; $db = $this->db;
if (!$this->tableExists(static::CHANNELS_TABLE)) { if (!$this->tableExists(static::CATALOG_TABLE)) {
# ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture # verrou en écriture
$db->exec($this->_createChannelsSql()); $db->exec($this->getCreateCatalogSql());
} }
if (!$this->channelExists($channel->getName())) { if (!$this->isInCatalog(["name" => $channel->getName()])) {
# ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture # verrou en écriture
$db->exec($this->_addToChannelsSql($channel)); $db->exec($this->addToCatalogSql($channel));
} }
} }

View File

@ -2,27 +2,15 @@
require __DIR__.'/../vendor/autoload.php'; require __DIR__.'/../vendor/autoload.php';
use nulib\cl; use nulib\cl;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\mysql\Mysql; use nulib\db\mysql\MysqlCapacitor;
use nulib\db\mysql\MysqlStorage;
use nulib\output\msg;
use nulib\output\std\ConsoleMessenger;
msg::set_messenger_class(ConsoleMessenger::class);
$db = new Mysql([
"type" => "mysql",
"name" => "mysql:host=mysql.devel.self;dbname=jclain;charset=utf8",
"user" => "jclain",
]);
class MyChannel extends CapacitorChannel { class MyChannel extends CapacitorChannel {
const TABLE_NAME = "my"; const TABLE_NAME = "my";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar(64) not null", "name" => "varchar(64) not null",
"age" => "integer", "age" => "integer",
"num" => ["integer"], "num" => "integer",
]; ];
function getItemValues($item): ?array { function getItemValues($item): ?array {
@ -35,7 +23,12 @@ class MyChannel extends CapacitorChannel {
} }
} }
new Capacitor(new MysqlStorage($db), $channel = new MyChannel()); $capacitor = new MysqlCapacitor([
"type" => "mysql",
"name" => "mysql:host=mysql.devel.self;dbname=jclain;charset=utf8",
"user" => "jclain",
]);
$channel = $capacitor->newChannel(new MyChannel());
$channel->charge("hello world"); $channel->charge("hello world");
$channel->charge(["bonjour monde"]); $channel->charge(["bonjour monde"]);

View File

@ -5,23 +5,15 @@ use nulib\cl;
use nulib\db\Capacitor; use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\pgsql\Pgsql; use nulib\db\pgsql\Pgsql;
use nulib\db\pgsql\PgsqlStorage; use nulib\db\pgsql\PgsqlCapacitor;
use nulib\db\sqlite\SqliteCapacitor;
$db = new Pgsql([
"host" => "pegase-dre.self",
"dbname" => "dre",
"user" => "root",
"password" => "admin",
#"user" => "reader",
#"password" => "reader",
]);
class MyChannel extends CapacitorChannel { class MyChannel extends CapacitorChannel {
const TABLE_NAME = "my"; const TABLE_NAME = "my";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar not null", "name" => "varchar not null",
"age" => "integer", "age" => "integer",
"num" => ["integer"], "num" => "integer",
]; ];
function getItemValues($item): ?array { function getItemValues($item): ?array {
@ -34,7 +26,15 @@ class MyChannel extends CapacitorChannel {
} }
} }
new Capacitor(new PgsqlStorage($db), $channel = new MyChannel()); $capacitor = new PgsqlCapacitor([
"host" => "pegase-dre.self",
"dbname" => "prod_dre",
"user" => "postgres",
"password" => "admin",
#"user" => "reader",
#"password" => "reader",
]);
$channel = $capacitor->newChannel(new MyChannel());
$channel->charge("hello world"); $channel->charge("hello world");
$channel->charge(["bonjour monde"]); $channel->charge(["bonjour monde"]);

View File

@ -2,19 +2,15 @@
require __DIR__.'/../vendor/autoload.php'; require __DIR__.'/../vendor/autoload.php';
use nulib\cl; use nulib\cl;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\sqlite\Sqlite; use nulib\db\sqlite\SqliteCapacitor;
use nulib\db\sqlite\SqliteStorage;
$db = new Sqlite(__DIR__.'/test_sqlite.db');
class MyChannel extends CapacitorChannel { class MyChannel extends CapacitorChannel {
const TABLE_NAME = "my"; const TABLE_NAME = "my";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar not null", "name" => "varchar not null",
"age" => "integer", "age" => "integer",
"num" => ["integer"], "num" => "integer",
]; ];
function getItemValues($item): ?array { function getItemValues($item): ?array {
@ -27,7 +23,8 @@ class MyChannel extends CapacitorChannel {
} }
} }
new Capacitor(new SqliteStorage($db), $channel = new MyChannel()); $capacitor = new SqliteCapacitor(__DIR__.'/test-sqlite.db');
$channel = $capacitor->newChannel(new MyChannel());
$channel->charge("hello world"); $channel->charge("hello world");
$channel->charge(["bonjour monde"]); $channel->charge(["bonjour monde"]);

View File

@ -1,22 +1,14 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
use nulib\db\Capacitor;
use nulib\db\sqlite\impl\MyChannel; use nulib\db\sqlite\impl\MyChannel;
use nulib\db\sqlite\impl\MyChannelV2; use nulib\db\sqlite\impl\MyChannelV2;
use nulib\db\sqlite\impl\MyChannelV3; use nulib\db\sqlite\impl\MyChannelV3;
use nulib\db\sqlite\impl\MyIndexChannel; use nulib\db\sqlite\impl\MyIndexChannel;
use nulib\output\msg;
use nulib\output\std\ConsoleMessenger;
use nulib\php\time\DateTime; use nulib\php\time\DateTime;
use nulib\tests\TestCase; use nulib\tests\TestCase;
class ChannelMigrationTest extends TestCase { class ChannelMigrationTest extends TestCase {
static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
msg::set_messenger_class(ConsoleMessenger::class);
}
protected function addData(MyChannel $channel, array $data): void { protected function addData(MyChannel $channel, array $data): void {
foreach ($data as [$name, $value, $dateCre, $dateMod, $age]) { foreach ($data as [$name, $value, $dateCre, $dateMod, $age]) {
$channel->charge([ $channel->charge([
@ -30,23 +22,23 @@ class ChannelMigrationTest extends TestCase {
} }
function testMigration() { function testMigration() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$data = [ $data = [
["first", "premier", new DateTime(), new DateTime(), 15], ["first", "premier", new DateTime(), new DateTime(), 15],
["second", "deuxieme", new DateTime(), new DateTime(), 15], ["second", "deuxieme", new DateTime(), new DateTime(), 15],
]; ];
new Capacitor($storage, $channel = new MyChannel()); $capacitor->newChannel($channel = new MyChannel());
$channel->reset(true); $channel->reset(true);
$this->addData($channel, $data); $this->addData($channel, $data);
new Capacitor($storage, $channel = new MyChannelV2()); $capacitor->newChannel($channel = new MyChannelV2());
$this->addData($channel, $data); $this->addData($channel, $data);
new Capacitor($storage, $channel = new MyChannelV3()); $capacitor->newChannel($channel = new MyChannelV3());
$this->addData($channel, $data); $this->addData($channel, $data);
$sql = $channel->getCapacitor()->getCreateSql(); $sql = $channel->getCreateSql();
$class = MyChannelV3::class; $class = MyChannelV3::class;
$expected = <<<EOT $expected = <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
@ -75,17 +67,17 @@ EOT;
} }
function testMigrationIndex() { function testMigrationIndex() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$data = [ $data = [
["un", "premier", "first"], ["un", "premier", "first"],
["deux", "deuxieme", "second"], ["deux", "deuxieme", "second"],
]; ];
new Capacitor($storage, $channel = new MyIndexChannel()); $capacitor->newChannel($channel = new MyIndexChannel());
$channel->reset(true); $channel->reset(true);
$channel->chargeAll($data); $channel->chargeAll($data);
$sql = $channel->getCapacitor()->getCreateSql(); $sql = $channel->getCreateSql();
$class = MyIndexChannel::class; $class = MyIndexChannel::class;
$expected = <<<EOT $expected = <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

View File

@ -6,38 +6,48 @@ use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\tests\TestCase; use nulib\tests\TestCase;
class SqliteStorageTest extends TestCase { class SqliteCapacitorTest extends TestCase {
static function Txx(...$values): void { static function Txx(...$values): void {
foreach ($values as $value) { foreach ($values as $value) {
var_export($value); var_export($value);
} }
} }
function _testChargeStrings(SqliteStorage $storage, ?string $channel) { function _testChargeStrings(SqliteCapacitor $capacitor, CapacitorChannel $channel) {
$storage->reset($channel); $capacitor->reset($channel);
$storage->charge($channel, "first"); $capacitor->charge($channel, "first");
$storage->charge($channel, "second"); $capacitor->charge($channel, "second");
$storage->charge($channel, "third"); $capacitor->charge($channel, "third");
$items = cl::all($storage->discharge($channel, false)); $items = cl::all($capacitor->discharge($channel, false));
self::assertSame(["first", "second", "third"], $items); self::assertSame(["first", "second", "third"], $items);
} }
function _testChargeArrays(SqliteStorage $storage, ?string $channel) { function _testChargeArrays(SqliteCapacitor $capacitor, CapacitorChannel $channel) {
$storage->reset($channel); $capacitor->reset($channel);
$storage->charge($channel, ["id" => 10, "name" => "first"]); $capacitor->charge($channel, ["id" => 10, "name" => "first"]);
$storage->charge($channel, ["name" => "second", "id" => 20]); $capacitor->charge($channel, ["name" => "second", "id" => 20]);
$storage->charge($channel, ["name" => "third", "id" => "30"]); $capacitor->charge($channel, ["name" => "third", "id" => "30"]);
} }
function testChargeStrings() { function testChargeStrings() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$this->_testChargeStrings($storage, null);
$storage->close(); $channel = $capacitor->newChannel(null);
$this->_testChargeStrings($capacitor, $channel);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$channel = $capacitor->newChannel("strings");
$this->_testChargeStrings($capacitor, $channel);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$capacitor->close();
self::assertTrue(true);
} }
function testChargeArrays() { function testChargeArrays() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$storage->addChannel(new class extends CapacitorChannel {
$channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "arrays"; const NAME = "arrays";
const COLUMN_DEFINITIONS = ["id" => "integer"]; const COLUMN_DEFINITIONS = ["id" => "integer"];
@ -45,15 +55,16 @@ class SqliteStorageTest extends TestCase {
return ["id" => $item["id"] ?? null]; return ["id" => $item["id"] ?? null];
} }
}); });
$this->_testChargeArrays($capacitor, $channel);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$this->_testChargeStrings($storage, "strings"); $capacitor->close();
$this->_testChargeArrays($storage, "arrays"); self::assertTrue(true);
$storage->close();
} }
function testEach() { function testEach() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $each = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "each"; const NAME = "each";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"age" => "integer", "age" => "integer",
@ -67,11 +78,11 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($each);
$capacitor->charge(["name" => "first", "age" => 5]); $capacitor->charge($each, ["name" => "first", "age" => 5]);
$capacitor->charge(["name" => "second", "age" => 10]); $capacitor->charge($each, ["name" => "second", "age" => 10]);
$capacitor->charge(["name" => "third", "age" => 15]); $capacitor->charge($each, ["name" => "third", "age" => 15]);
$capacitor->charge(["name" => "fourth", "age" => 20]); $capacitor->charge($each, ["name" => "fourth", "age" => 20]);
$setDone = function ($row, $suffix=null) { $setDone = function ($row, $suffix=null) {
$item = $row["item"]; $item = $row["item"];
@ -82,17 +93,18 @@ class SqliteStorageTest extends TestCase {
} }
return $updates; return $updates;
}; };
$capacitor->each(["age" => [">", 10]], $setDone, ["++"]); $capacitor->each($each, ["age" => [">", 10]], $setDone, ["++"]);
$capacitor->each(["done" => 0], $setDone); $capacitor->each($each, ["done" => 0], $setDone);
self::Txx(cl::all($capacitor->discharge($each, false)));
self::Txx(cl::all($capacitor->discharge(false)));
$capacitor->close(); $capacitor->close();
self::assertTrue(true); self::assertTrue(true);
} }
function testPrimayKey() { function testPrimayKey() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "pk"; const NAME = "pk";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"id_" => "varchar primary key", "id_" => "varchar primary key",
@ -106,21 +118,23 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->charge(["numero" => "a", "name" => "first", "age" => 5]); $capacitor->charge($channel, ["numero" => "a", "name" => "first", "age" => 5]);
$capacitor->charge(["numero" => "b", "name" => "second", "age" => 10]); $capacitor->charge($channel, ["numero" => "b", "name" => "second", "age" => 10]);
$capacitor->charge(["numero" => "c", "name" => "third", "age" => 15]); $capacitor->charge($channel, ["numero" => "c", "name" => "third", "age" => 15]);
$capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 20]); $capacitor->charge($channel, ["numero" => "d", "name" => "fourth", "age" => 20]);
sleep(2); sleep(2);
$capacitor->charge(["numero" => "b", "name" => "second", "age" => 100]); $capacitor->charge($channel, ["numero" => "b", "name" => "second", "age" => 100]);
$capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 200]); $capacitor->charge($channel, ["numero" => "d", "name" => "fourth", "age" => 200]);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$capacitor->close(); $capacitor->close();
self::assertTrue(true); self::assertTrue(true);
} }
function testSum() { function testSum() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "sum"; const NAME = "sum";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"a__" => "varchar", "a__" => "varchar",
@ -136,19 +150,17 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($channel);
$capacitor->charge(["a" => null, "b" => null]); $capacitor->charge($channel, ["a" => null, "b" => null]);
$capacitor->charge(["a" => "first", "b" => "second"]); $capacitor->charge($channel, ["a" => "first", "b" => "second"]);
self::Txx("=== all"); self::Txx("=== all");
/** @var Sqlite $sqlite */ self::Txx(cl::all($capacitor->db()->all([
$sqlite = $capacitor->getStorage()->db();
self::Txx(cl::all($sqlite->all([
"select", "select",
"from" => $capacitor->getChannel()->getTableName(), "from" => $channel->getTableName(),
]))); ])));
self::Txx("=== each"); self::Txx("=== each");
$capacitor->each(null, function ($row) { $capacitor->each($channel, null, function ($row) {
self::Txx($row); self::Txx($row);
}); });
@ -158,8 +170,8 @@ class SqliteStorageTest extends TestCase {
function testEachValues() { function testEachValues() {
# tester que values contient bien toutes les valeurs de la ligne # tester que values contient bien toutes les valeurs de la ligne
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "each_values"; const NAME = "each_values";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar primary key", "name" => "varchar primary key",
@ -176,8 +188,8 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($channel);
$capacitor->charge(["name" => "first", "age" => 5], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 5], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(5, $item["age"]); self::assertSame(5, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -189,7 +201,7 @@ class SqliteStorageTest extends TestCase {
], cl::select($row, ["name", "age", "item"])); ], cl::select($row, ["name", "age", "item"]));
self::assertNull($prow); self::assertNull($prow);
}); });
$capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -211,7 +223,7 @@ class SqliteStorageTest extends TestCase {
], cl::select($prow, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
$capacitor->each(null, function(array $row) { $capacitor->each($channel, null, function(array $row) {
$item = $row["item"]; $item = $row["item"];
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
@ -229,7 +241,7 @@ class SqliteStorageTest extends TestCase {
"notes" => "modified", "notes" => "modified",
]; ];
}); });
$capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -251,7 +263,7 @@ class SqliteStorageTest extends TestCase {
], cl::select($prow, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
$capacitor->charge(["name" => "first", "age" => 20], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 20], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(20, $item["age"]); self::assertSame(20, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -276,8 +288,8 @@ class SqliteStorageTest extends TestCase {
function testSetItemNull() { function testSetItemNull() {
# tester le forçage de $îtem à null pour économiser la place # tester le forçage de $îtem à null pour économiser la place
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "set_item_null"; const NAME = "set_item_null";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar primary key", "name" => "varchar primary key",
@ -294,8 +306,8 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($channel);
$nbModified = $capacitor->charge(["name" => "first", "age" => 5], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 5], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 5, "name" => "first", "age" => 5,
"item" => $item, "item" => $item,
@ -306,7 +318,7 @@ class SqliteStorageTest extends TestCase {
sleep(1); sleep(1);
# nb: on met des sleep() pour que la date de modification soit systématiquement différente # nb: on met des sleep() pour que la date de modification soit systématiquement différente
$nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
@ -321,7 +333,7 @@ class SqliteStorageTest extends TestCase {
sleep(1); sleep(1);
# pas de modification ici # pas de modification ici
$nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
@ -335,7 +347,7 @@ class SqliteStorageTest extends TestCase {
self::assertSame(0, $nbModified); self::assertSame(0, $nbModified);
sleep(1); sleep(1);
$nbModified = $capacitor->charge(["name" => "first", "age" => 20], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 20], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 20, "name" => "first", "age" => 20,
"item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5", "item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5",

View File

@ -1,117 +1,298 @@
#!/bin/bash #!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1 source "$(dirname -- "$0")/../load.sh" || exit 1
require: git pman pman.tool pman.conf require: git pman pman.conf "pman.tool.$MYNAME"
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de fusionner la branche"
function dump_action() {
echo -n "\
SRC_TYPE=$SRC_TYPE
SRC_BRANCH=$SRC_BRANCH
DEST_TYPE=$DEST_TYPE
DEST_BRANCH=$DEST_BRANCH
CurrentBranch=$CurrentBranch
LocalBranches=${LocalBranches[*]}
RemoteBranches=${RemoteBranches[*]}
AllBranches=${AllBranches[*]}
SrcType=$SrcType
SrcBranch=$SrcBranch
DestType=$DestType
DestBranch=$DestBranch
UpstreamBranch=$UpstreamBranch
FeatureBranches=${FeatureBranches[*]}
DevelopBranch=$DevelopBranch
ReleaseBranch=$ReleaseBranch
HotfixBranch=$HotfixBranch
MainBranch=$MainBranch
DistBranch=$DistBranch
"
}
function _ensure_src_branch() {
[ -n "$SrcBranch" ] || die "La branche $SRC_TYPE n'a pas été définie"
[ "$1" == init -o -n "${!SRC_BRANCH}" ] || die "$SrcBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _ensure_dest_branch() {
[ -n "$DestBranch" ] || die "La branche $DEST_TYPE n'a pas été définie"
[ "$1" == init -o -n "${!DEST_BRANCH}" ] || die "$DestBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function checkout_action() {
local -a push_branches
if [ -z "${!SRC_BRANCH}" ]; then
array_contains AllBranches "$SrcBranch" && exit_with enote "\
$SrcBranch: une branche du même nom existe dans l'origine
git checkout $SrcBranch"
_ensure_dest_branch
_ensure_src_branch init
resolve_should_push
enote "Vous allez créer la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} <-- ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
einfo "Création de la branche $SrcBranch"
git checkout -b "$SrcBranch" "$DestBranch" || die
push_branches+=("$SrcBranch")
_push_branches
fi
git checkout "$SrcBranch"
}
function ensure_branches() {
[ -n "${!SRC_BRANCH}" -a -n "${!DEST_BRANCH}" ] ||
die "${!SRC_BRANCH}: Aucune configuration de fusion trouvée pour cette branche"
array_contains LocalBranches "${!SRC_BRANCH}" || die "${!SRC_BRANCH}: branche source introuvable"
array_contains LocalBranches "${!DEST_BRANCH}" || die "${!DEST_BRANCH}: branche destination introuvable"
}
function _show_action() {
local commits
setx commits=_list_commits
if [ -n "$commits" ]; then
if [ $ShowLevel -ge 2 ]; then
{
echo "\
# Commits à fusionner ${!SRC_BRANCH} --> ${!DEST_BRANCH}
$commits
"
_sd_COLOR=always _show_diff
} | less -eRF
else
einfo "Commits à fusionner ${!SRC_BRANCH} --> ${!DEST_BRANCH}"
eecho "$commits"
fi
fi
}
function show_action() {
git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY"
ensure_branches
_show_action "$@"
}
function _merge_action() {
enote "\
Ce script va
- fusionner la branche ${COULEUR_BLEUE}${!SRC_BRANCH}${COULEUR_NORMALE} dans ${COULEUR_ROUGE}${!DEST_BRANCH}${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}"
ask_yesno "Voulez-vous continuer?" O || die
local script=".git/pman-merge.sh"
local -a push_branches delete_branches
local hook
local comment=
local or_die=" || exit 1"
_mscript_start
_scripta <<EOF
################################################################################
# merge
if [ -n "\$merge" ]; then
esection "Fusionner la branche"
EOF
hook="BEFORE_MERGE_$SRC_TYPE"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_mscript_merge_branch
hook="AFTER_MERGE_$SRC_TYPE"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
if [ -n "$ShouldDelete" ]; then
_scripta <<EOF
################################################################################
# delete
if [ -n "\$delete" ]; then
esection "Supprimer la branche"
EOF
_mscript_delete_branch
hook="AFTER_DELETE_$SRC_TYPE"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
fi
_scripta <<EOF
################################################################################
# push
if [ -n "\$push" ]; then
esection "Pousser les branches"
EOF
hook="BEFORE_PUSH_$DEST_TYPE"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_script_push_branches
if [ ${#delete_branches[*]} -gt 0 ]; then
_scripta <<<"if [ -n \"\$delete\" ]; then"
push_branches=("${delete_branches[@]}")
_script_push_branches
_scripta <<<fi
fi
hook="AFTER_PUSH_$DEST_TYPE"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
[ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
[ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
[ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then
eimportant "\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
git merge --continue
Sinon, veuillez consulter le script et/ou le relancer
./$script${Delete:+ delete}${Push:+ push}"
die
elif [ -n "$Deleted" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script"
[ -n "$AfterMerge" ] && eval "$AfterMerge"
else
local msg="\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
Vous pouvez consulter le script et/ou le relancer
./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
[ -n "$AfterMerge" ] && msg="$msg
Il y a aussi les commandes supplémentaires suivantes:
${AfterMerge//
/
}"
einfo "$msg"
fi
}
function merge_action() {
if [ -n "$MERGE_PREL" ]; then
[ -n "$ForceMerge" ] || die "$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
fi
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$SrcBranch"
[ -n "$ALLOW_DELETE" ] || Delete=
[ -z "$_Fake" ] && git_ensure_cleancheckout
if array_contains LocalBranches "$SrcBranch"; then
ensure_branches
resolve_should_push
_merge_action "$@"
elif array_contains AllBranches "$SrcBranch"; then
enote "$SrcBranch: une branche du même nom existe dans l'origine"
die "$SrcBranch: branche locale introuvable"
else
die "$SrcBranch: branche introuvable"
fi
}
function rebase_action() {
die "non implémenté"
}
################################################################################
# Programme principal
################################################################################
loaded_config= loaded_config=
if check_gitdir; then if check_gitdir; then
load_branches all load_branches all
load_config load_config "$PMAN_TOOL"
set_pman_vars if [ -n "${!SRC_TYPE}" ]; then
if [ -n "$PmanMergeSrc" ]; then load_branches current "${!SRC_TYPE}"
load_branches current "$PmanMergeSrc"
loaded_config=1
elif [ -n "$PmanRefBranch" ]; then
load_branches current "$PmanRefBranch"
loaded_config=1 loaded_config=1
fi fi
else
set_pman_vars
fi fi
[ -n "${!SRC_TYPE}" ] && SrcDesc="${!SRC_TYPE}" || SrcDesc="<$SRC_TYPE>"
SrcDesc="${COULEUR_BLEUE}$SrcDesc${COULEUR_NORMALE}"
DestDesc=
BranchDesc= purpose="gérer la branche $SrcDesc"
MergeSrcDesc= if [ -n "$DEST_TYPE" ]; then
MergeDestDesc= [ -n "${!DEST_TYPE}" ] && DestDesc="${!DEST_TYPE}" || DestDesc="<$DEST_TYPE>"
if [ -n "$PMAN_REF_BRANCH" ]; then DestDesc="${COULEUR_ROUGE}$DestDesc${COULEUR_NORMALE}"
BranchDesc="${COULEUR_BLANCHE}<$PMAN_REF_BRANCH>" purpose="${purpose} et sa fusion dans $DestDesc"
[ -n "$PmanRefBranch" -a -n "$PMAN_UNIQUE" ] && BranchDesc="$BranchDesc ($PmanRefBranch)"
BranchDesc="$BranchDesc${COULEUR_NORMALE}"
fi fi
if [ -n "$PMAN_MERGE_SRC" ]; then
MergeSrcDesc="${COULEUR_BLEUE}<$PMAN_MERGE_SRC>"
[ -n "$PmanMergeSrc" -a -n "$PMAN_UNIQUE" ] && MergeSrcDesc="$MergeSrcDesc ($PmanMergeSrc)"
MergeSrcDesc="$MergeSrcDesc${COULEUR_NORMALE}"
fi
if [ -n "$PMAN_MERGE_DEST" ]; then
MergeDestDesc="${COULEUR_ROUGE}<$PMAN_MERGE_DEST>"
[ -n "$PmanMergeDest" -a -n "$PMAN_UNIQUE" ] && MergeDestDesc="$MergeDestDesc ($PmanMergeDest)"
MergeDestDesc="$MergeDestDesc${COULEUR_NORMALE}"
fi
[ -n "$PMAN_UNIQUE" ] &&
purpose="gérer la branche $BranchDesc" ||
purpose="gérer les branches $BranchDesc"
usage="--checkout" usage="--checkout"
variables= variables=
args=( args=(
-d:,--chdir:BASEDIR chdir= "\ -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\ -O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées" origine à partir de laquelle les branches distantes sont considérées"
-B:,--config-branch ConfigBranch= "++\ -B:,--config-branch ConfigBranch= "++\
branche à partir de laquelle charger la configuration" branche à partir de laquelle charger la configuration"
-c:,--config-file:CONFIG ConfigFile= "++\ -c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. le fichier .pman.conf dans le répertoire fichier de configuration des branches. cette option est prioritaire sur --config-branch
du dépôt est utilisé par défaut s'il existe. cette option est prioritaire sur par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
--config-branch"
--fake _Fake=1 "++option non documentée" --fake _Fake=1 "++option non documentée"
--keep-script _KeepScript=1 "++option non documentée" --keep-script _KeepScript=1 "++option non documentée"
--dump action=dump "++afficher les noms des branches" --dump action=dump "++afficher les noms des branches"
)
if [ -n "$PmanRefBranch" -a -n "$PMAN_UNIQUE" ]; then
args+=(
--checkout action=checkout "++\ --checkout action=checkout "++\
créer le cas échéant la branche $BranchDesc et basculer vers elle. basculer sur la branche $SrcDesc. c'est l'action par défaut"
c'est l'option par défaut" )
) if [ -n "$ALLOW_MERGE" ]; then
elif [ -z "$PMAN_UNIQUE" ]; then
args+=(
--checkout action=checkout "\
créer le cas échéant la branche $BranchDesc et basculer vers elle.
c'est l'option par défaut"
)
else
args+=(
--checkout action=checkout "\
créer la branche $MergeDestDesc et basculer vers elle.
c'est l'option par défaut"
)
fi
if [ -n "$PMAN_CAN_MERGE" ]; then
if [ -n "$PMAN_UNIQUE" ]; then
usage="${usage}|--show|--merge" usage="${usage}|--show|--merge"
else
usage="${usage} $PMAN_REF_BRANCH
--show|--merge"
fi
if [ "$PMAN_REF_BRANCH" != "$PMAN_MERGE_SRC" ]; then
usage="$usage
NB: du fait de la configuration des branches, la fusion se fait dans le sens
inverse $MergeSrcDesc --> $MergeDestDesc"
else
usage="$usage
NB: La fusion se fait dans le sens $MergeSrcDesc --> $MergeDestDesc"
fi
variables="\ variables="\
Les variables supplémentaires suivantes peuvent être définies: Les variables supplémentaires suivantes peuvent être définies:
BEFORE_MERGE_${PMAN_MERGE_SRC} BEFORE_MERGE_${SRC_TYPE}
AFTER_MERGE_${PMAN_MERGE_SRC}" AFTER_MERGE_${SRC_TYPE}"
args+=( args+=(
-w,--show '$action=show; inc@ ShowLevel' "\ -w,--show '$action=show; inc@ ShowLevel' "\
lister ce qui serait fusionné dans la branche $MergeDestDesc" lister les modifications qui seraient fusionnées dans la branche $DestDesc"
# -b,--rebase action=rebase "\ -b,--rebase action=rebase "\
#lancer git rebase -i sur la branche $MergeSrcDesc. cela permet de réordonner lancer git rebase -i sur la branche $SrcDesc. cela permet de réordonner les
#les commits pour nettoyer l'historique avant la fusion" commits pour nettoyer l'historique avant la fusion"
-m,--merge action=merge "\ -m,--merge action=merge "++\
fusionner la branche $MergeSrcDesc dans la branche $MergeDestDesc" fusionner la branche $SrcDesc dans la branche $DestDesc"
--tech-merge TechMerge=1 "++option non documentée" --tech-merge TechMerge=1 "++option non documentée"
-s:,--squash:COMMIT_MSG SquashMsg= "\ -s:,--squash:COMMIT_MSG SquashMsg= "\
fusionner les modifications de la branche comme un seul commit" fusionner les modifications de la branche comme un seul commit"
) )
if [ -n "$PMAN_PREL_MERGE" ]; then if [ -n "$MERGE_PREL" ]; then
args+=( args+=(
-f,--force-merge ForceMerge=1 "++\ -f,--force-merge ForceMerge=1 "++\
forcer la fusion pour une branche qui devrait être traitée par prel" forcer la fusion pour une branche qui devrait être traitée par prel"
@ -124,23 +305,22 @@ ne pas pousser les branches vers leur origine après la fusion"
pousser les branches vers leur origine après la fusion. pousser les branches vers leur origine après la fusion.
c'est l'option par défaut" c'est l'option par défaut"
) )
if [ -n "$PMAN_DELETE_MERGED" ]; then if [ -n "$ALLOW_DELETE" ]; then
variables="${variables} variables="${variables}
AFTER_DELETE_${PMAN_MERGE_SRC}" AFTER_DELETE_${SRC_TYPE}}"
args+=( args+=(
-k,--no-delete Delete= "\ -k,--no-delete Delete= "\
ne pas supprimer la branche $MergeSrcDesc après la fusion dans la branche ne pas supprimer la branche $SrcDesc après la fusion dans la branche $DestDesc
$MergeDestDesc. cette option ne devrait pas être utilisée avec --squash" cette option ne devrait pas être utilisée avec --squash"
--delete Delete=1 "++\ --delete Delete=1 "++\
supprimer la branche $MergeSrcDesc après la fusion dans la branche supprimer la branche $SrcDesc après la fusion dans la branche $DestDesc
$MergeDestDesc.
c'est l'option par défaut" c'est l'option par défaut"
) )
fi fi
if [ -n "$PMAN_MERGE_DEST" ]; then if [ -n "$DEST_TYPE" ]; then
variables="${variables} variables="${variables}
BEFORE_PUSH_${PMAN_MERGE_DEST} BEFORE_PUSH_${DEST_TYPE}
AFTER_PUSH_${PMAN_MERGE_DEST}" AFTER_PUSH_${DEST_TYPE}"
fi fi
args+=( args+=(
-a:,--after-merge AfterMerge= "\ -a:,--after-merge AfterMerge= "\
@ -148,6 +328,7 @@ c'est l'option par défaut"
) )
fi fi
chdir= chdir=
Origin= Origin=
ConfigBranch= ConfigBranch=
@ -158,8 +339,8 @@ action=checkout
ShowLevel=0 ShowLevel=0
TechMerge= TechMerge=
SquashMsg= SquashMsg=
Push=1 [ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
Delete=1 [ -z "$PMAN_NO_DELETE" ] && Delete=1 || Delete=
AfterMerge= AfterMerge=
args=( args=(
"$purpose" "$purpose"
@ -178,14 +359,14 @@ if [ -z "$loaded_config" -o -n "$chdir" -o -n "$ConfigFile" -o -n "$ConfigBranch
# charger la configuration # charger la configuration
ensure_gitdir "$chdir" ensure_gitdir "$chdir"
load_branches all load_branches all
load_config load_config "$PMAN_TOOL"
set_pman_vars if [ -n "${!SRC_TYPE}" ]; then
if [ -n "$PmanMergeSrc" ]; then load_branches current "${!SRC_TYPE}"
load_branches current "$PmanMergeSrc"
elif [ -n "$PmanRefBranch" ]; then
load_branches current "$PmanRefBranch"
fi fi
fi fi
resolve_should_push quiet resolve_should_push quiet
[ -n "${!SRC_TYPE}" ] ||
die "Aucune branche définie pour $SRC_TYPE. Veuillez éditer le fichier .pman.conf"
"${action}_action" "$@" "${action}_action" "$@"

191
wip/pinit
View File

@ -1,191 +0,0 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
require: git pman pman.conf
################################################################################
# Informations
################################################################################
SHOW_VARS=(
--Configuration
"${CONFIG_VARS[@]}"
--Paramètres
CurrentBranch
CurrentType=SrcType
)
function show_action() {
local var src
echo_setv ConfigBranch="$ConfigBranch"
echo_setv ConfigFile="$(ppath "$ConfigFile")"
for var in "${SHOW_VARS[@]}"; do
if [ "${var#--}" != "$var" ]; then
estep "${var#--}"
else
splitfsep "$var" = var src
[ -n "$src" ] || src="$var"
echo_setv "$var=${!src}"
fi
done
}
################################################################################
# Initialisation
################################################################################
function _init_config() {
if [ ! -f .pman.conf -o -n "$ForceCreate" ]; then
ac_set_tmpfile config
cp "$ConfigFile" "$config"
"${EDITOR:-nano}" "$config"
[ -s "$config" ] || return 1
cp "$config" .pman.conf
if testdiff .pman.conf "$ConfigFile"; then
ConfigFile="$(pwd)/.pman.conf"
load_config
load_branches current "$SrcBranch"
fi
git add .pman.conf
fi
if [ ! -f ".gitignore" ]; then
echo >.gitignore "\
.~lock*#
.*.swp"
git add .gitignore
fi
return 0
}
function init_repo_action() {
local -a push_branches; local config
[ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé"
_init_config || exit_with ewarn "Initialisation du dépôt annulée"
einfo "Création de la branche $MAIN"
git symbolic-ref HEAD "refs/heads/$MAIN"
git commit -m "commit initial"
push_branches+=("$MAIN")
einfo "Création de la branche $DEVELOP"
git checkout -b "$DEVELOP"
push_branches+=("$DEVELOP")
_push_branches
}
function init_config_action() {
local -a push_branches; local config
[ -f .pman.conf -a -z "$ForceCreate" ] && die "La configuration pman a déjà été initialisée"
resolve_should_push
_init_config || exit_with ewarn "Initialisation de la configuration annulée"
git commit -m "configuration pman"
push_branches+=("$CurrentBranch")
_push_branches
}
function _init_composer() {
if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then
ac_set_tmpfile config
cat >"$config" <<EOF
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
composer:
match_prefix:
match_prefix-dev:
profiles: [ dev, dist ]
dev:
link: true
require:
reqire-dev:
dist:
link: false
require:
reqire-dev:
EOF
"${EDITOR:-nano}" "$config"
[ -s "$config" ] || return 1
cp "$config" .composer.pman.yml
git add .composer.pman.yml
fi
return 0
}
function init_composer_action() {
local -a push_branches; local config
[ -f .composer.pman.yml -a -z "$ForceCreate" ] && die "La configuration pman composer a déjà été initialisée"
resolve_should_push
_init_composer || exit_with ewarn "Initialisation de la configuration annulée"
git commit -m "configuration pman composer"
push_branches+=("$CurrentBranch")
_push_branches
}
function init_action() {
local what="${1:-repo}"; shift
case "$what" in
repo|r) init_repo_action "$@";;
config|c) init_config_action "$@";;
composer|o) init_composer_action "$@";;
*) die "$what: destination non implémentée"
esac
}
################################################################################
# Programme principal
################################################################################
chdir=
ConfigFile=
action=init
Origin=
Push=1
ForceCreate=
args=(
"initialiser un dépôt git"
"repo|config|composer|all"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
-w,--show-config action=show "++\
afficher la configuration chargée"
-O:,--origin Origin= "++\
origine vers laquelle pousser les branches"
-n,--no-push Push= "\
ne pas pousser les branches vers leur origine après leur création"
--push Push=1 "++\
pousser les branches vers leur origine après leur création.
c'est l'option par défaut"
-f,--force-create ForceCreate=1 "\
Avec config, forcer la (re)création du fichier .pman.conf"
)
parse_args "$@"; set -- "${args[@]}"
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config
load_branches current
# puis faire l'action que l'on nous demande
case "$action" in
show) show_action "$@";;
init)
git_ensure_cleancheckout
init_action "$@"
;;
*) die "$action: action non implémentée";;
esac

View File

@ -1 +0,0 @@
_pman.tool

View File

@ -1 +0,0 @@
_pman.tool

60
wip/pwip Executable file
View File

@ -0,0 +1,60 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
require: git pman pman.conf
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de créer une nouvelle branche"
chdir=
Origin=
ConfigBranch=
ConfigFile=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
args=(
"créer une branche de feature"
"<feature>"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées"
-B:,--config-branch ConfigBranch= "++\
branche à partir de laquelle charger la configuration"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
-n,--no-push Push= "\
ne pas pousser les branches vers leur origine après la fusion"
--push Push=1 "++\
pousser les branches vers leur origine après la fusion.
c'est l'option par défaut"
)
parse_args "$@"; set -- "${args[@]}"
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config "$MYNAME"
load_branches current
branch="$1"
if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then
branch="${FeatureBranches[0]}"
fi
[ -n "$branch" ] || die "Vous devez spécifier la branche à créer"
branch="$FEATURE${branch#$FEATURE}"
resolve_should_push
git_ensure_cleancheckout
if array_contains AllBranches "$branch"; then
git checkout -q "$branch"
else
# si la branche source n'existe pas, la créer
args=(--origin "$Origin")
if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile")
elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch")
fi
[ -z "$Push" ] && args+=(--no-push)
exec "$MYDIR/pman" "${args[@]}" "$branch"
fi