From: Joey Hess Date: Fri, 9 Oct 2009 00:33:58 +0000 (-0400) Subject: Merge branch 'master' into dependency-types X-Git-Tag: 3.20091017~27^2~60 X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/commitdiff_plain/37ec6a82433aa1d7030bca7cae9d3813a6bdbd1f?ds=sidebyside;hp=-c Merge branch 'master' into dependency-types Conflicts: IkiWiki.pm IkiWiki/Render.pm debian/changelog --- 37ec6a82433aa1d7030bca7cae9d3813a6bdbd1f diff --combined IkiWiki.pm index c735b26c8,d667e7e10..daa71059b --- a/IkiWiki.pm +++ b/IkiWiki.pm @@@ -17,28 -17,21 +17,27 @@@ use vars qw{%config %links %oldlinks %p %forcerebuild %loaded_plugins}; use Exporter q{import}; -our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match - pagespec_match_list bestlink htmllink readfile writefile - pagetype srcfile pagename displaytime will_render gettext urlto - targetpage add_underlay pagetitle titlepage linkpage - newpagefile inject add_link +our @EXPORT = qw(hook debug error template htmlpage deptype use_pagespec + add_depends pagespec_match pagespec_match_list bestlink + htmllink readfile writefile pagetype srcfile pagename + displaytime will_render gettext urlto targetpage + add_underlay pagetitle titlepage linkpage newpagefile + inject add_link %config %links %pagestate %wikistate %renderedfiles %pagesources %destsources); our $VERSION = 3.00; # plugin interface version, next is ikiwiki version our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE +# Page dependency types. +our $DEPEND_CONTENT=1; +our $DEPEND_PRESENCE=2; +our $DEPEND_LINKS=4; + # Optimisation. use Memoize; memoize("abs2rel"); memoize("pagespec_translate"); - memoize("file_pruned"); memoize("template_file"); sub getsetup () { @@@ -1530,28 -1523,18 +1529,28 @@@ sub loadindex () $links{$page}=$d->{links}; $oldlinks{$page}=[@{$d->{links}}]; } - if (exists $d->{depends_simple}) { + if (ref $d->{depends_simple} eq 'ARRAY') { + # old format $depends_simple{$page}={ map { $_ => 1 } @{$d->{depends_simple}} }; } + elsif (exists $d->{depends_simple}) { + $depends_simple{$page}=$d->{depends_simple}; + } if (exists $d->{dependslist}) { + # old format $depends{$page}={ - map { $_ => 1 } @{$d->{dependslist}} + map { $_ => $DEPEND_CONTENT } + @{$d->{dependslist}} }; } + elsif (exists $d->{depends} && ! ref $d->{depends}) { + # old format + $depends{$page}={$d->{depends} => $DEPEND_CONTENT }; + } elsif (exists $d->{depends}) { - $depends{$page}={$d->{depends} => 1}; + $depends{$page}=$d->{depends}; } if (exists $d->{state}) { $pagestate{$page}=$d->{state}; @@@ -1597,11 -1580,11 +1596,11 @@@ sub saveindex () }; if (exists $depends{$page}) { - $index{page}{$src}{dependslist} = [ keys %{$depends{$page}} ]; + $index{page}{$src}{depends} = $depends{$page}; } if (exists $depends_simple{$page}) { - $index{page}{$src}{depends_simple} = [ keys %{$depends_simple{$page}} ]; + $index{page}{$src}{depends_simple} = $depends_simple{$page}; } if (exists $pagestate{$page}) { @@@ -1769,145 -1752,35 +1768,149 @@@ sub rcs_receive () $hooks{rcs}{rcs_receive}{call}->(); } -sub add_depends ($$) { +sub add_depends ($$;$) { my $page=shift; my $pagespec=shift; + my $deptype=shift || $DEPEND_CONTENT; + # Is the pagespec a simple page name? if ($pagespec =~ /$config{wiki_file_regexp}/ && - $pagespec !~ /[\s*?()!]/) { - # a simple dependency, which can be matched by string eq - $depends_simple{$page}{lc $pagespec} = 1; + $pagespec !~ /[\s*?()!]/) { + $depends_simple{$page}{lc $pagespec} |= $deptype; return 1; } - return unless pagespec_valid($pagespec); - - $depends{$page}{$pagespec} = 1; + # Analyse the pagespec, and match it against all pages + # to get a list of influences, and add explicit dependencies + # for those. + #my $sub=pagespec_translate($pagespec); + #return if $@; + #foreach my $p (keys %pagesources) { + # my $r=$sub->($p, location => $page ); + # my %i=$r->influences; + # foreach my $i (keys %i) { + # $depends_simple{$page}{lc $i} |= $i{$i}; + # } + #} + print STDERR "warning: use of add_depends; influences not tracked\n"; + + $depends{$page}{$pagespec} |= $deptype; return 1; } +sub use_pagespec ($$;@) { + my $page=shift; + my $pagespec=shift; + my %params=@_; + + my $sub=pagespec_translate($pagespec); + error "syntax error in pagespec \"$pagespec\"" + if $@ || ! defined $sub; + + my @candidates; + if (exists $params{limit}) { + @candidates=grep { $params{limit}->($_) } keys %pagesources; + } + else { + @candidates=keys %pagesources; + } + + if (defined $params{sort}) { + my $f; + if ($params{sort} eq 'title') { + $f=sub { pagetitle(basename($a)) cmp pagetitle(basename($b)) }; + } + elsif ($params{sort} eq 'title_natural') { + eval q{use Sort::Naturally}; + if ($@) { + error(gettext("Sort::Naturally needed for title_natural sort")); + } + $f=sub { Sort::Naturally::ncmp(pagetitle(basename($a)), pagetitle(basename($b))) }; + } + elsif ($params{sort} eq 'mtime') { + $f=sub { $pagemtime{$b} <=> $pagemtime{$a} }; + } + elsif ($params{sort} eq 'age') { + $f=sub { $pagectime{$b} <=> $pagectime{$a} }; + } + else { + error sprintf(gettext("unknown sort type %s"), $params{sort}); + } + @candidates = sort { &$f } @candidates; + } + + @candidates=reverse(@candidates) if $params{reverse}; + + my @matches; + my $firstfail; + my $count=0; + foreach my $p (@candidates) { + my $r=$sub->($p, location => $page); + if ($r) { + push @matches, [$p, $r]; + last if defined $params{num} && ++$count == $params{num}; + } + elsif (! defined $firstfail) { + $firstfail=$r; + } + } + + $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT); + + my @ret; + if (@matches) { + # Add all influences from successful matches. + foreach my $m (@matches) { + push @ret, $m->[0]; + my %i=$m->[1]->influences; + foreach my $i (keys %i) { + $depends_simple{$page}{lc $i} |= $i{$i}; + } + } + } + elsif (defined $firstfail) { + # Add influences from one failure. (Which one should not + # matter; all should have the same influences.) + my %i=$firstfail->influences; + foreach my $i (keys %i) { + $depends_simple{$page}{lc $i} |= $i{$i}; + } + + error(sprintf(gettext("cannot match pages: %s"), $firstfail)) + if $firstfail->isa("IkiWiki::ErrorReason"); + } + + return @ret; +} + +sub deptype (@) { + my $deptype=0; + foreach my $type (@_) { + if ($type eq 'presence') { + $deptype |= $DEPEND_PRESENCE; + } + elsif ($type eq 'links') { + $deptype |= $DEPEND_LINKS; + } + elsif ($type eq 'content') { + $deptype |= $DEPEND_CONTENT; + } + } + return $deptype; +} + - sub file_pruned ($$) { - require File::Spec; - my $file=File::Spec->canonpath(shift); - my $base=File::Spec->canonpath(shift); - $file =~ s#^\Q$base\E/+##; + sub file_pruned ($;$) { + my $file=shift; + if (@_) { + require File::Spec; + $file=File::Spec->canonpath($file); + my $base=File::Spec->canonpath(shift); + return if $file eq $base; + $file =~ s#^\Q$base\E/+##; + } my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')'; - return $file =~ m/$regexp/ && $file ne $base; + return $file =~ m/$regexp/; } sub define_gettext () { @@@ -2003,10 -1876,10 +2006,10 @@@ sub pagespec_translate ($) }gx) { my $word=$1; if (lc $word eq 'and') { - $code.=' &&'; + $code.=' &'; } elsif (lc $word eq 'or') { - $code.=' ||'; + $code.=' |'; } elsif ($word eq "(" || $word eq ")" || $word eq "!") { $code.=' '.$word; @@@ -2092,54 -1965,36 +2095,54 @@@ sub glob2re ($) package IkiWiki::FailReason; use overload ( - '""' => sub { ${$_[0]} }, + '""' => sub { $_[0][0] }, '0+' => sub { 0 }, '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'}, + '&' => sub { $_[0]->merge_influences($_[1]); $_[0] }, + '|' => sub { $_[1]->merge_influences($_[0]); $_[1] }, fallback => 1, ); -sub new { - my $class = shift; - my $value = shift; - return bless \$value, $class; -} - -package IkiWiki::ErrorReason; - -our @ISA = 'IkiWiki::FailReason'; +our @ISA = 'IkiWiki::SuccessReason'; package IkiWiki::SuccessReason; use overload ( - '""' => sub { ${$_[0]} }, + '""' => sub { $_[0][0] }, '0+' => sub { 1 }, '!' => sub { bless $_[0], 'IkiWiki::FailReason'}, + '&' => sub { $_[1]->merge_influences($_[0]); $_[1] }, + '|' => sub { $_[0]->merge_influences($_[1]); $_[0] }, fallback => 1, ); sub new { my $class = shift; my $value = shift; - return bless \$value, $class; -}; + return bless [$value, {@_}], $class; +} + +sub influences { + my $this=shift; + if (! @_) { + return %{$this->[1]}; + } + else { + $this->[1]={@_}; + } +} + +sub merge_influences { + my $this=shift; + my $other=shift; + foreach my $influence (keys %{$other->[1]}) { + $this->[1]{$influence} |= $other->[1]{$influence}; + } +} + +package IkiWiki::ErrorReason; + +our @ISA = 'IkiWiki::FailReason'; package IkiWiki::PageSpec; @@@ -2190,20 -2045,19 +2193,20 @@@ sub match_link ($$;@) my $from=exists $params{location} ? $params{location} : ''; my $links = $IkiWiki::links{$page}; - return IkiWiki::FailReason->new("$page has no links") unless $links && @{$links}; + return IkiWiki::FailReason->new("$page has no links") + unless $links && @{$links}; my $bestlink = IkiWiki::bestlink($from, $link); foreach my $p (@{$links}) { if (length $bestlink) { - return IkiWiki::SuccessReason->new("$page links to $link") + return IkiWiki::SuccessReason->new("$page links to $link", $page => $IkiWiki::DEPEND_LINKS) if $bestlink eq IkiWiki::bestlink($page, $p); } else { - return IkiWiki::SuccessReason->new("$page links to page $p matching $link") + return IkiWiki::SuccessReason->new("$page links to page $p matching $link", $page => $IkiWiki::DEPEND_LINKS) if match_glob($p, $link, %params); my ($p_rel)=$p=~/^\/?(.*)/; $link=~s/^\///; - return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link") + return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link", $page => $IkiWiki::DEPEND_LINKS) if match_glob($p_rel, $link, %params); } } @@@ -2211,9 -2065,7 +2214,9 @@@ } sub match_backlink ($$;@) { - return match_link($_[1], $_[0], @_); + my $ret=match_link($_[1], $_[0], @_); + $ret->influences($_[1] => $IkiWiki::DEPEND_LINKS); + return $ret; } sub match_created_before ($$;@) { @@@ -2225,14 -2077,14 +2228,14 @@@ if (exists $IkiWiki::pagectime{$testpage}) { if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) { - return IkiWiki::SuccessReason->new("$page created before $testpage"); + return IkiWiki::SuccessReason->new("$page created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } else { - return IkiWiki::FailReason->new("$page not created before $testpage"); + return IkiWiki::FailReason->new("$page not created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } } else { - return IkiWiki::ErrorReason->new("$testpage does not exist"); + return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE); } } @@@ -2245,14 -2097,14 +2248,14 @@@ sub match_created_after ($$;@) if (exists $IkiWiki::pagectime{$testpage}) { if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) { - return IkiWiki::SuccessReason->new("$page created after $testpage"); + return IkiWiki::SuccessReason->new("$page created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } else { - return IkiWiki::FailReason->new("$page not created after $testpage"); + return IkiWiki::FailReason->new("$page not created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } } else { - return IkiWiki::ErrorReason->new("$testpage does not exist"); + return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE); } } diff --combined IkiWiki/Render.pm index 79935f323,a8236b954..0fe20c64f --- a/IkiWiki/Render.pm +++ b/IkiWiki/Render.pm @@@ -7,7 -7,7 +7,7 @@@ use strict use IkiWiki; use Encode; -my %backlinks; +my (%backlinks, %rendered); our %brokenlinks; my $links_calculated=0; @@@ -147,8 -147,6 +147,8 @@@ sub genpage ($$) sub scan ($) { my $file=shift; + debug(sprintf(gettext("scanning %s"), $file)); + my $type=pagetype($file); if (defined $type) { my $srcfile=srcfile($file); @@@ -204,11 -202,8 +204,11 @@@ sub fast_file_copy (@) } } -sub render ($) { +sub render ($$) { my $file=shift; + return if $rendered{$file}; + debug(shift); + $rendered{$file}=1; my $type=pagetype($file); my $srcfile=srcfile($file); @@@ -278,31 -273,32 +278,33 @@@ sub srcdir_check () } sub find_src_files () { - my (@files, %pages); + my @files; + my %pages; eval q{use File::Find}; error($@) if $@; find({ no_chdir => 1, wanted => sub { - $_=decode_utf8($_); - if (file_pruned($_, $config{srcdir})) { + my $file=decode_utf8($_); + $file=~s/^\Q$config{srcdir}\E\/?//; + my $page = pagename($file); + if (! exists $pagesources{$page} && + file_pruned($file)) { $File::Find::prune=1; + return; } - elsif (! -l $_ && ! -d _) { - my ($f)=/$config{wiki_file_regexp}/; # untaint - if (! defined $f) { - warn(sprintf(gettext("skipping bad filename %s"), $_)."\n"); - } - else { - $f=~s/^\Q$config{srcdir}\E\/?//; - push @files, $f; - my $page = pagename($f); - if ($pages{$page}) { - debug(sprintf(gettext("%s has multiple possible source pages"), $page)); - } - $pages{$page}=1; + return if -l $_ || -d _ || ! length $file; + + my ($f) = $file =~ /$config{wiki_file_regexp}/; # untaint + if (! defined $f) { + warn(sprintf(gettext("skipping bad filename %s"), $file)."\n"); + } + else { + push @files, $f; + if ($pages{$page}) { + debug(sprintf(gettext("%s has multiple possible source pages"), $page)); } + $pages{$page}=1; } }, }, $config{srcdir}); @@@ -310,41 -306,47 +312,42 @@@ find({ no_chdir => 1, wanted => sub { - $_=decode_utf8($_); - if (file_pruned($_, $dir)) { + my $file=decode_utf8($_); + $file=~s/^\Q$dir\E\/?//; + my $page=pagename($file); + if (! exists $pagesources{$page} && + file_pruned($file)) { $File::Find::prune=1; + return; } - elsif (! -l $_ && ! -d _) { - my ($f)=/$config{wiki_file_regexp}/; # untaint - if (! defined $f) { - warn(sprintf(gettext("skipping bad filename %s"), $_)."\n"); - } - else { - $f=~s/^\Q$dir\E\/?//; - # avoid underlaydir - # override attacks; see - # security.mdwn - if (! -l "$config{srcdir}/$f" && - ! -e _) { - my $page=pagename($f); - if (! $pages{$page}) { - push @files, $f; - $pages{$page}=1; - } + return if -l $_ || -d _ || ! length $file; + + my ($f) = $file =~ /$config{wiki_file_regexp}/; # untaint + if (! defined $f) { + warn(sprintf(gettext("skipping bad filename %s"), $file)."\n"); + } + else { + # avoid underlaydir override + # attacks; see security.mdwn + if (! -l "$config{srcdir}/$f" && + ! -e _) { + if (! $pages{$page}) { + push @files, $f; + $pages{$page}=1; } } } }, }, $dir); }; - - # Returns a list of all source files found, and a hash of - # the corresponding page names. return \@files, \%pages; } -sub refresh () { - srcdir_check(); - run_hooks(refresh => sub { shift->() }); - my ($files, $exists)=find_src_files(); +sub find_new_files ($) { + my $files=shift; + my @new; + my @internal_new; - my (%rendered, @add, @del, @internal); - # check for added or removed pages foreach my $file (@$files) { my $page=pagename($file); if (exists $pagesources{$page} && $pagesources{$page} ne $file) { @@@ -354,10 -356,10 +357,10 @@@ $pagesources{$page}=$file; if (! $pagemtime{$page}) { if (isinternal($page)) { - push @internal, $file; + push @internal_new, $file; } else { - push @add, $file; + push @new, $file; if ($config{getctime} && -e "$config{srcdir}/$file") { eval { my $time=rcs_getctime("$config{srcdir}/$file"); @@@ -374,19 -376,10 +377,19 @@@ } } } + + return \@new, \@internal_new; +} + +sub find_del_files ($) { + my $pages=shift; + my @del; + my @internal_del; + foreach my $page (keys %pagemtime) { - if (! $exists->{$page}) { + if (! $pages->{$page}) { if (isinternal($page)) { - push @internal, $pagesources{$page}; + push @internal_del, $pagesources{$page}; } else { debug(sprintf(gettext("removing old page %s"), $page)); @@@ -407,13 -400,8 +410,13 @@@ } } - # find changed and new files - my @needsbuild; + return \@del, \@internal_del; +} + +sub find_changed ($) { + my $files=shift; + my @changed; + my @internal_changed; foreach my $file (@$files) { my $page=pagename($file); my ($srcfile, @stat)=srcfile_stat($file); @@@ -421,242 -409,150 +424,242 @@@ $stat[9] > $pagemtime{$page} || $forcerebuild{$page}) { $pagemtime{$page}=$stat[9]; + if (isinternal($page)) { - push @internal, $file; # Preprocess internal page in scan-only mode. preprocess($page, $page, readfile($srcfile), 1); + push @internal_changed, $file; } else { - push @needsbuild, $file; + push @changed, $file; } } } - run_hooks(needsbuild => sub { shift->(\@needsbuild) }); + return \@changed, \@internal_changed; +} - # scan and render files - foreach my $file (@needsbuild) { - debug(sprintf(gettext("scanning %s"), $file)); - scan($file); - } - calculate_links(); - foreach my $file (@needsbuild) { - debug(sprintf(gettext("building %s"), $file)); - render($file); - $rendered{$file}=1; - } - foreach my $file (@internal) { - # internal pages are not rendered +sub calculate_old_links ($$) { + my ($changed, $del)=@_; + my %oldlink_targets; + foreach my $file (@$changed, @$del) { my $page=pagename($file); - delete $depends{$page}; - delete $depends_simple{$page}; - foreach my $old (@{$renderedfiles{$page}}) { - delete $destsources{$old}; + if (exists $oldlinks{$page}) { + foreach my $l (@{$oldlinks{$page}}) { + $oldlink_targets{$page}{$l}=bestlink($page, $l); + } } - $renderedfiles{$page}=[]; } - - # rebuild pages that link to added or removed pages - if (@add || @del) { - foreach my $f (@add, @del) { - my $p=pagename($f); - foreach my $page (keys %{$backlinks{$p}}) { - my $file=$pagesources{$page}; - next if $rendered{$file}; - debug(sprintf(gettext("building %s, which links to %s"), $file, $p)); - render($file); - $rendered{$file}=1; + return \%oldlink_targets; +} + +sub derender_internal ($) { + my $file=shift; + my $page=pagename($file); + delete $depends{$page}; + delete $depends_simple{$page}; + foreach my $old (@{$renderedfiles{$page}}) { + delete $destsources{$old}; + } + $renderedfiles{$page}=[]; +} + +sub render_linkers ($) { + my $f=shift; + my $p=pagename($f); + foreach my $page (keys %{$backlinks{$p}}) { + my $file=$pagesources{$page}; + render($file, sprintf(gettext("building %s, which links to %s"), $file, $p)); + } +} + +sub remove_unrendered () { + foreach my $src (keys %rendered) { + my $page=pagename($src); + foreach my $file (@{$oldrenderedfiles{$page}}) { + if (! grep { $_ eq $file } @{$renderedfiles{$page}}) { + debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page)); + prune($config{destdir}."/".$file); } } } +} - if (%rendered || @del || @internal) { - my @changed=(keys %rendered, @del); +sub calculate_changed_links ($$$) { + my ($changed, $del, $oldlink_targets)=@_; - my %lcchanged = map { lc(pagename($_)) => 1 } @changed; - - # rebuild dependant pages - foreach my $f (@$files) { - next if $rendered{$f}; - my $p=pagename($f); - my $reason = undef; + my (%backlinkchanged, %linkchangers); - if (exists $depends_simple{$p}) { - foreach my $d (keys %{$depends_simple{$p}}) { - if (exists $lcchanged{$d}) { - $reason = $d; - last; - } + foreach my $file (@$changed, @$del) { + my $page=pagename($file); + + if (exists $links{$page}) { + foreach my $l (@{$links{$page}}) { + my $target=bestlink($page, $l); + if (! exists $oldlink_targets->{$page}{$l} || + $target ne $oldlink_targets->{$page}{$l}) { + $backlinkchanged{$target}=1; + $linkchangers{lc($page)}=1; } + delete $oldlink_targets->{$page}{$l}; } + } + if (exists $oldlink_targets->{$page} && + %{$oldlink_targets->{$page}}) { + foreach my $target (values %{$oldlink_targets->{$page}}) { + $backlinkchanged{$target}=1; + } + $linkchangers{lc($page)}=1; + } + } - if (exists $depends{$p} && ! defined $reason) { - D: foreach my $d (keys %{$depends{$p}}) { - my $sub=pagespec_translate($d); - next if $@ || ! defined $sub; + return \%backlinkchanged, \%linkchangers; +} - # only consider internal files - # if the page explicitly depends - # on such files - foreach my $file (@changed, $d =~ /internal\(/ ? @internal : ()) { +sub render_dependent ($$$$$$$) { + my ($files, $new, $internal_new, $del, $internal_del, + $internal_changed, $linkchangers)=@_; + + my @changed=(keys %rendered, @$del); + my @exists_changed=(@$new, @$del); + + my %lc_changed = map { lc(pagename($_)) => 1 } @changed; + my %lc_exists_changed = map { lc(pagename($_)) => 1 } @exists_changed; + + foreach my $f (@$files) { + next if $rendered{$f}; + my $p=pagename($f); + my $reason = undef; + + if (exists $depends_simple{$p}) { + foreach my $d (keys %{$depends_simple{$p}}) { + if (($depends_simple{$p}{$d} & $IkiWiki::DEPEND_CONTENT && + $lc_changed{$d}) + || + ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_PRESENCE && + $lc_exists_changed{$d}) + || + ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_LINKS && + $linkchangers->{$d}) + ) { + $reason = $d; + last; + } + } + } + + if (exists $depends{$p} && ! defined $reason) { + foreach my $dep (keys %{$depends{$p}}) { + my $sub=pagespec_translate($dep); + next if $@ || ! defined $sub; + + # only consider internal files + # if the page explicitly depends + # on such files + my $internal_dep=$dep =~ /internal\(/; + + my $in=sub { + my $list=shift; + my $type=shift; + foreach my $file (@$list) { next if $file eq $f; my $page=pagename($file); if ($sub->($page, location => $p)) { - $reason = $page; - last D; + if ($type == $IkiWiki::DEPEND_LINKS) { + next unless $linkchangers->{lc($page)}; + } + return $page; } } + return undef; + }; + + if ($depends{$p}{$dep} & $IkiWiki::DEPEND_CONTENT) { + last if $reason = + $in->(\@changed, $IkiWiki::DEPEND_CONTENT); + last if $internal_dep && ($reason = + $in->($internal_new, $IkiWiki::DEPEND_CONTENT) || + $in->($internal_del, $IkiWiki::DEPEND_CONTENT) || + $in->($internal_changed, $IkiWiki::DEPEND_CONTENT)); } - } - - if (defined $reason) { - debug(sprintf(gettext("building %s, which depends on %s"), $f, $reason)); - render($f); - $rendered{$f}=1; - } - } - - # handle backlinks; if a page has added/removed links, - # update the pages it links to - my %linkchanged; - foreach my $file (@changed) { - my $page=pagename($file); - - if (exists $links{$page}) { - foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) { - if (length $link && - (! exists $oldlinks{$page} || - ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) { - $linkchanged{$link}=1; - } + if ($depends{$p}{$dep} & $IkiWiki::DEPEND_PRESENCE) { + last if $reason = + $in->(\@exists_changed, $IkiWiki::DEPEND_PRESENCE); + last if $internal_dep && ($reason = + $in->($internal_new, $IkiWiki::DEPEND_PRESENCE) || + $in->($internal_del, $IkiWiki::DEPEND_PRESENCE)); } - } - if (exists $oldlinks{$page}) { - foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) { - if (length $link && - (! exists $links{$page} || - ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) { - $linkchanged{$link}=1; - } + if ($depends{$p}{$dep} & $IkiWiki::DEPEND_LINKS) { + last if $reason = + $in->(\@changed, $IkiWiki::DEPEND_LINKS); + last if $internal_dep && ($reason = + $in->($internal_new, $IkiWiki::DEPEND_LINKS) || + $in->($internal_del, $IkiWiki::DEPEND_LINKS) || + $in->($internal_changed, $IkiWiki::DEPEND_LINKS)); } } } - - foreach my $link (keys %linkchanged) { - my $linkfile=$pagesources{$link}; - if (defined $linkfile) { - next if $rendered{$linkfile}; - debug(sprintf(gettext("building %s, to update its backlinks"), $linkfile)); - render($linkfile); - $rendered{$linkfile}=1; - } + + if (defined $reason) { + render($f, sprintf(gettext("building %s, which depends on %s"), $f, $reason)); + return 1; } } - # remove no longer rendered files - foreach my $src (keys %rendered) { - my $page=pagename($src); - foreach my $file (@{$oldrenderedfiles{$page}}) { - if (! grep { $_ eq $file } @{$renderedfiles{$page}}) { - debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page)); - prune($config{destdir}."/".$file); - } + return 0; +} + +sub render_backlinks ($) { + my $backlinkchanged=shift; + foreach my $link (keys %$backlinkchanged) { + my $linkfile=$pagesources{$link}; + if (defined $linkfile) { + render($linkfile, sprintf(gettext("building %s, to update its backlinks"), $linkfile)); } } +} + +sub refresh () { + srcdir_check(); + run_hooks(refresh => sub { shift->() }); + my ($files, $pages)=find_src_files(); + my ($new, $internal_new)=find_new_files($files); + my ($del, $internal_del)=find_del_files($pages); + my ($changed, $internal_changed)=find_changed($files); + run_hooks(needsbuild => sub { shift->($changed) }); + my $oldlink_targets=calculate_old_links($changed, $del); + + foreach my $file (@$changed) { + scan($file); + } + + calculate_links(); + + foreach my $file (@$changed) { + render($file, sprintf(gettext("building %s"), $file)); + } + foreach my $file (@$internal_new, @$internal_del, @$internal_changed) { + derender_internal($file); + } + + my ($backlinkchanged, $linkchangers)=calculate_changed_links($changed, + $del, $oldlink_targets); + + foreach my $file (@$new, @$del) { + render_linkers($file); + } + + if (@$changed || @$internal_changed || + @$del || @$internal_del || @$internal_new) { + 1 while render_dependent($files, $new, $internal_new, + $del, $internal_del, $internal_changed, + $linkchangers); + } + + render_backlinks($backlinkchanged); + remove_unrendered(); - if (@del) { - run_hooks(delete => sub { shift->(@del) }); + if (@$del) { + run_hooks(delete => sub { shift->(@$del) }); } if (%rendered) { run_hooks(change => sub { shift->(keys %rendered) }); diff --combined debian/changelog index 12ddebac9,6c4353065..3a6fdf77d --- a/debian/changelog +++ b/debian/changelog @@@ -10,32 -10,8 +10,34 @@@ ikiwiki (3.14159266) UNRELEASED; urgenc * mirrorlist: Display nothing if list is empty. * Fix a bug that could lead to duplicate links being recorded for tags. + * Added support framework for multiple types of dependencies. + * Allow declaring that a dependency is only affected by page presence + or changes to its links. + * pagecount, calendar, postsparkline, progress: Use a presence dependency, + which makes these directives much less expensive to use, since page + edits will no longer trigger an unnecessary update. + * map: Use a presence dependency unless show= is specified. + This makes maps efficient enough that they can be used on sidebars! + * inline: Use a presence dependency in quick mode. + * brokenlinks: Use a link dependency. + This makes it much more efficient, only updating when really necessary. + * orphans, pagestats: Use a combination of presence and link dependencies. + This makes them more efficient. It also fixes a longstanding bug, + where if only a small set of pages were considered by orphans/pagestats, + changes to links on other pages failed to cause an update. + * linkmap: Use a combination of presence and link dependencies. + This makes the map be regenerated much less frequently in many cases, + so larger maps are more practical to use now. + * Transitive dependencies are now correctly supported. + * Rebuild wikis on upgrade to this version to get improved dependency + info. + * Plugins providing PageSpec `match_*` functions should pass additional + influence information when creating result objects. + * Added `use_pagespec` function, that plugins can use to find a list + of matching pages and add dependencies and influences, all at once, + and efficiently. + * Optimize away most expensive file prune calls, when refreshing, + by only checking new files. -- Joey Hess Sun, 27 Sep 2009 17:40:03 -0400 diff --combined doc/todo/dependency_types.mdwn index 479cc95cc,f06603874..d9e68841d --- a/doc/todo/dependency_types.mdwn +++ b/doc/todo/dependency_types.mdwn @@@ -222,7 -222,7 +222,7 @@@ ShavedByBob.mdwn Does ShavedByBob.mdwn include itself? - (Yeah - in IkiWiki currently links are included by include, but the idea holds. I had a good example a while back, but I can't think of it right now.) + (Yeah - in IkiWiki currently links are *not* included by include, but the idea holds. I had a good example a while back, but I can't think of it right now.) sigh. @@@ -232,6 -232,36 +232,36 @@@ > to determine what metadata, pages, etc they depend on. It is indeed > tricky to do. More thoughts on influence lists a bit below. --[[Joey]] + >> The big part of what makes this tricky is that there may be cycles in the + >> dependency graph. This can lead to situations where the result is just not + >> well defined. This is what I was trying to get at above. -- [[Will]] + + >>> Hmm, I'm not seeing cycles be a problem, at least with the current + >>> pagespec terms. --[[Joey]] + + >>>> Oh, they're not with current pagespec terms. But this is really close to extending to handle + >>>> functional pagespecs, etc. And I think I'd like to think about that now. + >>>> + >>>> Having said that, I don't want to hold you up - you seem to be making progress. The best is + >>>> the enemy of the good, etc. etc. + >>>> + >>>> For my part, I'm imagining we have two more constructs in IkiWiki: + >>>> + >>>> * A map directive that actually wikilinks to the pages it links to, and + >>>> * A `match_sharedLink(pageX)` matching function that matches pageY if both pageX and pageY each have links to any same third page, pageZ. + >>>> + >>>> With those two constructs, one page changing might change the set of pages included in a map somewhere, which might then change the set of pages matched by some other pagespec, which might then... + >>>> + >>>> --[[Will]] + + >>>>> I think that should be supported by [[bugs/transitive_dependencies]]. + >>>>> At least in the current implementation, which considers each page + >>>>> that is rendered to be changed, and rebuilds pages that are dependent + >>>>> on it, in a loop. An alternate implementation, which could be faster, + >>>>> is to construct a directed graph and traverse it just once. Sounds + >>>>> like that would probably not support what you want to do. + >>>>> --[[Joey]] + ---- ### Link dependencies @@@ -248,7 -278,6 +278,7 @@@ that the page links to, which is just what link dependencies are triggered on. +[[done]] ---- ### the removal problem @@@ -273,7 -302,7 +303,7 @@@ One way to fix this is to include with that currently match it. If the list changes, the dependency is triggered. Should be doable, but may involve more work than - currently. Consider that a dependency on "bugs/*" currently + currently. Consider that a dependency on `bugs/*` currently is triggered by just checking until *one* page is found to match it. But to store the list, *every* page would have to be tried against it. Unless the list can somehow be intelligently updated, looking at only the @@@ -305,10 -334,53 +335,53 @@@ changes, is needed I'm using this term for the concept of a list of pages whose modification can indirectly influence what pages a pagespec matches. + > Trying to make a formal definition of this: (Note, I'm using the term sets rather than lists, but they're roughly equivalent) + > + > * Let the *matching set* for a pagespec be the set of existing pages that the pagespec matches. + > * Let a *influence set* for a pagespec be the set of all pages, *p*, whose alteration might: + > * cause the pagespec to include or exclude a page other than *p*, or + > * cause the pagespec to exclude *p*. + > + >> \[Will snipped some stuff and edited the formal definition] + > + > --[[Will]] + + >> I appreciate the formalism! + >> + >> Only existing pages need to be in these sets, because if a page is added + >> in the future, the existing dependency code will always test to see + >> if it matches. So it will be in the maching set (or not) at that point. + >> + >>> Hrm, I agree with you in general, but I think I can come up with nasty counter-examples. What about a pagespec + >>> of "!backlink(bogus)" where the page bogus doesn't exist? In this case, the page 'bogus' needs to be in the influence + >>> set even though it doesn't exist. + >>> + >>>> I think you're right, this is a case that the current code is not + >>>> handling. Actually, I made all the pagespecs return influences + >>>> even if the influence was not present or did not match. But, it + >>>> currently only records influences as dependencies when a pagespec + >>>> successfully matches. Now I'm sure that is wrong, and I've removed + >>>> that false optimisation. I've updated some of the below. --[[Joey]] + >>> + >>> Also, I would really like the formalism to include the whole dependency system, not just any additions to it. That will make + >>> the whole thing much easier to reason about. + >> + >> The problem with your definition of direct influence set seems to be + >> that it doesn't allow `link()` and `title()` to have as an indirect + >> influence, the page that matches. But I'm quite sure we need those. + >> --[[Joey]] + + >>> I see what you mean. Does the revised definition capture this effectively? + >>> The problem with this revised definition is that it still doesn't match your examples below. + >>> My revised definition will include pretty much all currently matching pages to be in the influence list + >>> because deletion of any of them would cause a change in which pages are matched - the removal problem. + >>> -- [[Will]] + #### Examples * The pagespec "created_before(foo)" has an influence list that contains foo. - The removal or (re)creation of foo changes what pages match it. + The removal or (re)creation of foo changes what pages match it. Note that + this is true even if the pagespec currently fails to match. * The pagespec "foo" has an empty influence list. This is because a modification/creation/removal of foo directly changes what the pagespec @@@ -318,20 -390,44 +391,44 @@@ Avoiding including every page in the wiki into its influence list is very important! + >>> So, why don't the above influence lists contain the currently matched pages? + >>> Don't you need this to handle the removal problem? -- [[Will]] + + >>>> The removal problem is slightly confusingly named, since it does not + >>>> affect pages that were matched by a glob and have been removed. Such + >>>> pages can be handled without being influences, because ikiwiki knows + >>>> they have been removed, and so can still match them against the + >>>> pagespec, and see they used to match; and thus knows that the + >>>> dependency has triggered. + >>>> + >>>> Maybe the thing to do is consider this an optimisation, where such + >>>> pages are influences, but ikiwiki is able to implicitly find them, + >>>> so they do not need to be explicitly stored. --[[Joey]] + * The pagespec "title(foo)" has an influence list that contains every page that currently matches it. A change to any matching page can change its - title. Why is that considered an indirect influence? Well, the pagespec - might be used in a presence dependency, and so its title changing - would not directly affect the dependency. + title, making it not match any more, and so the list is needed due to the + removal problem. A page that does not have a matching title is not an + influence, because modifying the page to change its title directly + changes what the pagespec matches. * The pagespec "backlink(index)" has an influence list that contains index (because a change to index changes the backlinks). + Note that this is true even if the backlink currently fails. * The pagespec "link(done)" has an influence list that contains every page that it matches. A change to any matching page can remove a link and make it not match any more, and so the list is needed due to the removal problem. + >> Why doesn't this include every page? If I change a page that doesn't have a link to + >> 'done' to include a link to 'done', then it will now match... or is that considered a + >> 'direct match'? -- [[Will]] + + >>> The regular dependency calculation code will check if every changed + >>> page matches every dependency. So it will notice the link was added. + >>> --[[Joey]] + #### Low-level Calculation One way to calculate a pagespec's influence would be to @@@ -379,17 -475,89 +476,89 @@@ Given that, the `backlink` will always onto the influence list. If we combine the influences from each successful match, we get the right result. - > This is implemented, seems to work ok. --[[Joey]] + > This is implemented, seems to work ok. --[[Joey]] - #### High-level Calculation and Storage + > `or` short-circuits too, but the implementation correctly uses `|`, + > which I assume is what you meant. --[[smcv]] - Calculating the full influence list for a pagespec requires trying to match - it against every page in the wiki. + >> Er, yeah. --[[Joey]] - I'd like to avoid doing such expensive matching redundantly. So add a - `pagespec_match_all`, which returns a list of all pages in the whole - wiki that match the pagespec, and also adds the pagespec as a dependency, - and while it's at it, calculates and stores the influence list. + ---- + + What about: "!link(done)" + + Specifically, I want to make sure it works now that I've changed + `match_link` to only return a page as an influence if it *does* + link to done. + + So, when matching against page P, that does not link to done, + there are no influences, and the pagespec matches. If P is later + changed to add a link to done, then the dependency resolver will directly + notice that. + + When matching against page P, that does link to done, P + is an influence, and the pagespec does not match. If P is later changed + to not link to done, the influence will do its job. + + Looks good! + + ---- + + Here is a case where this approach has some false positives. + + "bugs/* and link(patch)" + + This finds as influences all pages that link to patch, even + if they are not under bugs/, and so can never match. + + To fix this, the influence calculation would need to consider boolean + operators. Currently, this turns into roughly: + + `FailReason() & SuccessReason(patch)` + + Let's say that the glob instead returns a HardFailReason, which when + ANDed with another object, drops their influences. (But when ORed, combines + them.) Fixes the above, but does it always work? + + "(bugs/* or link(patch)) and backlink(index)" => + `( HardFailReason() | SuccessReason(page) ) & SuccessReason(index)`` => + `SuccessReason(page & SuccessReason(index)` => + SuccessReason(page, index) => right + + "(bugs/* and link(patch)) or backlink(index)" => + `( HardFailReason() & SuccessReason(page) ) | SuccessReason(index)`` => + `HardFailReason() | SuccessReason(index)` => + `SuccessReason(index)` => right + + "!bugs/* and link(patch)" => + `HardFailReason() | SuccessReason(bugs/foo)` => + `HardFailReason()` => right + + #### High-level Calculation and Storage + + Naively calculating the full influence list for a pagespec requires trying + to match it against every page in the wiki. I'd like to avoid doing such + expensive matching redundantly. + + It may be possible, for some types of pagespecs, to just try matching a + single, arbitrary page against it, and know the full influence list has + been obtained. It seems to be that case that if a pagespec has any + influences, matching any page will return at least one. So if none are + returned, we can skip trying other pages. + + If the influence list does not include the page that was tried, we know + that the pagespec does not things like `link()` and `title()`, that are + influenced by the page's own content. So it *might* be safe to not try + matching any more pages in this case too. I think it would work for all + current pagespec terms. There might be a hypothetical term where this + optimisation doesn't work. We could add a special case to ensure it can + work: If a term declares it is unfluenced by "", then it means it is + always influenced by the matching page. + + Anyway, this seems worth doing: Add a `pagespec_match_all`, which returns a + list of all pages in the whole wiki that match the pagespec, and also adds + the pagespec as a dependency, and while it's at it, calculates and stores + the influence list. It could have an optional sort parameter, and limit parameter, to control how many items to return and the sort order. So when inline wants to @@@ -414,7 -582,7 +583,7 @@@ it's calculated more smartly, and is ad > I've implemented influence calculation in `add_depends`. As expected, > it means rather a lot more work, and makes some things much slower. - > Optimisation via `pagespec_match_depends` next.. --[[Joey]] + > Optimisations next.. --[[Joey]] #### Influence types @@@ -422,3 -590,10 +591,10 @@@ Note that influences can also have type For example, "backlink(foo)" has an influence of foo, of type links. "created_before(foo)" also is influenced by foo, but it's a presence type. Etc. + + > This is an interesting concept that I hadn't considered. It might + > allow significant computational savings, but I suspect will be tricky + > to implement. -- [[Will]] + + >> It was actually really easy to implement it, assuming I picked the right + >> dependency types of course. --[[Joey]]