sub update()

in git-cvsserver.perl [3832:4138]


sub update
{
    my $self = shift;

    # first lets get the commit list
    $ENV{GIT_DIR} = $self->{git_path};

    my $commitsha1 = ::safe_pipe_capture('git', 'rev-parse', $self->{module});
    chomp $commitsha1;

    my $commitinfo = ::safe_pipe_capture('git', 'cat-file', 'commit', $self->{module});
    unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{$state->{hexsz}}/ )
    {
        die("Invalid module '$self->{module}'");
    }


    my $git_log;
    my $lastcommit = $self->_get_prop("last_commit");

    if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
         # invalidate the gethead cache
         $self->clearCommitRefCaches();
         return 1;
    }

    # Start exclusive lock here...
    $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN";

    # TODO: log processing is memory bound
    # if we can parse into a 2nd file that is in reverse order
    # we can probably do something really efficient
    my @git_log_params = ('--pretty', '--parents', '--topo-order');

    if (defined $lastcommit) {
        push @git_log_params, "$lastcommit..$self->{module}";
    } else {
        push @git_log_params, $self->{module};
    }
    # git-rev-list is the backend / plumbing version of git-log
    open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
                or die "Cannot call git-rev-list: $!";
    my @commits=readCommits($gitLogPipe);
    close $gitLogPipe;

    # Now all the commits are in the @commits bucket
    # ordered by time DESC. for each commit that needs processing,
    # determine whether it's following the last head we've seen or if
    # it's on its own branch, grab a file list, and add whatever's changed
    # NOTE: $lastcommit refers to the last commit from previous run
    #       $lastpicked is the last commit we picked in this run
    my $lastpicked;
    my $head = {};
    if (defined $lastcommit) {
        $lastpicked = $lastcommit;
    }

    my $committotal = scalar(@commits);
    my $commitcount = 0;

    # Load the head table into $head (for cached lookups during the update process)
    foreach my $file ( @{$self->gethead(1)} )
    {
        $head->{$file->{name}} = $file;
    }

    foreach my $commit ( @commits )
    {
        $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)");
        if (defined $lastpicked)
        {
            if (!in_array($lastpicked, @{$commit->{parents}}))
            {
                # skip, we'll see this delta
                # as part of a merge later
                # warn "skipping off-track  $commit->{hash}\n";
                next;
            } elsif (@{$commit->{parents}} > 1) {
                # it is a merge commit, for each parent that is
                # not $lastpicked (not given a CVS revision number),
                # see if we can get a log
                # from the merge-base to that parent to put it
                # in the message as a merge summary.
                my @parents = @{$commit->{parents}};
                foreach my $parent (@parents) {
                    if ($parent eq $lastpicked) {
                        next;
                    }
                    # git-merge-base can potentially (but rarely) throw
                    # several candidate merge bases. let's assume
                    # that the first one is the best one.
		    my $base = eval {
			    ::safe_pipe_capture('git', 'merge-base',
						 $lastpicked, $parent);
		    };
		    # The two branches may not be related at all,
		    # in which case merge base simply fails to find
		    # any, but that's Ok.
		    next if ($@);

                    chomp $base;
                    if ($base) {
                        my @merged;
                        # print "want to log between  $base $parent \n";
                        open(GITLOG, '-|', 'git', 'log', '--pretty=medium', "$base..$parent")
			  or die "Cannot call git-log: $!";
                        my $mergedhash;
                        while (<GITLOG>) {
                            chomp;
                            if (!defined $mergedhash) {
                                if (m/^commit\s+(.+)$/) {
                                    $mergedhash = $1;
                                } else {
                                    next;
                                }
                            } else {
                                # grab the first line that looks non-rfc822
                                # aka has content after leading space
                                if (m/^\s+(\S.*)$/) {
                                    my $title = $1;
                                    $title = substr($title,0,100); # truncate
                                    unshift @merged, "$mergedhash $title";
                                    undef $mergedhash;
                                }
                            }
                        }
                        close GITLOG;
                        if (@merged) {
                            $commit->{mergemsg} = $commit->{message};
                            $commit->{mergemsg} .= "\nSummary of merged commits:\n\n";
                            foreach my $summary (@merged) {
                                $commit->{mergemsg} .= "\t$summary\n";
                            }
                            $commit->{mergemsg} .= "\n\n";
                            # print "Message for $commit->{hash} \n$commit->{mergemsg}";
                        }
                    }
                }
            }
        }

        # convert the date to CVS-happy format
        my $cvsDate = convertToCvsDate($commit->{date});

        if ( defined ( $lastpicked ) )
        {
            my $filepipe = open(FILELIST, '-|', 'git', 'diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!");
	    local ($/) = "\0";
            while ( <FILELIST> )
            {
		chomp;
                unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{$state->{hexsz}}\s+([a-f0-9]{$state->{hexsz}})\s+(\w)$/o )
                {
                    die("Couldn't process git-diff-tree line : $_");
                }
		my ($mode, $hash, $change) = ($1, $2, $3);
		my $name = <FILELIST>;
		chomp($name);

                # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");

                my $dbMode = convertToDbMode($mode);

                if ( $change eq "D" )
                {
                    #$log->debug("DELETE   $name");
                    $head->{$name} = {
                        name => $name,
                        revision => $head->{$name}{revision} + 1,
                        filehash => "deleted",
                        commithash => $commit->{hash},
                        modified => $cvsDate,
                        author => $commit->{author},
                        mode => $dbMode,
                    };
                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                }
                elsif ( $change eq "M" || $change eq "T" )
                {
                    #$log->debug("MODIFIED $name");
                    $head->{$name} = {
                        name => $name,
                        revision => $head->{$name}{revision} + 1,
                        filehash => $hash,
                        commithash => $commit->{hash},
                        modified => $cvsDate,
                        author => $commit->{author},
                        mode => $dbMode,
                    };
                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                }
                elsif ( $change eq "A" )
                {
                    #$log->debug("ADDED    $name");
                    $head->{$name} = {
                        name => $name,
                        revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
                        filehash => $hash,
                        commithash => $commit->{hash},
                        modified => $cvsDate,
                        author => $commit->{author},
                        mode => $dbMode,
                    };
                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                }
                else
                {
                    $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name");
                    die;
                }
            }
            close FILELIST;
        } else {
            # this is used to detect files removed from the repo
            my $seen_files = {};

            my $filepipe = open(FILELIST, '-|', 'git', 'ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!");
	    local $/ = "\0";
            while ( <FILELIST> )
            {
		chomp;
                unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
                {
                    die("Couldn't process git-ls-tree line : $_");
                }

                my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );

                $seen_files->{$git_filename} = 1;

                my ( $oldhash, $oldrevision, $oldmode ) = (
                    $head->{$git_filename}{filehash},
                    $head->{$git_filename}{revision},
                    $head->{$git_filename}{mode}
                );

                my $dbMode = convertToDbMode($mode);

                # unless the file exists with the same hash, we need to update it ...
                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
                {
                    my $newrevision = ( $oldrevision or 0 ) + 1;

                    $head->{$git_filename} = {
                        name => $git_filename,
                        revision => $newrevision,
                        filehash => $git_hash,
                        commithash => $commit->{hash},
                        modified => $cvsDate,
                        author => $commit->{author},
                        mode => $dbMode,
                    };


                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                }
            }
            close FILELIST;

            # Detect deleted files
            foreach my $file ( sort keys %$head )
            {
                unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" )
                {
                    $head->{$file}{revision}++;
                    $head->{$file}{filehash} = "deleted";
                    $head->{$file}{commithash} = $commit->{hash};
                    $head->{$file}{modified} = $cvsDate;
                    $head->{$file}{author} = $commit->{author};

                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
                }
            }
            # END : "Detect deleted files"
        }


        if (exists $commit->{mergemsg})
        {
            $self->insert_mergelog($commit->{hash}, $commit->{mergemsg});
        }

        $lastpicked = $commit->{hash};

        $self->_set_prop("last_commit", $commit->{hash});
    }

    $self->delete_head();
    foreach my $file ( sort keys %$head )
    {
        $self->insert_head(
            $file,
            $head->{$file}{revision},
            $head->{$file}{filehash},
            $head->{$file}{commithash},
            $head->{$file}{modified},
            $head->{$file}{author},
            $head->{$file}{mode},
        );
    }
    # invalidate the gethead cache
    $self->clearCommitRefCaches();


    # Ending exclusive lock here
    $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
}