Utiliser un VCS pour gérer des fichiers (système)

etckeeper, c'est joli mais ça ne fonctionne que pour /etc sauf à créer plusieurs dépôts par système. De plus, toutes les solutions connues recourent à (au moins) un répertoire .foo.

On va donc :

  • créer un utilisateur vcs (groupe vcs) avec pour répertoire /var/vcs
  • dans /var/vcs, une copie des fichiers tels qu'ils existent
  • /var/vcs est un dépôt (ici Git).

Pour initialiser, utiliser le script suivant :

#!/bin/ksh
#
# $Id: 0737ef5ddc4cddf4ab801276207f5140a4b8e2c5 $
#
PATH=/bin:/sbin:/usr/sbin:/usr/bin:/usr/pkg/bin:/usr/pkg/sbin:/usr/local/scripts:/usr/local/bin
export PATH
 
addr="netadmin@local.net"
repo="/var/vcs"
users_home="/root /users/pc"
log="/var/log/$(basename $0).log"
 
groupadd -g 85 vcs
useradd -g vcs -d /var/vcs -m -c "VCS user" -M 0700 -u 85 -s /bin/ksh vcs
 
rm -f ~vcs/.profile && touch ~vcs/.profile
echo "# $0 $(date)" >> ~vcs/.profile
echo "export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/pkg/bin:/usr/pkg/sbin:\$HOME/bin" >> ~vcs/.profile
echo "export EDITOR=vi" >> ~vcs/.profile
echo "export PAGER=less" >> ~vcs/.profile
echo "umask 0027" >> ~vcs/.profile
 
touch $log
chmod 0600 $log
 
ls -A > .gitignore
echo ".lesshst" >> .gitignore
echo "*.sw[a-z]" >> .gitignore
echo "*,v" >> .gitignore
 
cd $repo || exit 1
git init
 
printf "\twhitespace = -trailing-space,-space-before-tab\n" >> $repo/.git/config
printf "[apply]\n" >> $repo/.git/config
printf "\twhitespace = fix\n" >> $repo/.git/config
 
git config user.name "VCS User"
git config user.email "vcs@$(hostname).local.net"
git config core.editor $(which vi)
git config core.sharedRepository "umask"
 
git add .gitignore
$hostname > $repo/.git/description
 
umask 0027
 
echo "XXX --------- $(pwd)"
cd $repo
cp /boot.cfg .
cp -r /etc .
mkdir -p usr/pkg && cp -r /usr/pkg/etc usr/pkg
git add .
git commit -a -m 'system files in /etc & /usr/pkg/etc added'
 
chown -R vcs:vcs $repo
 
echo "==> repo:host-$(hostname).git"

Ensuite, on versionne les fichiers après chaque modification avec :

#!/usr/pkg/bin/perl
#
# $Id: 780073bd6f1bccc789a01bf3bd466f02bdcbc7cf $
 
use Cwd;
use Getopt::Std;
use File::Basename;
use File::Copy;
use File::Path;
use File::Spec;
use File::Temp;
use Text::Wrap;
 
$ENV{'PATH'} = "/bin:/usr/bin:/usr/pkg/bin:/usr/local/scripts:/usr/local/bin";
 
$ENV{PAGER} = "less" if ( not defined $ENV{PAGER} );
 
my $fqdn = "local.net";
my $repo = "/var/vcs";
my %args;
 
sub usage {
    print
"$0: [ -t #1234 ][ -c \"bla bla\" ] | [ -l ] [ -D ] [ -d ] file1 [ file2 ... ]\n";
    print
      "\t\t-t #1234: numero de ticket, ajoute au commentaire lors du commit\n";
    print
      "\t\t-c \"bla bla\": commentaire, ajoute au commentaire lors du commit\n";
    print "\t\t-l: TODO liste les revisions du fichier\n";
    print "\t\t-D: do diff(1)\n";
    print "\t\t-d: debug messages\n";
}
 
#
# -------------------- affiche un diff(1) --------------------
#
sub do_diff {
    my @files = @_;
    my $error = 0;
 
    foreach my $f (@files) {
        my ( $real_dir, $fn ) = locate_file($f);
        if (   ( not -f "$repo/$real_dir/$fn" )
            || ( not -f "/$real_dir/$fn" ) )
        {
            warn "XXX fichier '$repo/$real_dir/$fn' inexistant $!\n";
            $error++;
        }
        else {
            printf "--------------- /$real_dir/$fn\n";
            system
"diff -E -w -B -N -s -u $repo/$real_dir/$fn /$real_dir/$fn | $ENV{PAGER}";
        }
    }
    return $error;
}
 
#
# -------------------- le fichier reel --------------------
#
sub locate_file {
    my $f = shift;
 
    my $dir = dirname $f;
    my $fn  = basename $f;
 
    my $real_dir = Cwd::realpath($dir);
    $real_dir =~ s/^\/*//;
    return ( $real_dir, $fn );
}
 
getopts( "t:c:dleD", \%args );
warn "XXX ------------ DEBUG VERSION\n" if ( $args{d} );
 
#
# -------------------- verification des arguments --------------------
#
if ( not $args{l} ) {
    if (   ( not defined $args{D} )
        && ( length $args{t} == 0 )
        && ( length $args{c} == 0 ) )
    {
        warn "XXX A minima indiquer un commentaire (-c \"bla bla bla\")"
          . "ou un ticket (-t 1234) ou faire un diff(1)\n";
    }
}
 
#
# -------------------- generation du commentaire --------------------
#
my $comment = "";
$comment .= "[tkt #$args{t}]" if ( defined $args{t} );
$comment .= " " if ( ( defined $args{c} ) && ( defined $args{t} ) );
$comment .= "$args{c}" if ( defined $args{c} );
 
my $log = File::Temp->new( UNLINK => 0 )
  or die "Can't create temporary file: $!\n";
my $commit_log = $log->filename();
 
if ( defined $args{e} ) {
    my $tmp = File::Temp->new()
      or die "Can't create temporary file: $!\n";
    my $editor = $ENV{'EDITOR'};
    $editor = "vi" if $editor eq qq{};
 
    my $commit_tmp = $tmp->filename();
 
    print $tmp $comment;
    print $tmp "\n\n";
    system("$editor $commit_tmp");
 
    if ( defined $args{d} ) {
        print "XXX fichier a la fin :\n'";
        print system("cat $commit_tmp");
        print "'\n";
        getc STDIN;
    }
 
    # XXX juste pour eviter textproc/p5-Text-Unaccent
    seek $commit_tmp, 0, 0;
    my @cmt;
    while (<$tmp>) {
        s/[<E0><E4><E2>]/a/g;
        s/[<E9><E8><EB><EA>]/e/g;
        s/[<EF><EE><EC>]/i/g;
        s/[<F4><F2>]/o/g;
        s/[<F9><FB>]/u/g;
        s/<E7>/c/g;
        push @cmt, $_;
    }
    close $tmp;
 
    if ( defined $args{d} ) {
        print "XXX commentaire a la fin :\n'@cmt'\n";
        getc STDIN;
    }
 
    $Text::Wrap::columns = 72;
    print $log wrap( '', '', @cmt );
 
    if ( defined $args{d} ) {
        system("cat $commit_log");
        getc STDIN;
    }
}
 
#
# -------------------- l'utilisateur... tout sauf root --------------------
#
my $user = "";
if ( ( defined $ENV{'USER'} ) && ( $ENV{'USER'} ne "root" ) ) {
    $user .= "$ENV{'USER'}";
}
elsif ( ( defined $ENV{'SUDO_USER'} ) && ( $ENV{'SUDO_USER'} ne "root" ) ) {
    $user .= "$ENV{SUDO_USER}";
}
elsif ( ( defined $ENV{'SU_FROM'} ) && ( $ENV{'SU_FROM'} ne "root" ) ) {
    $user .= "$ENV{'SU_FROM'}";
}
elsif ( ( defined $ENV{'LOGNAME'} ) && ( $ENV{'LOGNAME'} ne "root" ) ) {
    $user .= "$ENV{'LOGNAME'}";
}
else {
    $user = getlogin();
}
my @u = getpwnam($user);
$ENV{GIT_AUTHOR_NAME}  = "$u[6]";
$ENV{GIT_AUTHOR_EMAIL} = "$u[0]\@$fqdn";
#
# -------------------- traitement des fichiers --------------------
#
my @files = @ARGV;
if ( $#files < 0 ) {
    warn "XXX aucun fichier passe en argument\n";
    usage();
    exit 1;
}
else {
    my $all_files = "";
 
    # --------------- diff(1)
    if ( length $args{D} > 0 ) {
        my $error = do_diff(@files);
 
        # -D est incompatible avec -c / -t et -l
        $error == 0 ? exit 0 : exit 1;
    }
 
    # --------------- log
    if ( $args{l} ) {
        my $error = 0;
        foreach my $f (@files) {
            my ( $real_dir, $fn ) = locate_file($f);
 
            if (   ( not -f "$repo/$real_dir/$fn" )
                || ( not -f "/$real_dir/$fn" ) )
            {
                warn "XXX fichier '$repo/$real_dir/$fn' inexistant $!\n";
                $error++;
            }
            else {
                if ( $args{d} ) {
                    print "cd $repo ; git log $real_dir/$fn\n";
                }
 
                system "cd $repo ; git log $real_dir/$fn";
            }
        }
 
        # -l est incompatible avec -c et/ou -t
        $error == 0 ? exit 0 : exit 1;
    }
 
    foreach my $f (@files) {
        my ( $real_dir, $fn ) = locate_file($f);
        if ( not -f "/$real_dir/$fn" ) {
            warn "XXX fichier '$f' inexistant $!\n";
            exit 1;
        }
        else {
            mkpath "$repo/$real_dir" if ( not -d "$repo/$real_dir" );
 
            copy( "/$real_dir/$fn", "$repo/$real_dir/$fn" )
              or die "XXX echec de la copie de '$f': $!";
 
            # --------------- liste des fichiers a commiter
            $all_files .= " $real_dir/$fn";
 
            # --------------- git add
            $args{d}
              ? print "'git add $real_dir/$fn'\n"
              : system "cd $repo && git add $real_dir/$fn";
        }
    }
 
    do_diff(@files);
 
    # --------------- commit
    print "\n---- commit ----\n";
    if ( not defined $args{e} ) {
        system "cd $repo && git commit -m \"$comment\" $all_files";
    }
    else {
        system "cd $repo && git commit --file=$commit_log $all_files";
        close $log;
    }
}

À ce stade :

  • pour voir les modifications d'un fichier :
    $ sudo version -d /path/to/file
  • pour voir l'historique d'un fichier :
    $ sudo version -l /path/to/file
  • pour commiter les modifications d'un fichier :
    $ sudo version -t 4321 -c "my comment"  /path/to/file

    le commentaire du commit se résumera à : [tkt #4321] my comment, utile si on utilise un système de tickets

  • pour le reste, il faut passer root et utiliser git(1) as usual.

On peut améliorer encore et tous les jours lancer un ramasse-miettes :

#!/bin/ksh
#
# $Id: 463db40a0dda25d45f6a0254e9615c2b495e5988 $
 
export PATH=/bin:/usr/bin:/usr/pkg/bin:/usr/local/bin
 
repo="/var/vcs"
 
chmod 0700 $repo
cd $repo
 
printf "\ndaily import in $repo :\n"
printf "==========================\n"
# wrokgin file vs vcs
for f in $(git ls-files); do
        if [[ -f "/$f" ]]; then
                diff $f "/$f" 2>&1 >/dev/null
                if [[ $? -ne 0 ]]; then
                        cp "/$f" $f
                fi
        else
                echo "/$f vanished => git rm $f"
                git rm $f
        fi
done
 
# files in dirs
for dir in $(for f in $(git ls-files); do echo $(dirname $f); done | sort | uniq); do
        for file in $(ls /$dir); do
                if [[ -f /$dir/$file && ! -f $repo/$dir/$file ]]; then
                        cp /$dir/$file $repo/$dir/$file
                        git add $repo/$dir/$file
                fi
        done
done
echo "--------- status :"
git status
echo "--------- diff :"
git diff
echo "--------- commit :"
git commit -a -m "daily commit $(date '+%Y-%m-%d')"
git gc
git fsck --strict
 
printf "Repository $repo size :"
du -sh $repo
  • blog/version.txt
  • Dernière modification : 2013/03/17 16:58
  • de pc