468 lines
19 KiB
Bash
Executable File
468 lines
19 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
#----------------------------------------------------------------------------------------------------
|
|
#~ Nom : rpi-backup
|
|
#~ Description : Sauvegarde le contenu d'un Raspberry pi
|
|
#~ Auteur : Etienne Girault <etienne.girault@gmail.com>
|
|
#~
|
|
#~ Usage : [-h] [-D] [-V] [-t] [-q] [-o] [-b] [-v] [-c]
|
|
#~ Usage : [-f [<filename>]
|
|
#~ Usage : [-l "DIR1 DIR2 DIR3"...] or [-l DIR1,DIR2,DIR3...]
|
|
#~
|
|
#~ Options : -h, --help ; Affiche l'aide
|
|
#~ Options : -V, --version ; Affiche la version
|
|
#~ Options : -D, --debug ; Mode debug
|
|
#~ Options : -f, --file=[FILE] ; Détermine le chemin de la sauvegarde
|
|
#~ Options : -t, --test ; Mode test
|
|
#~ Options : -q, --quiet ; Mode silencieux
|
|
#~ Options : -l, --list ; Spécifie les répertoires à sauvegarder
|
|
#~ Options : -p, --print ; Affiche la taille et le chemin de la dernière sauvegarde
|
|
#~ Options : -b, --background ; Lancement du script en arrière-plan
|
|
#~ Options : -v, --view ; Affiche les derniers logs
|
|
#~ Options : -c, --configure ; Configure les variables du script
|
|
|
|
#----------------------------------------------------------------------------------------------------
|
|
# Déclaration des variables
|
|
#----------------------------------------------------------------------------------------------------
|
|
script_name="$(grep '^#~ Nom' "$0" | sed 's/^.*: //')"
|
|
script_version="$(grep '^#~ v' "$0" | tail -1 | sed -e 's/^#~ //' -e 's/ *\-.*$//')"
|
|
|
|
backup_user=backup
|
|
host_name=$(hostname -s)
|
|
|
|
# Répertoire où la sauvegarde sera déposée
|
|
backup_dir=/backup
|
|
# Nom et chemin de la sauvegarde
|
|
backup_name=backup_${host_name}_$(date +"%Y%m%d")
|
|
backup_path="${backup_dir}/${backup_name}"
|
|
|
|
# Répertoire où les listes des exports sera déposée
|
|
export_dir=${backup_dir}/export
|
|
# Localisation des fichiers config des repo git (dans un tableau pour prendre en compte '*')
|
|
git_configs=(/appli/source/*/.git/config)
|
|
|
|
# Répertoires depuis la racine à sauvegarder
|
|
saved_dir="appli/data backup/export boot etc home root seedbox srv"
|
|
|
|
# Répertoire d'archive
|
|
archive_dir=/data/backup
|
|
# Répertoire des anciennes archives
|
|
old_archive_dir=${archive_dir}/old
|
|
|
|
# Fichier de log
|
|
log_file=/var/log/${script_name}.log
|
|
|
|
# Variables configurables
|
|
configurable_vars="backup_user backup_dir export_dir archive_dir old_archive_dir saved_dir git_configs log_file"
|
|
|
|
#----------------------------------------------------------------------------------------------------
|
|
# Déclaraition des fonctions
|
|
#----------------------------------------------------------------------------------------------------
|
|
print_script_name_version() { echo "$script_name $script_version" && sed -n '/^#~ Version/,$p' "$0" | grep '^#~' | sed 's/^#~ //'; }
|
|
print_script_description() { grep '^#~ Description' "$0" | sed 's/^.*: //'; }
|
|
print_script_usage() { grep '^#~ Usage' "$0" | sed -e 's/^.*: //' -e '1 s/^/Usage: '"$script_name"' /' -e '2,$ s/^/'"$(printf '%*s' "$(echo "Usage: $script_name" | wc -c)")"'/'; }
|
|
print_script_options() { echo "Options" && grep '^#~ Options' "$0" | sed -e 's/^.*://' -e 's/^ --/ --/' | column -ts \;; }
|
|
print_script_help() { echo "$script_name $script_version" && print_script_description && print_script_usage && print_script_options; }
|
|
|
|
# Séquence de test avant le lancement du script
|
|
prelaunch_test() {
|
|
# Relance le script avec droits sudo si pas deja fait
|
|
[[ "$(whoami)" = root ]] || { sudo "$0" "$@"; exit; }
|
|
|
|
# Détection du mode interactif ou silencieux
|
|
[[ ! "$mode_quiet" && -t 0 && -t 1 ]] && mode_interactive=0
|
|
|
|
# Décalage des arguments en fonction du nombre d'options choisies
|
|
shift $((OPTIND-1))
|
|
# Si un argument reste en fin de chaîne, c'est qu'il y a un problème sur le nombre d'argument
|
|
[[ "$1" ]] && argument_error="$1"
|
|
# Si une erreur d'argument a été trouvée, l'affiche et sort du script
|
|
if [[ "$argument_error" ]]; then
|
|
msg_log 3 "Argument incompris: $argument_error"
|
|
exit 1
|
|
fi
|
|
|
|
# Test si l'utilisateur de backup existe, active le mode standalone sinon
|
|
if ! id "$backup_user" &>/dev/null; then
|
|
msg_log 3 "utilisateur $backup_user absent sur le système"
|
|
mode_standalone=0
|
|
fi
|
|
|
|
# Vérifie si un chemin de sauvegarde personnalisé a été demandé
|
|
custom_backup_path 1
|
|
|
|
# Test la présence du répertoire de sauvegarde (création si besoin)
|
|
test_directory "$backup_dir" 0
|
|
# Test la présence du répertoire d'export (création si besoin)
|
|
test_directory "$export_dir" 0
|
|
# Test la présence du répertoire d'archive, active le mode standalone sinon
|
|
test_directory "$archive_dir" || mode_standalone=0
|
|
test_directory "$old_archive_dir" 0
|
|
|
|
# Test de la présence des répertoires à sauvegarder, les supprime de la liste si absent
|
|
for directory in $saved_dir; do
|
|
test_directory "/${directory}" || saved_dir="${saved_dir/$directory/}"
|
|
done
|
|
|
|
# Si le dossier de log n'existe pas, active le flag no_logging
|
|
test_directory "$(dirname $log_file)" || no_logging=0
|
|
|
|
# Test des commandes d'export, affiche son absence si c'est le cas
|
|
test_command dpkg || msg_log 3 "l'outil dpkg est introuvable"
|
|
test_command pip2 || msg_log 3 "l'outil pip2 est introuvable"
|
|
test_command pip3 || msg_log 3 "l'outil pip3 est introuvable"
|
|
|
|
# Mode test pour dev
|
|
[[ "$mode_test" ]] && fonction_test "$@"
|
|
return 0
|
|
}
|
|
|
|
# Test la présence d'une commande, retourne 1 si elle n'existe pas
|
|
# $1 : Commande testée
|
|
test_command() { type "$1" &>/dev/null && return 0 || return 1; }
|
|
|
|
# Test la présence d'un réperoire, la possiblité de le créer, retourne 1 si pas présent/créé
|
|
# $1 : Répertoire testé
|
|
# [$2] : Si initialisée, le répertoire sera créé si possible
|
|
test_directory() {
|
|
# si le répertoire n'existe pas
|
|
[[ -d "$1" ]] && return
|
|
# si $2 est initialisée et si le répertoire parent existe
|
|
if [[ "$2" && -d "$(dirname $1)" ]]; then
|
|
# demande de creation du répertoire
|
|
if ask_question "Répertoire $1 absent, dois-je le créer ?"; then
|
|
msg_log 1 "Création du répertoire $1"
|
|
msg_log 2 "mkdir $1"
|
|
return 0
|
|
# si l'utilisateur répond non, retourne 1
|
|
else return 1; fi
|
|
# sinon, $2 n'est pas initialisée ou le répertoire parent n'existe pas, retourne 1
|
|
else return 1; fi
|
|
}
|
|
|
|
# Pose une question à l'utilisateur, si pas intéractif, la réponse est oui par défaut
|
|
# $1 : Question posée
|
|
ask_question() {
|
|
[[ ! "$mode_interactive" ]] && return 0
|
|
echo -e "$1 \c"
|
|
read -r -n 1 question_answer < /dev/tty
|
|
echo ""
|
|
case $question_answer in
|
|
o|O|y|Y) return 0;;
|
|
*) return 1;;
|
|
esac
|
|
}
|
|
|
|
# Affiche les étapes du script sur la sortie standard en mode interactif ou le fichier de log en mode silencieux
|
|
# $1 : Mode (0=message, 1=début de tâche, 2=fin de tache, 3=message sur le stderr, 4=debut/fin de script)
|
|
# $2 : Message à afficher ou code retour de la précédente commande
|
|
msg_log() {
|
|
# Si mode interactif, affichage des messages sur la console
|
|
|
|
if [[ "$mode_interactive" ]]; then
|
|
case $1 in
|
|
0) echo "$2";;
|
|
1) echo -e "${2} \c";;
|
|
2) if eval "$2" &> /dev/null; then
|
|
echo -e "\033[32;1mOK\033[0m"
|
|
else
|
|
echo -e "\033[31;1mErreur\033[0m"
|
|
fi;;
|
|
3) echo "$2" >&2;;
|
|
esac
|
|
# Sinon, mode non interactif, affichage des messages sur le fichier de log
|
|
else
|
|
[[ "$no_logging" ]] && return 0
|
|
format_date="$(date "+%h %d %T") ${host_name}:"
|
|
case $1 in
|
|
0|3) echo "$format_date $2" >> "$log_file";;
|
|
1) echo -e "$format_date ${2} \c" >> "$log_file";;
|
|
2) if eval "$2" &> /dev/null; then
|
|
echo -e "\033[32;1mOK\033[0m" >> "$log_file"
|
|
else
|
|
echo -e "\033[31;1mErreur\033[0m" >> "$log_file"
|
|
fi;;
|
|
4) echo "$format_date --- $2" >> "$log_file";;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
# Export des listes de paquets/librairies/repos installés dans le dossier d'export
|
|
export_lists() {
|
|
msg_log 1 "Export des listes de paquets/librairies/repos"
|
|
# Export de la liste des paquets apt
|
|
test_command dpkg && dpkg --get-selections > "${export_dir}/apt.list"
|
|
# Export de la liste des librairies python2
|
|
test_command pip2 && pip2 freeze > "${export_dir}/pip2.list"
|
|
# Export de la liste des librairies python3
|
|
test_command pip3 && pip3 freeze > "${export_dir}/pip3.list"
|
|
# Extraction des URLs des repos github
|
|
ls ${git_configs[*]} >/dev/null 2>&1 && sed -n '/url/p' ${git_configs[*]} | sed 's/^.*= //' > "${export_dir}/appli.list"
|
|
msg_log 2
|
|
return 0
|
|
}
|
|
|
|
# Création de la sauvegarde
|
|
create_backup() {
|
|
# Suppression d'éventuel reliquat d'archive
|
|
[[ -f "$backup_path".tar ]] && rm -f "$backup_path".tar
|
|
# Création du fichier de sauvegarde et application du user de backup
|
|
touch "$backup_path".tar
|
|
[[ ! "$mode_standalone" ]] && chown "${backup_user}:${backup_user}" "$backup_path".tar
|
|
|
|
# Sauvegarde de la liste des répertoires dans backup_name.tar dans le backup_dir
|
|
cd / || exit
|
|
for directory in $saved_dir; do
|
|
msg_log 1 "Sauvegarde du dossier /$directory"
|
|
msg_log 2 'tar --append --warning=no-file-ignored --file "$backup_path".tar "$directory"'
|
|
done
|
|
|
|
msg_log 1 "Compression de la sauvegarde"
|
|
# Compression de l'archive tar en tar.gz
|
|
[[ -f "$backup_path".tar ]] && msg_log 2 'gzip -1 "$backup_path".tar'
|
|
[[ -f "$backup_path".tar.gz ]] && mv "$backup_path".t{ar.gz,gz}
|
|
|
|
return 0
|
|
}
|
|
|
|
# Supprime les précédentes sauvegardes du dossier de sauvegarde
|
|
clear_backup() {
|
|
[[ "$mode_standalone" ]] && return
|
|
msg_log 1 "Suppression des précédentes sauvegardes du dossier $backup_dir"
|
|
msg_log 2 'find "$backup_dir" -maxdepth 1 ! -name "$backup_name".tgz -user "$backup_user" -print0 | xargs -r0 rm'
|
|
return 0
|
|
}
|
|
|
|
|
|
# Rotations des sauvegardes réalisées par le user backup
|
|
rotate_backup() {
|
|
# Si le répertoire d'archive est injoignable, retour
|
|
[[ "$mode_standalone" ]] && return
|
|
msg_log 1 "Rotations des sauvegardes dans les archives $archive_dir"
|
|
for day in 01 10 20; do
|
|
# Déplace les sauvegardes de ce serveur datant du 01, 10 et 20 du mois, qui ont plus de 3 jours, du dossier archives vers les anciennes archives
|
|
su "$backup_user" -s /bin/bash -c "find $archive_dir -maxdepth 1 -name *${host_name}*${day}.tgz -type f -mtime +1 -print0 | xargs -r0 mv -t $old_archive_dir"
|
|
# Supprime les sauvegardes de ce serveur qui ont plus de 30 jours du dossier des anciennes archives
|
|
[[ ! "$day" = 01 ]] && su "$backup_user" -s /bin/bash -c "find $old_archive_dir -maxdepth 1 -name *${host_name}*${day}.tgz -mtime +30 -type f -print0 | xargs -r0 rm"
|
|
done
|
|
# Supprime les sauvegardes de ce serveur de plus de 5 jours du dossier d'archive
|
|
msg_log 2 'su "$backup_user" -s /bin/bash -c "find $archive_dir -maxdepth 1 -name *${host_name}* -type f -mtime +1 -print0 | xargs -r0 rm"'
|
|
|
|
msg_log 1 "Copie la sauvegarde $backup_name dans le dossier d'archive $archive_dir"
|
|
# Copie la sauvegarde dans le dossier d'archive
|
|
msg_log 2 'su "$backup_user" -s /bin/bash -c "cp $backup_path.tgz $archive_dir"'
|
|
return 0
|
|
}
|
|
|
|
test_nextarg() {
|
|
[[ "${1::1}" = "-" ]] && argument_error="$1"
|
|
return 0
|
|
}
|
|
|
|
# Spécifie au script un chemin de sauvegarde personnalisé
|
|
# $1 : Mode (0=Initialise la variable backup_path avec $2, 1=Vérifie l'état)
|
|
# $2 : valeur du backup_path demandé
|
|
custom_backup_path() {
|
|
# Si la fonction est appelé avec un argument
|
|
case $1 in
|
|
0) # $1 = 0, le backup_path est associé à $2
|
|
backup_path="$2"
|
|
# Le drapeau custom_backup_path_set est levé
|
|
custom_backup_path_set=0;;
|
|
1) # $1 = 1, on vérifie l'état des drapeaux, si custom_backup_path_flag pas levé, on retourne 0
|
|
[[ ! "$custom_backup_path_flag" ]] && return 0
|
|
# Si mode interactif
|
|
if [[ "$mode_interactive" ]]; then
|
|
# Si custom_backup_path_set n'est pas levé, on demande a l'utilisateur la valeur de backup_path
|
|
if [[ ! "$custom_backup_path_set" ]]; then
|
|
echo -e "Chemin de la sauvegarde ? \c"
|
|
read -r backup_path < /dev/tty;
|
|
fi
|
|
# Si mode interactif
|
|
# Tant que le répertoire de la valeur de backup_path n'existe pas, on redemande la valeur de backup_set à l'utilisateur
|
|
until test_directory "$(dirname $backup_path)"; do
|
|
echo -e "Chemin de sauvegarde invalide, réessaye :\c"
|
|
read -r backup_path < /dev/tty;
|
|
done
|
|
# Sinon, mode silencieux
|
|
else
|
|
# Si le drapeau custom_backup_path_set est levé
|
|
if [[ "$custom_backup_path_set" ]]; then
|
|
# Si le répertoire de la valeur de backup_path n'existe pas, affiche l'erreur sur les logs et quitte le script
|
|
test_directory "$(dirname $backup_path)" || { msg_log 3 "Le chemin de la sauvegarde $backup_path n'est pas valable"; exit 1;}
|
|
# Sinon, le drapeau custom_backup_path_set n'est pas levé, incompatible en mode silencieux, affiche l'erreur sur les logs et quitte le script
|
|
else
|
|
msg_log 3 "Le chemin de la sauvegarde $backup_path n'est pas spécifié"
|
|
exit 1;
|
|
fi
|
|
fi
|
|
# Si le backup_path est donné en relatif, transformation en chemin absolu
|
|
[[ ! "${backup_path::1}" = "/" ]] && backup_path="$(pwd)/$backup_path"
|
|
return 0;;
|
|
esac
|
|
# Le drapeau custom_backup_path_path est levé
|
|
custom_backup_path_flag=0
|
|
mode_standalone=0
|
|
return 0
|
|
}
|
|
|
|
# Sauvegarde les bases MySQL/MariaDB si le service est lancé
|
|
save_mysql() {
|
|
# Test si le service MySQL est démarré, retourne 0 sinon
|
|
systemctl is-active mysql --quiet || return 0
|
|
msg_log 1 "Sauvegarde des bases MySQL"
|
|
# Dump des bases de données dans le répertoire d'export
|
|
msg_log 2 'mysqldump --all-databases > "${export_dir}/mysql-dump.sql"'
|
|
return 0
|
|
}
|
|
|
|
# Affiche la taille et le nom de la dernière sauvegarde
|
|
print_backup_name() { ls -lth "$backup_dir"/*.tgz | head -1 | awk '{printf "%-6s %s\n", $5, $9}'; }
|
|
|
|
# Lance un éditeur de texte avec les variables configurables
|
|
configure_variable() {
|
|
mode_interactive=0
|
|
# Crée un fichier de configuration temporaire
|
|
conf_file=$(mktemp)
|
|
# Affiche chaque variable dans le fichier de configuration
|
|
for each_variable in $configurable_vars; do
|
|
sed -n "/^${each_variable}=/p" "$0" >> "$conf_file"
|
|
done
|
|
# Edite le fichier de configuration
|
|
vi "$conf_file"
|
|
# Changement de l'IFS pour la lecture du fichier de configuration
|
|
IFS='='
|
|
# Pour chaque ligne du fichier de configuration
|
|
while read -r the_variable new_value; do
|
|
# Vérifie si la variable écrite sur le fichier est modifiable
|
|
if echo "$configurable_vars" | grep -q "$the_variable"; then
|
|
# Si la valeur de la variable a été modifiée
|
|
if ! grep -q -F "$the_variable=$new_value" "$0"; then
|
|
# Demande confirmation à l'utilisateur pour modifier la variable
|
|
if ask_question "Modifier la variable '$the_variable=$new_value' ?"; then
|
|
# script_path=$(readlink -f "$0")
|
|
msg_log 1 "Modification de la variable '$the_variable'"
|
|
# Modifie la valeur de la variable directement dans le script
|
|
msg_log 2 'sed -i "s#^$the_variable=.*#$the_variable=$new_value#" "$(readlink -f $0)"'
|
|
fi
|
|
fi
|
|
else
|
|
msg_log 0 "Impossible de changer la variable '$the_variable'"
|
|
fi
|
|
done < "$conf_file"
|
|
# Supprime le fichier de configuration temporaire
|
|
rm "$conf_file"
|
|
}
|
|
|
|
# Affiche les derniers logs
|
|
view_log() {
|
|
# Cherche le motif '--- Début', le stock quand il le trouve et ajoute au stock, si
|
|
sed -n '/--- Début/h;/--- Début/!H;$!b;x;p' "$log_file"
|
|
}
|
|
|
|
# Fonction de test pour le developpement
|
|
fonction_test() {
|
|
|
|
sed -n '/url/p' ${git_configs[*]} | sed 's/^.*= //'
|
|
msg_log 0 "test ok"
|
|
exit 0
|
|
}
|
|
|
|
#----------------------------------------------------------------------------------------------------
|
|
# Début du script
|
|
#----------------------------------------------------------------------------------------------------
|
|
|
|
main() {
|
|
prelaunch_test "$@"
|
|
|
|
msg_log 4 "Début de la sauvegarde"
|
|
|
|
export_lists
|
|
save_mysql
|
|
create_backup
|
|
clear_backup
|
|
rotate_backup
|
|
|
|
msg_log 4 "Fin de la sauvegarde"
|
|
|
|
exit 0
|
|
}
|
|
|
|
#----------------------------------------------------------------------------------------------------
|
|
# Gestion des options
|
|
#----------------------------------------------------------------------------------------------------
|
|
while getopts ":-:hVDf:tql:pbvc" OPTION; do
|
|
case $OPTION in
|
|
h) print_script_help; exit;;
|
|
V) print_script_name_version; exit;;
|
|
D) set -x;;
|
|
f) test_nextarg "$OPTARG" && custom_backup_path 0 "$OPTARG";;
|
|
t) mode_test=0;;
|
|
q) mode_quiet=0;;
|
|
l) test_nextarg "$OPTARG" && saved_dir="${OPTARG//,/ }"; custom_backup_path;;
|
|
p) print_backup_name; exit;;
|
|
b) nohup "$script_name" "-q" "${@/-b/}" &> /dev/null & exit;;
|
|
v) view_log; exit;;
|
|
c) configure_variable; exit;;
|
|
:) [ "$OPTARG" = "f" ] && custom_backup_path;;
|
|
\?) echo "-$OPTARG: option invalide" && print_script_usage; exit;;
|
|
-) case $OPTARG in
|
|
help) print_script_help; exit;;
|
|
version) print_script_name_version; exit;;
|
|
debug) set -x;;
|
|
file) custom_backup_path 0 "$OPTARG";; # VERIF
|
|
test) mode_test=0;;
|
|
quiet) mode_quiet=0;;
|
|
list) saved_dir="${OPTARG//,/ }"; custom_backup_path;;
|
|
print) print_backup_name; exit;;
|
|
background) nohup "$script_name" "-q" "${@/-b/}" &> /dev/null & exit;;
|
|
view) view_log; exit;;
|
|
configure) configure_variable; exit;;
|
|
*) echo "--$OPTARG: option invalide" && print_script_usage; exit;;
|
|
esac ;;
|
|
esac
|
|
done
|
|
|
|
# Reception de signaux d'interruptions
|
|
trap 'exit 1' INT TERM
|
|
|
|
set -o errexit
|
|
|
|
main "$@"
|
|
|
|
#----------------------------------------------------------------------------------------------------
|
|
# Fin du script
|
|
#----------------------------------------------------------------------------------------------------
|
|
|
|
#~ Version changelog
|
|
#~ v0.1 - Création du script
|
|
#~ v0.2 - Sauvegarde totale de la carte SD (~25min)
|
|
#~ v0.2.1 - Sauvegarde uniquement les répertoires utiles à la restauration (~2min)
|
|
#~ v0.2.2 - Nommage de la sauvegarde en fonction de la date
|
|
#~ v0.2.3 - Ajout des exports des listes de paquets/librairies/URLs github
|
|
#~ v0.2.4 - Ajout de l'option file pour définir un chemin à la sauvegarde
|
|
#~ v0.3 - Réécriture du script suivant les best practice bash
|
|
#~ v0.4 - Ajout des fonctions test_command et test_directory
|
|
#~ v0.4.1 - Ajout de la fonction ask_question
|
|
#~ v0.4.2 - Ajout du mode verbeux et interactif
|
|
#~ v0.4.3 - Ajout du mode standalone et la creation des répertoires nécessaires au script
|
|
#~ v0.5 - Utilisable multi serveur
|
|
#~ v0.5.1 - Mode verbeux par défaut en interactif, ajout du mode silencieux
|
|
#~ v0.5.2 - Refonte de la fonction msg_log
|
|
#~ v0.5.3 - Ajout du logging en mode non interactif
|
|
#~ v0.6 - Ajout de l'option list pour spécifier les répertoires à sauvegarder, à associer à l'option file
|
|
#~ v0.6.1 - Ajout de la fonction custom_backup_path pour gérer les erreurs d'un backup_path personnalisé
|
|
#~ v0.6.2 - Ajout de la fonction test_nextarg pour gérer les problèmes d'argument
|
|
#~ v1.0 - Publication pour mise en fonctionnement
|
|
#~ v1.1 - Ajout de la sauvegarde des bases MySQL/MariaDB
|
|
#~ v1.1.1 - Modification de la fonction msg_log pour la prise en compte du code retour
|
|
#~ v1.2 - Ajout de l'option print pour l'affichage de la taille et du nom de la dernière sauvegarde
|
|
#~ v1.3 - Ajout de l'option background pour le lancement en arriere plan
|
|
#~ v1.4 - Ajout de l'option view pour le l'affichage des derniers logs
|
|
#~ v1.5 - Ajout de l'option configure pour la reconfiguration des variables
|
|
#~ v1.5.1 - Améliorations de synthaxe
|
|
#~ v1.5.2 - Correction de l'utilisation de la variable git_config
|
|
|
|
|
|
# TODO - Restauration
|