git-svn: fix committing to subdirectories, add tests
[gitweb.git] / git-svn.perl
index f01fb9a35d55dd2e6499ef0df884188f5c839910..1d448e75dab8e494afb1f5c3eede924bf95e9592 100755 (executable)
@@ -277,8 +277,13 @@ sub cmd_init {
 }
 
 sub cmd_fetch {
+       if (@_) {
+               die "Additional fetch arguments are no longer supported.\n",
+                   "Use --follow-parent if you have moved/copied directories
+                   instead.\n";
+       }
        my $gs = Git::SVN->new;
-       $gs->fetch(@_);
+       $gs->fetch(parse_revision_argument());
        if ($gs->{last_commit} && !verify_ref('refs/heads/master^0')) {
                command_noisy(qw(update-ref refs/heads/master),
                              $gs->{last_commit});
@@ -310,7 +315,7 @@ sub cmd_set_tree {
        my $gs = Git::SVN->new;
        my ($r_last, $cmt_last) = $gs->last_rev_commit;
        $gs->fetch;
-       if ($r_last != $gs->{last_rev}) {
+       if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
                fatal "There are new revisions that were fetched ",
                      "and need to be merged (or acknowledged) ",
                      "before committing.\nlast rev: $r_last\n",
@@ -349,7 +354,7 @@ sub cmd_dcommit {
                        my $pool = SVN::Pool->new;
                        my %ed_opts = ( r => $last_rev,
                                        ra => $ra->dup,
-                                       svn_path => $ra->{svn_path} );
+                                       svn_path => $gs->{path} );
                        my $ed = SVN::Git::Editor->new(\%ed_opts,
                                         $ra->get_commit_editor($log,
                                         sub { print "Committed r$_[0]\n";
@@ -432,6 +437,7 @@ sub cmd_commit_diff {
        my $usage = "Usage: $0 commit-diff -r<revision> ".
                    "<tree-ish> <tree-ish> [<URL>]\n";
        fatal($usage) if (!defined $ta || !defined $tb);
+       my $svn_path;
        if (!defined $url) {
                my $gs = eval { Git::SVN->new };
                if (!$gs) {
@@ -439,6 +445,7 @@ sub cmd_commit_diff {
                              "the command-line\n", $usage);
                }
                $url = $gs->{url};
+               $svn_path = $gs->{path};
        }
        unless (defined $_revision) {
                fatal("-r|--revision is a required argument\n", $usage);
@@ -454,6 +461,7 @@ sub cmd_commit_diff {
                $_message ||= get_commit_entry($tb)->{log};
        }
        my $ra ||= Git::SVN::Ra->new($url);
+       $svn_path ||= $ra->{svn_path};
        my $r = $_revision;
        if ($r eq 'HEAD') {
                $r = $ra->get_latest_revnum;
@@ -463,7 +471,7 @@ sub cmd_commit_diff {
        my $pool = SVN::Pool->new;
        my %ed_opts = ( r => $r,
                        ra => $ra->dup,
-                       svn_path => $ra->{svn_path} );
+                       svn_path => $svn_path );
        my $ed = SVN::Git::Editor->new(\%ed_opts,
                                       $ra->get_commit_editor($_message,
                                         sub { print "Committed r$_[0]\n" }),
@@ -477,6 +485,18 @@ sub cmd_commit_diff {
 
 ########################### utility functions #########################
 
+sub parse_revision_argument {
+       if (!defined $_revision || $_revision eq 'BASE:HEAD') {
+               return (undef, undef);
+       }
+       return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
+       return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
+       return (undef, $1) if ($_revision =~ /^BASE:(\d+)$/);
+       return ($1, undef) if ($_revision =~ /^(\d+):HEAD$/);
+       die "revision argument: $_revision not understood by git-svn\n",
+           "Try using the command-line svn client instead\n";
+}
+
 sub complete_svn_url {
        my ($url, $path) = @_;
        $path =~ s#/+$##;
@@ -720,6 +740,7 @@ sub read_all_remotes {
 }
 
 sub verify_remotes_sanity {
+       return unless -d $ENV{GIT_DIR};
        my %seen;
        foreach (command(qw/config -l/)) {
                if (m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {
@@ -742,35 +763,78 @@ sub sanitize_remote_name {
        $name;
 }
 
-sub init {
-       my ($class, $url, $path, $repo_id, $ref_id) = @_;
-       my $self = _new($class, $repo_id, $ref_id, $path);
-       if (defined $url) {
-               $url =~ s!/+$!!; # strip trailing slash
+sub find_existing_remote {
+       my ($url, $remotes) = @_;
+       my $existing;
+       foreach my $repo_id (keys %$remotes) {
+               my $u = $remotes->{$repo_id}->{url} or next;
+               next if $u ne $url;
+               $existing = $repo_id;
+               last;
+       }
+       $existing;
+}
 
+sub init_remote_config {
+       my ($self, $url) = @_;
+       $url =~ s!/+$!!; # strip trailing slash
+       my $r = read_all_remotes();
+       my $existing = find_existing_remote($url, $r);
+       if ($existing) {
+               print STDERR "Using existing ",
+                            "[svn-remote \"$existing\"]\n";
+               $self->{repo_id} = $existing;
+       } else {
+               my $min_url = Git::SVN::Ra->new($url)->minimize_url;
+               $existing = find_existing_remote($min_url, $r);
+               if ($existing) {
+                       print STDERR "Using existing ",
+                                    "[svn-remote \"$existing\"]\n";
+                       $self->{repo_id} = $existing;
+               }
+               if ($min_url ne $url) {
+                       print STDERR "Using higher level of URL: ",
+                                    "$url => $min_url\n";
+                       my $old_path = $self->{path};
+                       $self->{path} = $url;
+                       $self->{path} =~ s!^\Q$min_url\E/*!!;
+                       if (length $old_path) {
+                               $self->{path} .= "/$old_path";
+                       }
+                       $url = $min_url;
+               }
+       }
+       my $orig_url;
+       if (!$existing) {
                # verify that we aren't overwriting anything:
-               my $orig_url = eval {
+               $orig_url = eval {
                        command_oneline('config', '--get',
-                                       "svn-remote.$repo_id.url")
+                                       "svn-remote.$self->{repo_id}.url")
                };
                if ($orig_url && ($orig_url ne $url)) {
-                       die "svn-remote.$repo_id.url already set: ",
+                       die "svn-remote.$self->{repo_id}.url already set: ",
                            "$orig_url\nwanted to set to: $url\n";
                }
-               my ($xrepo_id, $xpath) = find_ref($self->refname);
-               if (defined $xpath) {
-                       die "svn-remote.$xrepo_id.fetch already set to track ",
-                           "$xpath:refs/remotes/", $self->refname, "\n";
-               }
-               if (!$orig_url) {
-                       command_noisy('config',
-                                     "svn-remote.$repo_id.url", $url);
-               }
-               command_noisy('config', '--add',
-                             "svn-remote.$repo_id.fetch",
-                             "$path:".$self->refname);
        }
+       my ($xrepo_id, $xpath) = find_ref($self->refname);
+       if (defined $xpath) {
+               die "svn-remote.$xrepo_id.fetch already set to track ",
+                   "$xpath:refs/remotes/", $self->refname, "\n";
+       }
+       command_noisy('config',
+                     "svn-remote.$self->{repo_id}.url", $url);
+       command_noisy('config', '--add',
+                     "svn-remote.$self->{repo_id}.fetch",
+                     "$self->{path}:".$self->refname);
        $self->{url} = $url;
+}
+
+sub init {
+       my ($class, $url, $path, $repo_id, $ref_id) = @_;
+       my $self = _new($class, $repo_id, $ref_id, $path);
+       if (defined $url) {
+               $self->init_remote_config($url);
+       }
        $self;
 }
 
@@ -865,6 +929,9 @@ sub traverse_ignore {
        }
 }
 
+sub last_rev { ($_[0]->last_rev_commit)[0] }
+sub last_commit { ($_[0]->last_rev_commit)[1] }
+
 # returns the newest SVN revision number and newest commit SHA1
 sub last_rev_commit {
        my ($self) = @_;
@@ -902,22 +969,11 @@ sub last_rev_commit {
        return ($rev, $c);
 }
 
-sub parse_revision {
-       my ($self, $base) = @_;
-       my $head = $self->ra->get_latest_revnum;
-       if (!defined $::_revision || $::_revision eq 'BASE:HEAD') {
-               return ($base + 1, $head) if (defined $base);
-               return (0, $head);
-       }
-       return ($1, $2) if ($::_revision =~ /^(\d+):(\d+)$/);
-       return ($::_revision, $::_revision) if ($::_revision =~ /^\d+$/);
-       if ($::_revision =~ /^BASE:(\d+)$/) {
-               return ($base + 1, $1) if (defined $base);
-               return (0, $head);
-       }
-       return ($1, $head) if ($::_revision =~ /^(\d+):HEAD$/);
-       die "revision argument: $::_revision not understood by git-svn\n",
-               "Try using the command-line svn client instead\n";
+sub get_fetch_range {
+       my ($self, $min, $max) = @_;
+       $max ||= $self->ra->get_latest_revnum;
+       $min ||= $self->last_rev || 0;
+       (++$min, $max);
 }
 
 sub tmp_index_do {
@@ -1050,11 +1106,30 @@ sub revisions_eq {
 
 sub find_parent_branch {
        my ($self, $paths, $rev) = @_;
+       return undef unless $::_follow_parent;
+       unless (defined $paths) {
+               $self->ra->get_log([$self->{path}], $rev, $rev, 0, 1, 1,
+                                  sub { $paths = dup_changed_paths($_[0]) });
+       }
+       return undef unless defined $paths;
 
        # look for a parent from another branch:
-       my $i = $paths->{'/'.$self->rel_path} or return;
-       my $branch_from = $i->copyfrom_path or return;
-       my $r = $i->copyfrom_rev;
+       my @b_path_components = split m#/#, $self->rel_path;
+       my @a_path_components;
+       my $i;
+       while (@b_path_components) {
+               $i = $paths->{'/'.join('/', @b_path_components)};
+               last if $i;
+               unshift(@a_path_components, pop(@b_path_components));
+       }
+       goto not_found unless defined $i;
+       my $branch_from = $i->{copyfrom_path} or goto not_found;
+       if (@a_path_components) {
+               print STDERR "branch_from: $branch_from => ";
+               $branch_from .= '/'.join('/', @a_path_components);
+               print STDERR $branch_from, "\n";
+       }
+       my $r = $i->{copyfrom_rev};
        my $repos_root = $self->ra->{repos_root};
        my $url = $self->ra->{url};
        my $new_url = $repos_root . $branch_from;
@@ -1083,15 +1158,12 @@ sub find_parent_branch {
        }
        my ($r0, $parent) = $gs->find_rev_before($r, 1);
        if ($::_follow_parent && (!defined $r0 || !defined $parent)) {
-               foreach (0 .. $r) {
-                       my $log_entry = eval { $gs->do_fetch(undef, $_) };
-                       $gs->do_git_commit($log_entry) if $log_entry;
-               }
+               $gs->fetch(0, $r);
                ($r0, $parent) = $gs->last_rev_commit;
        }
        if (defined $r0 && defined $parent && $gs->revisions_eq($r0, $r)) {
                print STDERR "Found branch parent: ($self->{ref_id}) $parent\n";
-               command_noisy('read-tree', $parent);
+               $self->assert_index_clean($parent);
                my $ed;
                if ($self->ra->can_do_switch) {
                        print STDERR "Following parent with do_switch\n";
@@ -1112,7 +1184,21 @@ sub find_parent_branch {
                }
                return $self->make_log_entry($rev, [$parent], $ed);
        }
-       print STDERR "Branch parent not found...\n";
+not_found:
+       print STDERR "Branch parent for path: '/",
+                    $self->rel_path, "' @ r$rev not found:\n";
+       return undef unless $paths;
+       print STDERR "Changed paths:\n";
+       foreach my $x (sort keys %$paths) {
+               my $p = $paths->{$x};
+               print STDERR "\t$p->{action}\t$x";
+               if ($p->{copyfrom_path}) {
+                       print STDERR "(from $p->{copyfrom_path}: ",
+                                    "$p->{copyfrom_rev})";
+               }
+               print STDERR "\n";
+       }
+       print STDERR '-'x72, "\n";
        return undef;
 }
 
@@ -1139,50 +1225,46 @@ sub do_fetch {
        $self->make_log_entry($rev, \@parents, $ed);
 }
 
-sub write_untracked {
-       my ($self, $rev, $fh, $untracked) = @_;
-       my $h;
-       print $fh "r$rev\n" or croak $!;
-       $h = $untracked->{empty};
+sub get_untracked {
+       my ($self, $ed) = @_;
+       my @out;
+       my $h = $ed->{empty};
        foreach (sort keys %$h) {
                my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
-               print $fh "  $act: ", uri_encode($_), "\n" or croak $!;
+               push @out, "  $act: " . uri_encode($_);
                warn "W: $act: $_\n";
        }
        foreach my $t (qw/dir_prop file_prop/) {
-               $h = $untracked->{$t} or next;
+               $h = $ed->{$t} or next;
                foreach my $path (sort keys %$h) {
                        my $ppath = $path eq '' ? '.' : $path;
                        foreach my $prop (sort keys %{$h->{$path}}) {
                                next if $SKIP_PROP{$prop};
                                my $v = $h->{$path}->{$prop};
+                               my $t_ppath_prop = "$t: " .
+                                                   uri_encode($ppath) . ' ' .
+                                                   uri_encode($prop);
                                if (defined $v) {
-                                       print $fh "  +$t: ",
-                                                 uri_encode($ppath), ' ',
-                                                 uri_encode($prop), ' ',
-                                                 uri_encode($v), "\n"
-                                                 or croak $!;
+                                       push @out, "  +$t_ppath_prop " .
+                                                  uri_encode($v);
                                } else {
-                                       print $fh "  -$t: ",
-                                                 uri_encode($ppath), ' ',
-                                                 uri_encode($prop), "\n"
-                                                 or croak $!;
+                                       push @out, "  -$t_ppath_prop";
                                }
                        }
                }
        }
        foreach my $t (qw/absent_file absent_directory/) {
-               $h = $untracked->{$t} or next;
+               $h = $ed->{$t} or next;
                foreach my $parent (sort keys %$h) {
                        foreach my $path (sort @{$h->{$parent}}) {
-                               print $fh "  $t: ",
-                                     uri_encode("$parent/$path"), "\n"
-                                     or croak $!;
+                               push @out, "  $t: " .
+                                          uri_encode("$parent/$path");
                                warn "W: $t: $parent/$path ",
                                     "Insufficient permissions?\n";
                        }
                }
        }
+       \@out;
 }
 
 sub parse_svn_date {
@@ -1205,12 +1287,17 @@ sub check_author {
 }
 
 sub make_log_entry {
-       my ($self, $rev, $parents, $untracked) = @_;
-       my $rp = $self->ra->rev_proplist($rev);
-       my %log_entry = ( parents => $parents || [], revision => $rev,
-                         revprops => $rp, log => '');
+       my ($self, $rev, $parents, $ed) = @_;
+       my $untracked = $self->get_untracked($ed);
+
+       return undef if ($ed->{nr} == 0 && scalar @$untracked == 0);
+
        open my $un, '>>', "$self->{dir}/unhandled.log" or croak $!;
-       $self->write_untracked($rev, $un, $untracked);
+       print $un "r$rev\n" or croak $!;
+       print $un $_, "\n" foreach @$untracked;
+       my %log_entry = ( parents => $parents || [], revision => $rev,
+                         log => '');
+       my $rp = $self->ra->rev_proplist($rev);
        foreach (sort keys %$rp) {
                my $v = $rp->{$_};
                if (/^svn:(author|date|log)$/) {
@@ -1221,6 +1308,7 @@ sub make_log_entry {
                }
        }
        close $un or croak $!;
+
        $log_entry{date} = parse_svn_date($log_entry{date});
        $log_entry{author} = check_author($log_entry{author});
        $log_entry{log} .= "\n";
@@ -1228,9 +1316,9 @@ sub make_log_entry {
 }
 
 sub fetch {
-       my ($self, @parents) = @_;
+       my ($self, $min_rev, $max_rev, @parents) = @_;
        my ($last_rev, $last_commit) = $self->last_rev_commit;
-       my ($base, $head) = $self->parse_revision($last_rev);
+       my ($base, $head) = $self->get_fetch_range($min_rev, $max_rev);
        return if ($base > $head);
        if (defined $last_commit) {
                $self->assert_index_clean($last_commit);
@@ -1238,15 +1326,27 @@ sub fetch {
        my $inc = 1000;
        my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
        my $err_handler = $SVN::Error::handler;
-       $SVN::Error::handler = \&skip_unknown_revs;
+       my $err;
+       $SVN::Error::handler = sub { ($err) = @_; skip_unknown_revs($err); } ;
        while (1) {
                my @revs;
-               $self->ra->get_log([$self->{path}], $min, $max, 0, 1, 1, sub {
-                       my ($paths, $rev, $author, $date, $log) = @_;
-                       push @revs, [ $paths, $rev ] });
+               $self->ra->get_log([$self->{path}], $min, $max, 0, 1, 1,
+                   sub {
+                       my ($paths, $rev) = @_;
+                       push @revs, [ dup_changed_paths($paths), $rev ];
+                       });
+               if (! @revs && $err && $max >= $head) {
+                       print STDERR "Branch probably deleted:\n  ",
+                                    $err->expanded_message,
+                                    "\nWill attempt to follow revisions ",
+                                    "r$min .. r$max",
+                                    "committed before the deletion\n";
+                       @revs = map { [ undef, $_ ] } ($min .. $max);
+               }
                foreach (@revs) {
-                       my $log_entry = $self->do_fetch(@$_);
-                       $self->do_git_commit($log_entry, @parents);
+                       if (my $log_entry = $self->do_fetch(@$_)) {
+                               $self->do_git_commit($log_entry, @parents);
+                       }
                }
                last if $max >= $head;
                $min = $max + 1;
@@ -1264,7 +1364,7 @@ sub set_tree_cb {
                $log_entry->{author} = $author;
                $self->do_git_commit($log_entry, "$rev=$tree");
        } else {
-               $self->fetch("$rev=$tree");
+               $self->fetch(undef, undef, "$rev=$tree");
        }
 }
 
@@ -1277,7 +1377,7 @@ sub set_tree {
        my $pool = SVN::Pool->new;
        my $ed = SVN::Git::Editor->new({ r => $self->{last_rev},
                                         ra => $self->ra->dup,
-                                        svn_path => $self->ra->{svn_path}
+                                        svn_path => $self->{path}
                                       },
                                       $self->ra->get_commit_editor(
                                         $log_entry->{log}, sub {
@@ -1310,6 +1410,24 @@ sub skip_unknown_revs {
        croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
 }
 
+# svn_log_changed_path_t objects passed to get_log are likely to be
+# overwritten even if only the refs are copied to an external variable,
+# so we should dup the structures in their entirety.  Using an externally
+# passed pool (instead of our temporary and quickly cleared pool in
+# Git::SVN::Ra) does not help matters at all...
+sub dup_changed_paths {
+       my ($paths) = @_;
+       return undef unless $paths;
+       my %ret;
+       foreach my $p (keys %$paths) {
+               my $i = $paths->{$p};
+               my %s = map { $_ => $i->$_ }
+                             qw/copyfrom_path copyfrom_rev action/;
+               $ret{$p} = \%s;
+       }
+       \%ret;
+}
+
 # rev_db:
 # Tie::File seems to be prone to offset errors if revisions get sparse,
 # it's not that fast, either.  Tie::File is also not in Perl 5.6.  So
@@ -1568,8 +1686,7 @@ sub new {
        $self->{file_prop} = {};
        $self->{absent_dir} = {};
        $self->{absent_file} = {};
-       ($self->{gui}, $self->{ctx}) = $git_svn->tmp_index_do(
-              sub { command_input_pipe(qw/update-index -z --index-info/) } );
+       $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
        require Digest::MD5;
        $self;
 }
@@ -1596,7 +1713,6 @@ sub git_path {
 
 sub delete_entry {
        my ($self, $path, $rev, $pb) = @_;
-       my $gui = $self->{gui};
 
        my $gpath = $self->git_path($path);
        # remove entire directories.
@@ -1606,14 +1722,15 @@ sub delete_entry {
                                                     $self->{c}, '--', $gpath);
                local $/ = "\0";
                while (<$ls>) {
-                       print $gui '0 ',0 x 40,"\t",$_ or croak $!;
+                       chomp;
+                       $self->{gii}->remove($_);
                        print "\tD\t$_\n" unless $self->{q};
                }
                print "\tD\t$gpath/\n" unless $self->{q};
                command_close_pipe($ls, $ctx);
                $self->{empty}->{$path} = 0
        } else {
-               print $gui '0 ',0 x 40,"\t",$gpath,"\0" or croak $!;
+               $self->{gii}->remove($gpath);
                print "\tD\t$gpath\n" unless $self->{q};
        }
        undef;
@@ -1749,22 +1866,23 @@ sub close_file {
                $hash = $fb->{blob} or die "no blob information\n";
        }
        $fb->{pool}->clear;
-       my $gui = $self->{gui};
-       print $gui "$fb->{mode_b} $hash\t$path\0" or croak $!;
+       $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
        print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $self->{q};
        undef;
 }
 
 sub abort_edit {
        my $self = shift;
-       eval { command_close_pipe($self->{gui}, $self->{ctx}) };
+       $self->{nr} = $self->{gii}->{nr};
+       delete $self->{gii};
        $self->SUPER::abort_edit(@_);
 }
 
 sub close_edit {
        my $self = shift;
-       command_close_pipe($self->{gui}, $self->{ctx});
        $self->{git_commit_ok} = 1;
+       $self->{nr} = $self->{gii}->{nr};
+       delete $self->{gii};
        $self->SUPER::close_edit(@_);
 }
 
@@ -1787,6 +1905,8 @@ sub new {
        $self->{pool} = SVN::Pool->new;
        $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
        $self->{rm} = { };
+       $self->{path_prefix} = length $self->{svn_path} ?
+                              "$self->{svn_path}/" : '';
        require Digest::MD5;
        return $self;
 }
@@ -1796,7 +1916,8 @@ sub split_path {
 }
 
 sub repo_path {
-       (defined $_[1] && length $_[1]) ? $_[1] : ''
+       my ($self, $path) = @_;
+       $self->{path_prefix}.(defined $path ? $path : '');
 }
 
 sub url_path {
@@ -2146,7 +2267,6 @@ sub dup {
 sub get_log {
        my ($self, @args) = @_;
        my $pool = SVN::Pool->new;
-       $args[4]-- if $args[4] && ! $::_follow_parent;
        splice(@args, 3, 1) if ($SVN::Core::VERSION le '1.2.0');
        my $ret = $self->SUPER::get_log(@args, $pool);
        $pool->clear;
@@ -2191,6 +2311,19 @@ sub gs_do_switch {
        $editor->{git_commit_ok};
 }
 
+sub minimize_url {
+       my ($self) = @_;
+       return $self->{url} if ($self->{url} eq $self->{repos_root});
+       my $url = $self->{repos_root};
+       my @components = split(m!/!, $self->{svn_path});
+       my $c = '';
+       do {
+               $url .= "/$c" if length $c;
+               eval { (ref $self)->new($url)->get_latest_revnum };
+       } while ($@ && ($c = shift @components));
+       $url;
+}
+
 sub can_do_switch {
        my $self = shift;
        unless (defined $can_do_switch) {
@@ -2744,6 +2877,38 @@ sub migration_check {
        minimize_connections() if $_minimize;
 }
 
+package Git::IndexInfo;
+use strict;
+use warnings;
+use Git qw/command_input_pipe command_close_pipe/;
+
+sub new {
+       my ($class) = @_;
+       my ($gui, $ctx) = command_input_pipe(qw/update-index -z --index-info/);
+       bless { gui => $gui, ctx => $ctx, nr => 0}, $class;
+}
+
+sub remove {
+       my ($self, $path) = @_;
+       if (print { $self->{gui} } '0 ', 0 x 40, "\t", $path, "\0") {
+               return ++$self->{nr};
+       }
+       undef;
+}
+
+sub update {
+       my ($self, $mode, $hash, $path) = @_;
+       if (print { $self->{gui} } $mode, ' ', $hash, "\t", $path, "\0") {
+               return ++$self->{nr};
+       }
+       undef;
+}
+
+sub DESTROY {
+       my ($self) = @_;
+       command_close_pipe($self->{gui}, $self->{ctx});
+}
+
 __END__
 
 Data structures: