3 # vv : visual versioning
4 # zz : generic shell clip board
5 # ezz : clip board editor
7 # http://fex.rus.uni-stuttgart.de/fstools/vv.html
8 # http://fex.rus.uni-stuttgart.de/fstools/zz.html
10 # by Ulli Horlacher <framstag@rus.uni-stuttgart.de>
12 # Perl Artistic Licence
14 # vv is a script to handle file versions:
15 # list, view, recover, diff, purge, migrate, save, delete
17 # vv is an extension to emacs idea of backup~ files
19 # File versions are stored in local subdirectory .versions/
21 # To use vv with jed, install to your jed library path:
23 # http://fex.rus.uni-stuttgart.de/sw/share/jedlib/vv.sl
25 # To use vv with vim, add to your .vimrc:
27 # autocmd BufWritePre * execute '! vv -s ' . shellescape(@%)
28 # autocmd BufWritePost * execute '! vv -b ' . shellescape(@%)
30 # To use vv with emacs, add to your .emacs:
32 # (add-hook 'before-save-hook (lambda () (shell-command (
33 # concat "vv -s " (shell-quote-argument (buffer-file-name))))))
34 # (add-hook 'after-save-hook (lambda () (shell-command (
35 # concat "vv -b " (shell-quote-argument (buffer-file-name))))))
36 # (setq make-backup-files nil)
38 # To use vv with ANY editor, first set:
40 # export EDITOR=your_favourite_editor
43 # and then edit your file with:
47 # $HOME/.vvrc is the config file for vv
49 # 2013-04-15 initial version
50 # 2013-04-16 added options -m and -v
51 # 2013-04-18 added option -s
52 # 2013-04-22 realfilename() fixes symlink problematics
53 # 2013-04-22 use rsync instead of cp
54 # 2013-04-23 added option -I
55 # 2013-04-23 renamed from jedv to vv
56 # 2013-04-24 added options -e -0
57 # 2013-05-09 added option -R
58 # 2013-05-22 modified option -d to double argument
59 # 2013-05-22 added vvrc with $exclude and @diff
60 # 2013-07-05 fixed bug potential endless loop in rotate()
61 # 2014-04-16 added change-file-test for opt_s (needs .versions/$file)
62 # 2014-04-18 added option -b : save backup
63 # 2014-05-02 fixed bug wrong file ownership when using as root
64 # 2014-06-18 options -r -d -v : parameter is optional, default is 1
65 # 2014-06-18 fixed (stupid!) bug option -s does only sometimes saving
66 # 2014-06-20 options -d -v : argument is optional, default is last file
67 # 2014-07-22 fixed bug no (new) backup version 0 on option -r
68 # 2014-11-14 added option -D : delete last saved version
69 # 2014-11-14 make .versions/ mode 777 if parent directory is world writable
70 # 2015-03-19 allow write access by root even if file and .versions/ have different owners
71 # 2015-03-20 better error formating for jed
72 # 2015-06-02 added option -r . to restore last saved backup
73 # 2016-03-07 added options -M -L
74 # 2016-03-08 renamed option -I to -H
75 # 2016-05-02 added -A option to preserve ACLs with rsync
76 # 2016-06-07 option -v : use PAGER=cat if STDOUT is not a tty
77 # 2016-06-08 added zz, ezz and installer vvzz
78 # 2016-07-06 avoid empty $ZZ versioning
79 # 2016-09-12 added option -q quiet mode
83 use Digest::MD5 'md5_hex';
89 $ZZ = $ENV{ZZ} || "$ENV{HOME}/.zz";
91 &install if $0 eq 'vvzz';
99 $0 -r version-number file [new-file]
100 $0 -d version-number[:version-number] file
101 $0 -v version-number file
111 options: -l list available versions
113 -r recover file (. is last saved backup)
115 -s save file to new version
116 -D delete last saved version
117 -e edit file with \$EDITOR (with versioning)
118 -p purge orphaned versions (without current file)
120 -m migrate backup files to version files (-R all recursive)
121 -M migrate to more versions (upto 100)
122 -L migrate to less versions (upto 10)
123 -H show more information
124 examples: $0 project.pl
126 $0 -r 2 project.pl project_2.pl
129 $vvrc = $ENV{HOME} . '/.vvrc';
132 $opt_h = $opt_p = $opt_m = $opt_s = $opt_0 = $opt_e = $opt_H = $opt_b = 0;
133 $opt_q = $opt_D = $opt_R = 0;
134 $opt_r = $opt_d = $opt_v = $opt_M = $opt_L = '';
136 getopts('hHls0bepqmRDrdv+M:L:') or die $usage;
144 open $prg,$prg or die "$0: $prg - $!\n";
148 last if /^\s*$/ or /^#\s*\d\d\d\d-\d\d-\d\d/;
155 die "usage: $0 -r version-number file\n" unless @ARGV;
156 if ($ARGV[0] =~ /^(\d\d?|\.)$/) { $opt_r = shift }
158 die "usage: $0 -r version-number file\n" if scalar @ARGV != 1;
162 if (@ARGV and $ARGV[0] =~ /^\d\d?(:\d\d?)?$/) { $opt_d = shift }
165 die "usage: $0 -d version-number file\n" unless @ARGV;
169 if (@ARGV and $ARGV[0] =~ /^\d\d?$/) { $opt_v = shift }
172 die "usage: $0 -v version-number file\n" unless @ARGV;
175 if ($0 eq 've' or $opt_e) {
176 $a = pop @ARGV or die $usage;
180 die $usage if not $opt_r and @ARGV;
184 open $vvrc,'>',$vvrc or die "$0: cannot write $vvrc - $!\n";
202 $file = realfilename($a);
204 $bfile = basename($file);
205 $dir = dirname($file);
206 $vdir = "$dir/.versions";
207 $vfile = "$vdir/$bfile";
208 $vfile0 = "$vfile~0~";
209 $vfile1 = "$vfile~1~";
210 $vfile01 = "$vfile~01~";
212 # change eugid if root and version directory belongs user
214 if ($> == 0 and (not @s or $s[4])) {
215 if (my @s = stat($a)) {
221 if ($opt_r ne '.' and not ($opt_M or $opt_L)) {
222 if (not -e $file and -s $vfile) {
223 warn "$0: $a does not exist any more\n";
224 print "found $vfile - recover it? ";
226 copy($vfile,$file,'.') if /^y/i;
229 die "$0: $a does not exist\n" unless -e $file;
230 die "$0: $a is not a regular file\n" if -l $file or not -f $file;
238 if (-d $opt_M and not -l $opt_M) {
239 my $vvv = "$opt_M/.versions";
241 die "$0: cannot mkdir $vvv - $!\n" unless -d $vvv;
242 opendir $vvv,$vvv or die "$0: cannot opendir $vvv - $!\n";
243 while (my $v = readdir($vvv)) {
244 mv100("$opt_M/$1") if -f "$vvv/$v" and $v =~ /(.+)~1~$/;
247 $vvv .= "/.versions";
249 mkdir $vvv or die "$0: cannot mkdir $vvv - $!\n";
253 symlink 100,$vvv or die "$0: cannot create $vvv - $!\n";
255 die "usage: $0 -M file\n" if @ARGV or $opt_r;
262 if (-d $opt_L and not -l $opt_L) {
263 my $vvv = "$opt_L/.versions";
265 die "$0: cannot mkdir $vvv - $!\n" unless -d $vvv;
266 opendir $vvv,$vvv or die "$0: cannot opendir $vvv - $!\n";
267 while (my $v = readdir($vvv)) {
268 mv10("$opt_L/$1") if -f "$vvv/$v" and $v =~ /(.+)~01~$/;
271 $vvv .= "/.versions";
273 mkdir $vvv or die "$0: cannot mkdir $vvv - $!\n";
277 symlink 10,$vvv or die "$0: cannot create $vvv - $!\n";
279 die "usage: $0 -L file\n" if @ARGV or $opt_r;
286 die $usage unless $a;
287 $editor = $ENV{EDITOR} or die "$0: environment variable EDITOR not set\n";
288 system(qw'vv -s',$file) if -f $file; # save current version
289 system($editor,@ARGV,$file); exit $? if $?;
290 unlink $ofile; # delete new file~ created by editor
291 system(qw'vv -0',$file); # post rotating
292 system(qw'vv -b',$file); # save backup
297 die "$0: no such file $bfile\n" unless $bfile;
298 if (-f "$vfile~0$opt_v~") { $vfile .= "~0$opt_v~" }
299 else { $vfile .= "~$opt_v~" }
302 if (($ENV{EDITOR}||$0) =~ /jed/) {
303 $ENV{JEDINIT} = "SAVE_STATE=0";
304 exec 'jed',$vfile,qw'-tmp -f set_readonly(1)';
305 } elsif ($ENV{PAGER}) {
306 exec $ENV{PAGER},$vfile;
314 die "$0: no $vfile\n";
320 opendir $vdir,$vdir or die "$0: no $vdir\n";
321 while ($vfile = readdir($vdir)) {
322 next unless -f "$vdir/$vfile";
324 $bfile =~ s/~\d\d?~$//;
325 if (not -f $bfile or -l $bfile) {
326 unlink "$vdir/$vfile";
330 if (@purge = keys %purge) {
331 foreach $p (@purge) {
332 printf "%2d %s~ purged\n",$purge{$p},$p;
343 if (length($opt_r)) {
344 die "$0: no such file $bfile\n" unless $bfile;
346 die "$0: no $vfile\n" unless -f $vfile;
347 copy($vfile,$file,$opt_r);
349 if ($opt_r =~ /^\d$/ and -f "$vfile~0$opt_r~") {
350 $vfile .= "~0$opt_r~"
354 die "$0: no version $opt_r for $file\n" unless -f $vfile;
355 if ($nfile = shift @ARGV) {
358 copy($file,$vfile0) if mtime($file) > mtime($vfile0);
365 if (length($opt_d)) {
366 die "$0: no such file $bfile\n" unless $bfile;
367 @diff = qw'diff -u' unless @diff;
368 if ($opt_d =~ /^(\d\d?):(\d\d?)$/) {
369 if (-f "$vdir/$bfile~0$1~" and -f "$vdir/$bfile~0$2~") {
370 exec @diff,"$vdir/$bfile~0$2~","$vdir/$bfile~0$1~"
372 exec @diff,"$vdir/$bfile~$2~","$vdir/$bfile~$1~"
375 if (-f "$vdir/$bfile~0$opt_d~") {
376 exec @diff,"$vdir/$bfile~0$opt_d~",$file;
378 exec @diff,"$vdir/$bfile~$opt_d~",$file;
385 die $usage unless $file;
387 $exclude =~ s/^\s+//;
388 $exclude =~ s/\s+$//;
389 $exclude =~ s/\s+/|/g;
390 if ($bfile =~ /$exclude/) {
391 warn "\r\n$0: ignoring $bfile\n";
396 mkdir $vdir or die "$0: cannot mkdir $vdir - $!\n";
398 chmod 0777,$vdir if (stat $dir)[2] & 00002;
400 # migrate old file~ to versions
401 if (-f $ofile and not -l $ofile and -r $ofile) {
402 $vfn = rotate($vfile);
406 # rotate and save if file has changed
408 if (md5f($vfile1) ne md5f($file)) {
409 $vfn = rotate($vfile);
414 # rotate and save if file has changed
416 if (md5f($vfile01) ne md5f($file)) {
417 $vfn = rotate($vfile);
423 if ((readlink("$vdir/.versions/n")||10) == 100) {
424 copy($file,$vfile01);
433 die $usage unless $file;
435 mkdir $vdir or die "\r\n$0: cannot mkdir $vdir - $!\n";
438 if ($ENV{VIMRUNTIME}) {
441 warn "$file --> $vfile\n" unless $opt_q;
446 # special post rotating from -e
448 my @sb = stat $file or die "$0: $file - $!\n";
450 while (my @sv = stat $vfile1) {
452 if ($sb[7] == $sv[7] and $sb[9] == $sv[9]) {
461 while (my @sv = stat $vfile01) {
463 if ($sb[7] == $sv[7] and $sb[9] == $sv[9]) {
474 # delete last version, roll back
476 die "usage: $0 -D file\n" unless $vfile1 or $vfile01;
477 stat $file or die "$0: $file - $!\n";
483 rb10($vfile) if -f $vfile1;
484 rb100($vfile) if -f $vfile01;
492 `stty -a` =~ /columns (\d+)/;
494 if (opendir $vdir,$vdir) {
495 while ($vfile = readdir($vdir)) {
496 if (-f "$vdir/$vfile") {
498 if ($vfile =~ /^\Q$bfile\E~(\d\d?)~$/) {
499 push @{$v{$file}},$1;
502 if ($vfile =~ /^(.+)~(\d\d?)~$/) {
505 push @{$v{$vfile}},0;
512 foreach $file (sort keys %v) {
513 if (not -f $file or -l $file) {
514 warn "$0: orphaned $file~\n";
517 @v = sort @{$v{$file}};
519 @stat = stat $file or die "$0: $file - $!\n";
520 print "version bytes date time";
523 $ct = content($file);
524 $ct =~ s/(.{$tw}).+/$1*/;
527 if (length($v[0]) == 1) { $lf = "%s %10s %s %s\n" }
528 else { $lf = "%2s %10s %s %s\n" }
529 printf $lf,'.',size($stat[7]),isodate($stat[9]),$ct;
531 $vfile = "$vdir/$bfile~$v~";
532 @stat = stat $vfile or next;
534 $ct = content($vfile);
535 $ct =~ s/(.{$tw}).+/$1*/;
537 printf $lf,int($v),size($stat[7]),isodate($stat[9]),$ct;
541 $n-- if $v[0] == 0; # do not count zero version
542 printf "%d %s\n",$n,$file;
552 if ($s > 9999999999) { $s = int($s/2**30).'G' }
553 elsif ($s > 9999999) { $s = int($s/2**20).'M' }
554 elsif ($s > 9999) { $s = int($s/2**10).'k' }
564 chomp ($ct = `file $file`);
568 if ($ct =~ /text/ and open $file,$file) {
582 my @d = localtime shift;
583 return sprintf('%d-%02d-%02d %02d:%02d:%02d',
584 $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]);
588 my $vf = shift; # version base file
590 my $vf01 = "$vf~01~";
594 for (my $i = 8; $i >= 0; $i--) {
595 $vfi = sprintf("%s~%d~",$vf,$i);
596 $vfn = sprintf("%s~%d~",$vf,$i+1);
598 rename $vfi,$vfn or die "$0: $vfi --> $vfn : $!\n";
601 # was there a version 0?
604 $bf =~ s:/\.versions/:/:;
607 # version change? (other size or mtime)
608 if (@sb and @sv and $sb[7] == $sv[7] and $sb[9] == $sv[9]) {
618 for (my $i = 98; $i >= 0; $i--) {
619 $vfi = sprintf("%s~%02d~",$vf,$i);
620 $vfn = sprintf("%s~%02d~",$vf,$i+1);
622 rename $vfi,$vfn or die "$0: $vfi --> $vfn : $!\n";
625 # was there a version 0?
628 $bf =~ s:/\.versions/:/:;
631 # version change? (other size or mtime)
632 if (@sb and @sv and $sb[7] == $sv[7] and $sb[9] == $sv[9]) {
647 my ($from,$to,$restore) = @_;
650 if (-l $file or not -f $file) {
651 die "$0: $file is not a regular file\n";
655 if (open $to,'>>',$to) {
657 if (system(qw'rsync -aA',$from,$to) == 0) {
658 if ($ENV{VIMRUNTIME}) {
661 warn "$from --> $to\n" unless $opt_q;
667 die "\r\n$0: cannot write $to - $!\n";
674 return $file unless -e $file;
677 my $link = readlink($file);
678 if ($link !~ /^\// and $file =~ m:(.*/).:) {
681 return realfilename($link);
689 my $vdir = "$dir/.versions";
692 opendir $dir,$dir or die "$0: cannot read directory $dir - $!\n";
693 while ($file = readdir($dir)) {
694 $dfile = "$dir/$file";
695 next if -l $dfile or $file eq '.' or $file eq '..';
696 if (-d $dfile and $opt_R and $file ne '.versions') {
698 } elsif (-f $dfile and $file =~ /~$/) {
700 for ($i = 8; $i > 0; $i--) {
702 rename "$vdir/$file$i~","$vdir/$file$n~";
705 mkdir $vdir or die "$0: cannot mkdir $vdir - $!\n";
707 $nfile = sprintf("%s/%s1~",$vdir,$file);
708 rename $dfile,$nfile or die "$0: cannot move $dfile to $nfile - $!\n";
709 warn "$dfile --> $nfile\n" unless $opt_q;
717 return @s ? $s[9] : 0;
725 if (open $file,$file) {
726 $md5 = md5_hex(<$file>);
733 # if ARGV is empty use last saved file as default file argument
739 if (-d '.versions' and open V,'ls -at .versions|') {
757 my $vfile = dirname($file).'/.versions/'.basename($file);
759 die "$0: $file has no extended versions\n" unless -f "$vfile~01~";
760 for (my $i=1; $i<10; $i++) {
761 my $vfile1 = "$vfile~$i~";
762 my $vfile2 = "$vfile~0$i~";
764 warn "$vfile2 --> $vfile1\n" unless $opt_q;
765 rename $vfile2,$vfile1 or die "$0: $!\n";
768 for (my $i=10; $i<100; $i++) {
775 my $vfile = dirname($file).'/.versions/'.basename($file);
777 die "$0: $file has already extended versions\n" if -f "$vfile~01~";
778 die "$0: $file has no versions\n" unless -f "$vfile~1~";
779 for (my $i=1; $i<10; $i++) {
780 my $vfile1 = "$vfile~$i~";
781 my $vfile2 = "$vfile~0$i~";
783 warn "$vfile1 --> $vfile2\n" unless $opt_q;
784 rename $vfile1,$vfile2 or die "$0: $!\n";
794 for (my $i = 1; $i <= 8; $i++) {
795 my $vfi = sprintf("%s~%d~",$vfile,$i);
796 my $vfn = sprintf("%s~%d~",$vfile,$i+1);
800 unlink $vfi if $i == 1;
811 for (my $i = 1; $i <= 98; $i++) {
812 my $vfi = sprintf("%s~%02d~",$vfile,$i);
813 my $vfn = sprintf("%s~%02d~",$vfile,$i+1);
817 unlink $vfi if $i == 1;
828 foreach my $dir (split(':',$ENV{PATH})) {
829 return "$dir/$prg" if -x "$dir/$prg";
834 # zz is the generic clip board program
836 # to use zz with vim, write to your .vimrc:
838 # noremap <silent> zz> :w !zz<CR><CR>
839 # noremap <silent> zz< :r !zz --<CR>
845 if ("@ARGV" =~ /^(-h|--help)$/) {
847 zz is the generic clip board program. It can hold any data, ASCII or binary.
848 The clip board itself is $ZZ (default: $HOME/.zz).
849 See also the clip board editor "ezz".
850 Limitation: zz does not work across accounts or hosts! Use xx instead.
852 Options and modes are:
854 "zz" show content of $ZZ
855 "zz file(s)" copy file(s) content into $ZZ
856 "zz -" write STDIN (keyboard, mouse buffer) to $ZZ
857 "zz +" add STDIN (keyboard, mouse buffer) to $ZZ
858 "... | zz" write STDIN from pipe to $ZZ
859 "... | zz +" add STDIN from pipe to $ZZ
860 "... | zz -" write STDIN from pipe to $ZZ and STDOUT
861 "zz | ..." write $ZZ to pipe
862 "... | zz | ..." save pipe data to $ZZ (like tee)
863 "zz --" write $ZZ to STDOUT
864 "zz -v" show clip board versions (history)
865 "zz -1" write $ZZ version 1 to STDOUT
866 "zz -9" write $ZZ version 9 to STDOUT
880 if ("@ARGV" eq '-v') {
884 if ("@ARGV" =~ /^-(\d)$/) {
885 exec "vv -v $1 '$ZZ' | cat";
889 if (-t STDIN and not @ARGV or "@ARGV" eq '--') {
894 system "vv -s '$ZZ' >/dev/null 2>&1" if -s $ZZ;
896 if (@ARGV and $ARGV[0] eq '+') {
901 if ("@ARGV" eq '-') {
903 $tee = 1 unless -t STDIN;
906 $tee = 1 unless @ARGV or -t STDIN or -t STDOUT;
909 open $ZZ,$wm,$ZZ or die "$0: cannot write $ZZ - $!\n";
912 while ($file = shift @ARGV) {
914 if (open $file,$file) {
915 while (read($file,$x,$bs)) {
916 my $s = syswrite $ZZ,$x;
917 defined($s) or die "$0: cannot write to $ZZ - $!\n";
921 warn "$0: cannot read $file - $!\n";
924 warn "$0: $file is not a regular file\n";
926 warn "$0: $file does not exist\n";
931 $ZZ1 =~ s:(.*)/(.*):$1/.versions/$2:;
932 if (-e $ZZ and not -s $ZZ and -s $ZZ1 ) {
933 system qw'rsync -aA',$ZZ1,$ZZ;
936 while (read(STDIN,$x,$bs)) {
938 syswrite STDOUT,$x if $tee;
949 my $editor = $ENV{EDITOR} || 'vi';
952 $ENV{JEDINIT} = "SAVE_STATE=0";
954 if ("@ARGV" =~ /^(-h|--help)$/) {
956 ezz is the edit helper for the zz clip board program.
957 The clip board itself is $ZZ (default: $HOME/.zz).
959 Options and modes are:
961 "ezz" edit $ZZ with $EDITOR
962 "... | ezz" write STDIN from pipe to $ZZ and call $EDITOR
963 "... | ezz +" add STDIN from pipe to $ZZ and call $EDITOR
964 "ezz 'perl commands'" execute perl commands on $ZZ
965 "ezz - 'perl commands'" execute perl commands on $ZZ and show result
966 "ezz filter [args]" run filter [with args] on $ZZ
967 "ezz - filter [args]" run filter [with args] on $ZZ and show result
979 system "vv -s '$ZZ' >/dev/null 2>&1" if -s $ZZ;
982 if ("@ARGV" eq '+') {
986 open $ZZ,$wm,$ZZ or die "$0: cannot write $ZZ - $!\n";
987 syswrite $ZZ,$x while read(STDIN,$x,$bs);
992 $out = shift @ARGV if $ARGV[0] eq '-';
993 $cmd = shift @ARGV or exec 'cat',$ZZ;
994 rename $ZZ,"$ZZ~" or die "$0: cannot move $ZZ to $ZZ~ - $!\n";
995 $cmd = quotemeta $cmd;
996 @ARGV = map { quotemeta } @ARGV;
997 if (pathsearch($cmd)) {
998 system "$cmd @ARGV <'$ZZ~'>'$ZZ'";
1000 system "perl -pe $cmd @ARGV <'$ZZ~'>'$ZZ'";
1002 if ($? == 0) { unlink "$ZZ~" }
1003 else { rename "$ZZ~",$ZZ }
1004 exec 'cat',$ZZ if $out;
1016 print "Installation directory: ";
1022 unlink qw'zz ezz vv';
1023 link $prg,'zz' or die "$0: cannot create zz - $!\n";
1024 link $prg,'ezz' or die "$0: cannot create ezz - $!\n";
1025 rename $prg,'vv' or die "$0: cannot create vv - $!\n";
1027 die "$0: $dir does not exist\n" unless -e $dir;
1028 die "$0: $dir is not a directory\n" unless -d $dir;
1029 die "$0: $dir is not writable\n" unless -w $dir;
1030 chdir $dir or die "$0: cannot cd $dir - $!\n";
1031 unlink qw'zz ezz vv';
1032 system qw'rsync -a',$prg,'vv';
1034 link 'vv','zz' or die "$0: cannot create $dir/zz - $!\n";
1035 link 'vv','ezz' or die "$0: cannot create $dir/ezz - $!\n";
1037 print "Installation completed. See:\n";
1038 print "\t$dir/vv -h\n";
1039 print "\t$dir/zz -h\n";
1040 print "\t$dir/ezz -h\n";