# -*-perl-*-
#------------------------------------------------------------------------------
# cvsget.pl --- Download CVS repositort (like CVSGrab)
# Copyright (C) 2003 Dhruva Krishnamurthy

# Author: Dhruva Krishnamurthy
# Maintainer: seagull@fastmail.fm
# Keywords: cvs, proxy, firewall, grab
# Status: BETA
#         o have Tried with PERL 5.8 and GNU Wget 1.9-beta for GNU Emacs
# Created: 01st August 2003

# Time-stamp: <2003-08-01 19:09:31 dhruva>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, you can either send email to this
# program's maintainer or write to: The Free Software Foundation,
# Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA.

# DISCLAIMER:
# I do not take any responsibility for the functioning of cvsget.pl script
# I have tested it on Windows 2000 using PERL 5.8 and GNU Wget 1.9-beta
# I request any modifications/improvements to cvsget.pl to be posted to me.

# Commentary:

# PERL script to download a CVS repository through HTTP. The CVS repository
# must be viewable in the browser throught ViewCVS. Useful when behind the
# all too (in)famous fascist firewall and proxy. As I am behind one, this
# served as an inspiration.
# In rememberence of my cousin.

# Usage:
#    o Copy the script to somewhere in your path
#    o Edit the user modifiable variables
#    o $base_url must be the base URL for download link, NOT view link
#    o perl -S cvsget.pl

# Todo:
#    o Support command line options
#    o Use Berkeley DB to store info
#    o Remove dependency on GNU wget
#------------------------------------------------------------------------------
use File::Path;
$use_lwp=0;

#----------------------- User modifiable values -------------------------------
$package="emacs";
$base_url="http://savannah.gnu.org/cgi-bin/viewcvs/*checkout*/";
$cdir="emacs/";
$cvsroot="D:/tmp/cvs/";
#----------------------- User modifiable values -------------------------------

$version=1.0;

@dirlist=();
%filehash;

%prevdirhash;
%prevfilehash;

%updatedirhash;
%updatefilehash;

$newrepository=0;
$dirinfo="$cvsroot$package/dirlist";
$fileinfo="$cvsroot$package/filelist";
$indexfile="$cvsroot$package/index.html";

$ua;
if($use_lwp){
    use LWP::UserAgent;
    $ua=LWP::UserAgent->new(env_proxy => 1,
                            keep_alive => 1,
                            timeout => 30,
                           );

}

#------------------------------------------------------------------------------
# GetRemoteFile
#------------------------------------------------------------------------------
sub GetRemoteFile
{
    my $url=@_[0];
    my $tar=@_[1];

    if($use_lwp && $ua){
        my $response=$ua->get($url);
        if($response->is_success){
            open(URLOUT,">$tar")
                || die("Cannot open $tar for write");
            printf(URLOUT $response->content);
            close(URLOUT);
        }else{
            return(1);
        }
    }else{
        system("wget",
               "--quiet",
               "--output-document=$tar",
               "$url");
        if($?){
            return(1);
        }
    }

    return(0);
}

#------------------------------------------------------------------------------
# GetLocalRepositoryData
#  Extracts information of local CVS reporitory from previous update
#------------------------------------------------------------------------------
sub GetLocalRepositoryData
{
    if(! -f $dirinfo || ! -f $fileinfo){
        $newrepository=1;
        return(0);
    }

    open(PDIRIN,"<$dirinfo")
        || die("Cannot open previous $dirinfo for read");
    open(PFILEIN,"<$fileinfo")
        || die("Cannot open previous $fileinfo for read");

    while(<PDIRIN>){
        chomp();
        # Default - Remove all
        $prevdirhash{$_}='r';
    }
    close(PDIRIN);

    while(<PFILEIN>){
        chomp();
        my @arr=split(/ /,$_);
        if($#arr==1){
            $prevfilehash{@arr[0]}=@arr[1];
        }
    }
    close(PFILEIN);

    return(0);
}

#------------------------------------------------------------------------------
# GetRemoteRepositoryData
#  Extracts information of remote CVS reporitory
#------------------------------------------------------------------------------
sub GetRemoteRepositoryData
{
    printf(".");
    if(&GetRemoteFile("$base_url$cdir",$indexfile)){
        return(1);
    }

    open(INDEX,"<$indexfile")
        || die("Unable to open $indexfile file for read");

    # Add the directory to traversed list
    push(@dirlist,$cdir);

    my @ldirlist=();
    while(<INDEX>){
        chomp();
        if(/[a-zA-Z0-9]+/){
            if(/href/i){
                my @arr=split(/ /,$_);
                foreach(@arr){
                    # Ignore .cvsignore, Attic
                    if($_=~/(\.cvsignore|attic\/)/i){
                        next;
                    }

                    tr/'"'//d;
                    # For folders
                    if(/href=.+\/[ \t]*\>/i){
                        tr/>//d;
                        my @type=split(/=/,$_);
                        my $dir="$cdir@type[$#type]";
                        push(@ldirlist,$dir);
                        next;
                    }

                    # For files
                    if(/href=.+\?rev=([0-9]+\.?[0-9]+)/i){
                        my @data=split(/\?/,$_);
                        my @type=split(/=/,@data[0]);
                        my @rev=split(/=/,@data[1]);
                        my $file="$cdir@type[1]";
                        my $ver=@rev[1];
                        $ver=~s/&.+//g;
                        $filehash{$file}=$ver;
                        next;
                    }
                }
            }
        }
    }
    close(INDEX);
    unlink($indexfile);

    # Recurse through folders
    foreach(@ldirlist){
        $cdir=$_;
        &GetRemoteRepositoryData;
    }

    return(0);
}

#------------------------------------------------------------------------------
# MakeUpdateInfo
#  Generates the difference between local and remote repository
#------------------------------------------------------------------------------
sub MakeUpdateInfo
{
    # Dir info
    foreach(@dirlist){
        if(! -d "$cvsroot$_"){
            $prevdirhash{$_}='y';
        }else{
            $prevdirhash{$_}='n';
        }
    }

    # File info - New or modified
    foreach(keys %filehash){
        if(exists($prevfilehash{$_})){
            if($filehash{$_} eq $prevfilehash{$_}){
                # If the actual file is deleted
                if(! -f "$cvsroot$_"){
                    $prevfilehash{$_}='y';
                }else{
                    $prevfilehash{$_}='n';
                }
            }else{
                $prevfilehash{$_}='y';
            }
        }else{
            $prevfilehash{$_}='y';
        }
    }
    # File info - Deleted
    if(!$newrepository){
        foreach(keys %prevfilehash){
            if(!exists($filehash{$_})){
                $prevfilehash{$_}='r';
            }
        }
    }

    return(0);
}

#------------------------------------------------------------------------------
# UpdateLocalRepository
#  Synchronizes the local repository with remote CVS repository
#------------------------------------------------------------------------------
sub UpdateLocalRepository
{
    # Update directories
    foreach(sort keys %prevdirhash){
        if($prevdirhash{$_} eq 'y'){
            printf("Updating $cvsroot$_\n");
            mkpath("$cvsroot$_");
        }elsif($prevdirhash{$_} eq 'r'){
            printf("Removing $cvsroot$_\n");
            rmtree("$cvsroot$_");
        }
    }

    # Update files
    foreach(sort keys %prevfilehash){
        if($prevfilehash{$_} eq 'y'){
            printf("Updating $cvsroot$_\n");
            &GetRemoteFile("$base_url$_","$cvsroot$_")
            }elsif($prevfilehash{$_} eq 'r'){
                printf("Removing $cvsroot$_\n");
                unlink("$cvsroot$_");
            }
    }
    return(0);
}

#------------------------------ Execution starts ------------------------------
$cdir="$package/$cdir";
unlink($indexfile);

# Gets the current CVS repository's data
&GetLocalRepositoryData;

# Recursively get the CVS contents
printf("Getting remote data from $cdir\n");
printf("Progressing: ");
&GetRemoteRepositoryData;
printf("\n");

# Generate the update/modify info
&MakeUpdateInfo;

# Do the actual updation of local repository from remote CVS
&UpdateLocalRepository;

# Write out the remote directory list
open(DIROUT,">$dirinfo")
    || die("Could not open file for $dirinfo for write");
foreach(@dirlist){
    printf(DIROUT "$_\n");
}
close(DIROUT);

# Write out the remote file list
open(FILEOUT,">$fileinfo")
    || die("Could not open file for $fileinfo for write");
foreach(sort keys %filehash){
    printf(FILEOUT "$_ $filehash{$_}\n");
}
close(FILEOUT);
