# from [2b0993e2ffb4ffd5f510aeac54ef9689da04e25d]
# to [2938e1bfc2138b20460b6cc8914ff337a4ef775e]
---
+++
@@ -0,0 +1,947 @@
+#! /usr/bin/perl
+
+# Copyright (c) 2005 by Richard Levitte
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+use strict;
+use warnings;
+use Getopt::Long;
+use Pod::Usage;
+use MIME::Lite;
+use File::Spec::Functions qw(:ALL);
+
+my $VERSION = '1.0';
+
+######################################################################
+# User options
+#
+my $help = 0;
+my $man = 0;
+my $user_database = undef;
+my $root = undef;
+my @user_branches = ();
+my @user_not_branches = ();
+my $update = -1;
+my $mail = -1;
+my $attachments = 1;
+my $ignore_merges = 1;
+my $from = undef;
+my $difflogs_to = undef;
+my $nodifflogs_to = undef;
+my $before = undef;
+my $since = undef;
+my $workdir = undef;
+my $quiet = 0;
+my $debug = 0;
+my $monotone = "mtn";
+
+GetOptions('help|?' => \$help,
+ 'man' => \$man,
+ 'db=s' => \$user_database,
+ 'root=s' => \$root,
+ 'branch=s' => address@hidden,
+ 'not-branch=s' => address@hidden,
+ 'update!' => \$update,
+ 'mail!' => \$mail,
+ 'attachments!' => \$attachments,
+ 'ignore-merges!' => \$ignore_merges,
+ 'from=s' => \$from,
+ 'difflogs-to=s' => \$difflogs_to,
+ 'nodifflogs-to=s' => \$nodifflogs_to,
+ 'before=s' => \$before,
+ 'since=s' => \$since,
+ 'workdir=s' => \$workdir,
+ 'quiet' => \$quiet,
+ 'debug' => \$debug,
+ 'monotone=s' => \$monotone) or pod2usage(2);
+
+$SIG{HUP} = \&my_exit;
+$SIG{KILL} = \&my_exit;
+$SIG{TERM} = \&my_exit;
+$SIG{INT} = \&my_exit;
+
+######################################################################
+# Respond to user input
+#
+
+# For starters, output help if requested
+pod2usage(1) if $help;
+pod2usage(-exitstatus => 0, -verbose => 2) if $man;
+
+# Then check for certain conditions:
+ # If --debug was used and --update wasn't, force --noupdate
+$update = 0 if ($debug && $update == -1);
+ # If --debug was used and --mail wasn't, force --nomail
+$mail = 0 if ($debug && $mail == -1);
+ # If --debug was used, refuse to be quiet
+$quiet = 0 if $debug;
+
+# The check for missing mandatory options (oxymoron, I know :-))
+# Actually, they're only mandatory if $mail or $debug is true...
+if ($mail || $debug) {
+ if (!defined $from) {
+ my_errlog("You need to specify a From address with --from");
+ pod2usage(2);
+ }
+ if (!defined $difflogs_to && !defined $nodifflogs_to) {
+ my_errlog("You need to specify a To address with --difflogs-to or --nodifflogs-to");
+ pod2usage(2);
+ }
+}
+
+######################################################################
+# Make sure we have a database, and that the file spec is absolute.
+#
+
+# If no database is given, check the monotone options file (_MTN/options).
+# Do NOT use the branch option from there.
+if (!defined $user_database) {
+ $root = rel2abs($root) if defined $root;
+ $root = rootdir() unless defined $root;
+
+ my $curdir = rel2abs(curdir());
+ while(! -f catfile($curdir, "_MTN", "options") && $curdir ne $root) {
+ $curdir = updir($curdir);
+ }
+ my $options = catfile($curdir, "_MTN", "options");
+
+ my_debug("found options file $options");
+
+ open OPTIONS, "<$options"
+ || my_error("Couldn't open $options");
+ ($user_database) = grep(/^\s*database\s/, map { chomp; $_ } );
+ close OPTIONS;
+ $user_database =~ s/^\s*database\s"(.*)"$/$1/;
+
+ my_log("found the database $user_database in $options.");
+ my_log("the branch option from $options will NOT be used.");
+} elsif (!file_name_is_absolute($user_database)) {
+ $user_database = rel2abs($user_database);
+}
+
+######################################################################
+# Set up internal variables.
+#
+my $database = " --db=$user_database";
+my_debug("using database $user_database");
+
+my $remove_workdir = 0;
+if (!defined $workdir) {
+ $workdir = "/var/tmp/monotone_notify.work.$$";
+ mkdir $workdir;
+ $remove_workdir = 1;
+} elsif (! file_name_is_absolute($workdir)) {
+ $workdir = rel2abs($workdir);
+}
+if (! -d $workdir && ! -w $workdir && ! -r $workdir) {
+ my_error("work directory $workdir not accessible, exiting");
+}
+my_debug("using work directory $workdir");
+my_debug("(to be removed after I'm done)") if $remove_workdir;
+
+my $branches_re = "^.*\$";
+if ($#user_branches >= 0) {
+ $branches_re=
+ '^('.join('|', map { s/([^a-zA-Z0-9\[\]\*_])/\\$1/g;
+ s/\*/.\*/g;
+ $_ } @user_branches).')$';
+}
+my $not_branches_re = "^\$";
+if ($#user_not_branches >= 0) {
+ $not_branches_re=
+ '^('.join('|', map { s/([^a-zA-Z0-9\[\]\*\?_])/\\$1/g;
+ s/\?/./g;
+ s/\*/.\*/g;
+ $_ } @user_not_branches).')$';
+}
+my_debug("using the regular expression /$branches_re/ to select branches");
+
+my @files_to_clean_up = ();
+
+######################################################################
+# Move to the working directory.
+#
+chdir $workdir;
+my_debug("changed current directory to $workdir");
+
+######################################################################
+# Save all the branches that we want to look at
+#
+my %branches =
+ map { $_ => 1 }
+ grep (/$branches_re/,
+ grep (!/$not_branches_re/,
+ map { chomp; $_ }
+ my_backtick("$monotone$database list branches")));
+my_debug("collected the following branches:\n",
+ map { " $_\n" } keys %branches);
+
+######################################################################
+# Find all the current leaves, for the branches that we want.
+#
+my_log("finding all current leaves.");
+# Format: revision => { branch1 => 1, branch2 => 1, ... }
+my %current_leaves = ();
+foreach my $branch (keys %branches) {
+ foreach my $revision (my_backtick("$monotone$database automate heads $branch")) {
+ chomp $revision;
+ $current_leaves{$revision} = {} if !defined $current_leaves{$revision};
+ $current_leaves{$revision}->{$branch} = 1;
+ my_debug("address@hidden");
+ }
+}
+my_debug("found ", list_size(keys %current_leaves)," current leaves");
+
+######################################################################
+# Find the IDs of the leaves from last run.
+#
+my_log("finding all old leaves.");
+my %old_leaves = ();
+foreach my $notify_entry (my_backtick("$monotone$database list vars notify")) {
+ chomp $notify_entry;
+ if ($notify_entry =~ /^notify:\s([0-9a-fA-F]{40})\@([^\s]+)\s1$/) {
+ # Found the new format that keeps track of which branches each
+ # revision is part of.
+ if (defined $branches{$2}) {
+ $old_leaves{$1} = {} if !defined $current_leaves{$1};
+ $old_leaves{$1}->{$2} = 1;
+ }
+ } elsif ($notify_entry =~ /^notify:\s([0-9a-fA-F]{40})\s1$/) {
+ $old_leaves{$1} = {} if !defined $current_leaves{$1};
+ $old_leaves{$1}->{"*"} = 1;
+ }
+}
+
+# We save them in a file as well, to be used with
+# 'automate ancestry_difference', to avoid problems with system
+# that have small command line size limits, in case there were
+# many heads...
+my $old_leaves_file = "old_leaves";
+open OLDLEAVES, ">$old_leaves_file"
+ || my_error("Couldn't write to $old_leaves_file: $!");
+print OLDLEAVES join("\n", keys %old_leaves),"\n";
+close OLDLEAVES;
+
+my_debug("found ", list_size(keys %old_leaves),
+ " previous leaves\n (saved in $old_leaves_file)");
+
+if ($mail || $debug) {
+ ##################################################################
+ # Collect IDs for all revisions we want to log.
+ #
+ # Use the old_leaves file created by the previous collection.
+ #
+ my_log("collecting all revision IDs between current and old leaves.");
+ my %revisions =
+ map { chomp; $_ => 1 }
+ map { my_backtick("$monotone$database automate ancestry_difference $_ -@ $old_leaves_file") }
+ keys %current_leaves;
+ push @files_to_clean_up, "$old_leaves_file";
+ my @revisions_keys = keys %revisions;
+ my_debug("found ",
+ list_size(keys %revisions),
+ " revisions to log");
+
+ ##################################################################
+ # Collect all the logs.
+ #
+ # Note that if we would discard it, we skip this step and the next,
+ # so as not to waste time...
+ #
+ my_log("collecting the logs for all collected revision IDs.");
+ my %timeline = (); # hash of revision lists, keyed by date.
+ my %revision_data = (); # hash of logs (represented as arrays of
+ # strings), keyed by revision id.
+
+ foreach my $revision (keys %revisions) {
+ $revision_data{$revision} =
+ [ map { chomp; $_ }
+ my_backtick("$monotone$database log --no-graph --last=1 --from=$revision") ];
+ my $date = (split(' ', (grep(/^Date:/, @{$revision_data{$revision}}))[0]))[1];
+
+ if (defined $before && $date ge $before) {
+ my_debug("Rejecting $revision because it's too recent ($date >= $before (--before))\n");
+ next;
+ }
+ if (defined $since && $date lt $since) {
+ my_debug("Rejecting $revision because it's too old ($date < $since (--since))\n");
+ next;
+ }
+ $timeline{$date} = {} if !defined $timeline{$date};
+ $timeline{$date}->{$revision} = 1;
+ }
+
+ ##################################################################
+ # Generate messages.
+ #
+ my_log("generating messages for all collected revision IDs.");
+
+ my $message_count = 0; # It's nice with a little bit of statistics.
+
+ foreach my $date (sort keys %timeline) {
+ foreach my $revision (keys %{$timeline{$date}}) {
+ foreach my $sendinfo (([ 1, "Notify.debug-diffs", $difflogs_to ],
+ [ 0, "Notify.debug-nodiffs", $nodifflogs_to ])) {
+ my $diffs = $sendinfo->[0];
+ my $debugfile = $sendinfo->[1];
+ my $to = $sendinfo->[2];
+ next if !defined $to;
+
+ # Depending on mtn version, the line will start with
+ # "Ancestor:" or "Parent:"
+ my @ancestors =
+ map { (split ' ')[1] }
+ grep(/^(Ancestor|Parent):/, @{$revision_data{$revision}});
+
+ # If this revision has more than one ancestor, it's the
+ # result of a merge. If we have already shown the
+ # participating ancestors, let's not show the diffs again.
+ if ($ignore_merges && $diffs && $#ancestors > 0) {
+ my $will_ignore = 1;
+ my %revision_branches =
+ map { (split ' ')[1] => 1 }
+ grep /^Branch:/, @{$revision_data{$revision}};
+ my_debug("Checking if ${revision}'s ancestor have already been shown (probably).");
+ foreach (@ancestors) {
+ if (!revision_is_in_branch($_,
+ { %branches,
+ %revision_branches },
+ { %revision_data })) {
+ my_debug("Not ignoring this one!");
+ $will_ignore = 0;
+ }
+ }
+ if ($will_ignore) {
+ $diffs = 0;
+ my_debug("Not showing diff for revision $revision, because all it's ancestors\nhave already been shown.");
+ }
+ }
+
+ # If --nodiffs was used, it's silly to use attachments
+ my $attach = $attachments;
+ $attach = 0 if $diffs == 0;
+
+ my $msg;
+ my @files = (); # Makes sure we have the files in
+ # correctly sorted order.
+ my %file_info = (); # Hold information about each file.
+
+ # Make sure we have a null ancestor if there are none.
+ # generate_diff will do the right thing with it.
+ if ($#ancestors < 0) {
+ push @ancestors, "";
+ }
+
+ ######################################################
+ # Create the summary.
+ #
+ my $summary_file = "message.txt";
+ open SUMMARY,">$summary_file"
+ || my_error("Notify: couldn't create $summary_file: $!");
+ foreach (@{$revision_data{$revision}}) {
+ print SUMMARY "$_\n";
+ }
+ if (!$diffs) {
+ print SUMMARY "\n";
+ }
+ close SUMMARY;
+ push @files, $summary_file;
+
+ # This information is only used when $attachments is true.
+ $file_info{$summary_file} = { Title => 'change summary',
+ Disposition => 'inline' };
+
+ ######################################################
+ # Create the diffs.
+ #
+ if ($attach) {
+ foreach my $ancestor (@ancestors) {
+ my $diff_file = "diff.$ancestor.txt";
+ generate_diff($database, $ancestor, $revision,
+ ">$diff_file", $diffs, 0);
+ push @files, $diff_file;
+
+ $file_info{$diff_file} = {
+ Title => "Diff [$ancestor] -> [$revision]",
+ Disposition => 'attachment' };
+ }
+ } else {
+ foreach my $ancestor (@ancestors) {
+ generate_diff($database, $ancestor, $revision,
+ ">>$summary_file", $diffs, 1);
+ }
+ open SUMMARY,">>$summary_file"
+ || my_error("Notify: couldn't append to $summary_file: $!");
+ print SUMMARY "-" x 70,"\n";
+ close SUMMARY;
+ }
+
+ ######################################################
+ # Create the email.
+ #
+ if ($attach) {
+ $msg = MIME::Lite->new(From => $from,
+ To => $to,
+ Subject => "Revision $revision",
+ Type => 'multipart/mixed');
+ foreach my $file (@files) {
+ $msg->attach(Type => 'TEXT',
+ Path => $file,
+ Disposition => $file_info{$file}->{Disposition},
+ Encoding => '8bit');
+ }
+
+ # MIME:Lite has some quircks that we need to deal with
+ foreach my $part ($msg->parts()) {
+ my $filename = $part->filename();
+ my_debug("message part: $filename: { ",
+ join(', ',
+ map { "$_ => $file_info{$filename}->{$_}" }
+ keys %{$file_info{$filename}}),
+ " }");
+ # Hacks to avoid having file names, and to added a
+ # description field
+ $part->attr("content-disposition.filename" => undef);
+ $part->attr("content-type.name" => undef);
+ $part->attr("content-description" =>
+ $file_info{$filename}->{Title});
+ }
+ } else {
+ $msg = MIME::Lite->new(From => $from,
+ To => $to,
+ Subject => "Revision $revision",
+ Type => 'TEXT',
+ Path => "$summary_file",
+ Encoding => '8bit');
+ # Hacks to avoid having file names
+ $msg->attr("content-disposition.filename" => undef);
+ $msg->attr("content-type.name" => undef);
+ }
+
+ ######################################################
+ # Send it or log it (or discard it).
+ #
+ if ($mail) {
+ $msg->send();
+ } elsif ($debug) {
+ open MESSAGEDBG,">>$debugfile"
+ || my_error("Couldn't create $debugfile: $!");
+ print MESSAGEDBG "======================================================================\n";
+ $msg->print(\*MESSAGEDBG);
+ print MESSAGEDBG "======================================================================\n";
+ close MESSAGEDBG;
+ }
+ $message_count++;
+
+ ######################################################
+ # Clean up the files used to create the message.
+ #
+ my_debug("cleaning up.");
+ unlink @files;
+ }
+ }
+ }
+
+ my_log("$message_count messages were sent.");
+}
+
+######################################################################
+# Update the database with new heads
+#
+my %new_notifications =
+ map { my $rev = $_;
+ map { my $key = "address@hidden"; $key => 1 }
+ keys %{$current_leaves{$rev}} }
+ keys %current_leaves;
+my %old_notifications =
+ map { my $rev = $_;
+ map { my $key = "address@hidden"; $key => 1 }
+ keys %{$old_leaves{$rev}} }
+ keys %old_leaves;
+
+my_log("updating the table of last logged revisions.");
+
+map { my_conditional_system($update,
+ "$monotone$database set notify $_ 1") }
+ grep { !defined $old_notifications{$_} && $_ !~ /address@hidden/ }
+ keys %new_notifications;
+map { s/address@hidden//;
+ my_conditional_system($update,
+ "$monotone$database unset notify $_") }
+ grep { !defined $new_notifications{$_} }
+ keys %old_notifications;
+
+######################################################################
+# Clean up.
+#
+my_exit();
+
+######################################################################
+# Subroutines
+#
+
+# generate_diff does just that, including for the case where there is
+# no ancestor. For that latter case, we need to synthesise the diff,
+# since monotone doesn't know how to do that
+sub generate_diff
+{
+ my ($db, $ancestor, $revision, $filespec, $really_show_diffs,
+ $decorate_p, @dummy) = @_;
+
+ open OUTPUT, $filespec
+ || my_error("Couldn't write to $filespec: $!");
+ if ($really_show_diffs && $decorate_p) {
+ print OUTPUT "-" x 70, "\n";
+ print OUTPUT "Diff [$ancestor] -> [$revision]\n";
+ }
+ if ($ancestor eq "") {
+ if (!$really_show_diffs) {
+ print OUTPUT "This is the first commit, and there's no easy way to create a diff\n";
+ print OUTPUT "These are the commands to view the individual files of that commit instead:\n";
+ print OUTPUT "\n";
+ }
+ my @status = my_backtick("$monotone$db automate get_revision $revision");
+ my $line;
+ $line = shift @status;
+ if ($line =~ /^format_version\s+"([0-9]+)"\s*$/) {
+ # New versioned format
+ my $format_version = $1;
+ if ($format_version == 1) {
+ while($line =~ /^(\s*
+ |format_version\s+"[0-9]+"
+ |new_manifest\s+\[[0-9a-f]{40}\]
+ |old_revision\s+\[[0-9a-f]{40}\]
+ )\s*$/x) {
+ $line = shift @status;
+ }
+ }
+ } else {
+ while($line =~ /^(\s*
+ |(new|old)_manifest\s+\[[0-9a-f]{40}\]
+ |old_revision\s+\[[0-9a-f]{40}\]
+ )\s*$/x) {
+ $line = shift @status;
+ }
+ }
+ foreach (@status) {
+ chomp;
+ print OUTPUT ($_ eq "" ? "#" : "# $_"), "\n";
+ }
+ my $added_file = "";
+ foreach my $line (@status) {
+ my $id = undef;
+ $added_file = $1 if $line =~ /^add_file\s+"(.*)"\s*$/;
+ $id = $1 if $line =~ /^\s+content\s\[([0-9a-fA-F]{40})\]\s*$/;
+ # older format had the add_file just name the file, and having
+ # the content IDs come much later, preceded by a patch line
+ $added_file = $1 if $line =~ /^patch\s+"(.*)"\s*$/;
+ $id = $1 if $line =~ /^\s+to\s\[([0-9a-fA-F]{40})\]\s*$/;
+ if (defined $id) {
+ if ($really_show_diffs) {
+ my @file = my_backtick("$monotone$db automate get_file $id");
+ print OUTPUT "--- $added_file\n";
+ print OUTPUT "+++ $added_file\n";
+ print OUTPUT "address@hidden@ -0,0 +1,",list_size(@file)," address@hidden@\n";
+ map { print OUTPUT "+" . $_ } @file;
+ } else {
+ print OUTPUT "$monotone --db={your.database} automate get_file $id\n";
+ }
+ }
+ }
+ } else {
+ if ($really_show_diffs) {
+ print OUTPUT my_backtick("$monotone$db diff --revision=$ancestor --revision=$revision");
+ } else {
+ print OUTPUT "mtn --db={your.database} diff --revision=$ancestor --revision=$revision\n";
+ }
+ }
+ close OUTPUT;
+}
+
+# revision_is_in_branch checks if the given revision is in one of the
+# given branches. The latter is given in form of a hash.
+sub revision_is_in_branch
+{
+ my ($revision, $branches, $revision_data) = @_;
+ my $bool = 0;
+
+ my_debug("Checking if $revision has already been shown in one of
+ these branches:\n ",
+ join("\n ", keys %$branches));
+
+ if (!defined $$revision_data{$revision}) {
+ $$revision_data{$revision} =
+ [ map { chomp; $_ }
+ my_backtick("$monotone$database log --no-graph --last=1 --from=$revision") ];
+ }
+
+ map {
+ my $branch = (split ' ')[1];
+ if (defined $$branches{$branch}) {
+ $bool = 1;
+ my_debug("Found it in $branch");
+ }
+ } grep /^Branch:/, @{$$revision_data{$revision}};
+
+ my_debug("Didn't find it in any of the branches...") if !$bool;
+
+ return $bool;
+}
+
+# my_log will simply output all it's arguments, prefixed with "Notify: ",
+# unless $quiet is true.
+sub my_log
+{
+ if (!$quiet && $#_ >= 0) {
+ print STDERR "Notify: ", join("\nNotify: ",
+ split("\n",
+ join('', @_))), "\n";
+ }
+}
+
+# my_errlog will simply output all it's arguments, prefixed with "Notify: ".
+sub my_errlog
+{
+ if ($#_ >= 0) {
+ print STDERR "Notify: ", join("\nNotify: ",
+ split("\n",
+ join('', @_))), "\n";
+ }
+}
+
+# my_error will output all it's arguments, prefixed with "Notify: ", then die.
+sub my_error
+{
+ my $save_syserr = "$!";
+ if ($#_ >= 0) {
+ print STDERR "Notify: ", join("\nNotify: ",
+ split("\n",
+ join('', @_))), "\n";
+ }
+ die "$save_syserr";
+}
+
+# debug will simply output all it's arguments, prefixed with "DEBUG: ",
+# when $debug is true.
+sub my_debug
+{
+ if ($debug && $#_ >= 0) {
+ print STDERR "DEBUG: ", join("\nDEBUG: ",
+ split("\n",
+ join('', @_))), "\n";
+ }
+}
+
+# my_system does the same thing as system, but will print a bit of debugging
+# output when $debug is true. It will also die if the subprocess returned
+# an error code.
+sub my_system
+{
+ my $command = shift @_;
+
+ my_debug("'${command}'\n");
+ my $return = system($command);
+ my $exit = $? >> 8;
+ die "'${command}' returned with exit code $exit\n" if ($exit);
+ return $return;
+}
+
+# my_conditional_system does the same thing as system, but will print a bit
+# of debugging output when $debug is true, and will only actually run the
+# command if the condition is true. It will also die if the subprocess
+# returned an error code.
+sub my_conditional_system
+{
+ my $condition = shift @_;
+ my $command = shift @_;
+ my $return = 0; # exit code for 'true'
+
+ my_debug("'${command}'\n");
+ if ($condition) {
+ $return = system($command);
+ my $exit = $? >> 8;
+ die "'${command}' returned with exit code $exit\n" if ($exit);
+ } else {
+ my_debug("... not actually executed.\n");
+ }
+ return $return;
+}
+
+# my_exit removes temporary files and then exits.
+sub my_exit
+{
+ my_log("cleaning up.");
+ unlink @files_to_clean_up;
+ rmdir $workdir if $remove_workdir;
+ my_log("all done.");
+ exit(0);
+}
+
+# my_backtick does the same thing as backtick commands, but will print a bit
+# of debugging output when $debug is true. It will also die if the subprocess
+# returned an error code.
+sub my_backtick
+{
+ my $command = shift @_;
+
+ my_debug("\`$command\`\n");
+ my @return = `$command`;
+ my $exit = $? >> 8;
+ if ($exit) {
+ my_debug(map { "> ".$_ } @ return);
+ die "'${command}' returned with exit code $exit\n";
+ }
+ return @return;
+}
+
+# list_size returns the size of the list. It's better than $#{var}
+# because it doesn't require the input to be a variable, and it
+# doesn't return one less than the size.
+sub list_size
+{
+ return $#_ + 1;
+}
+
+
+__END__
+
+=head1 NAME
+
+monotone-notify.pl - a script to send monotone change notifications by email
+
+=head1 SYNOPSIS
+
+monotone-notify.pl [--help] [--man]
+[--db=database] [--root=path] [--branch=branch ...]
+[--[no]update] [--[no]mail] [--[no]attachments] [--[no]ignore-merges]
+[--from=email-sender]
+[--difflogs-to=email-recipient] [--nodifflogs-to=email-recipient]
+[--workdir=path] [--before=yyyy-mm-ddThh:mm:ss] [--since=yyyy-mm-ddThh:mm:ss]
+[--quiet] [--debug] [--monotone=path]
+
+=head1 DESCRIPTION
+
+B is used to generate emails containing monotone
+change logs for recent changes. It uses monotone database variables
+in the domain 'notify' to keep track of the latest revisions already
+logged.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--help>
+
+Print a brief help message and exit.
+
+=item B<--man>
+
+Print the manual page and exit.
+
+=item B<--db>=I
+
+Sets which database to use. If not given, the database given in
+_MTN/options is used.
+
+=item B<--root>=I
+
+Stop the search for a working copy (containing the F<_MTN> directory) at
+the specified root directory rather than at the physical root of the
+filesystem.
+
+=item B<--branch>=I
+
+Sets a branch that should be checked. Can be used multiple times to
+set several branches. If not given at all, all available branches are
+used.
+
+=item B<--update>
+
+Has B update the database variables at the end of
+the run. This is the default unless B<--debug> is given.
+
+=item B<--noupdate>
+
+The inverse of B<--update>. This is the default when B<--debug> is
+given.
+
+=item B<--mail>
+
+Has B send the constructed logs as emails. This
+is the default unless B<--debug> is given.
+
+=item B<--nomail>
+
+The inverse of B<--mail>. This is the default when B<--debug> is
+given.
+
+=item B<--attachments>
+
+Add the change summary and the output of 'monotone diff' as
+attachments in the emails. This is the default behavior.
+
+=item B<--noattachments>
+
+Have the change summary and the output of 'monotone diff' in the body
+of the email, separated by lines of dashes.
+
+=item B<--ignore-merges>
+
+Do not create difflogs for merges (revisions with more than one
+ancestor), if the ancestors are in at least one of the branches that
+are monitored. This is the default behavior.
+
+=item B<--noignore-merges>
+
+Always create difflogs, even for merges.
+
+=item B<--from>=I
+
+Sets the sender address to be used when creating the emails. There is
+no default, so this is a required option.
+
+=item B<--difflogs-to>=I
+
+Sets the recipient address to be used when creating the emails with
+logs containing diffs. This or B<--nodifflogs-to> MUST be used, and
+both can be given at the same time (thereby generating two emails for
+each log).
+
+=item B<--nodifflogs-to>=I
+
+Sets the recipient address to be used when creating the emails with
+logs without diffs. This or B<--difflogs-to> MUST be used, and both
+can be given at the same time (thereby generating two emails for each
+log).
+
+=item B<--before>=I
+
+Only log revisions where the datetime is less than the one given.
+
+=item B<--since>=I
+
+Only log revisions where the datetime is greater or equal to than the
+one given.
+
+=item B<--workdir>=I
+
+Sets the working directory to use for temporary files. This working
+directory should be empty to avoid having files overwritten. When
+B<--debug> is used and unless B<--mail> is given, one or both of the
+two files F and F will be
+left in the work directory.
+
+The default working directory is F,
+and will be removed automatically unless F or
+F are left in it.
+
+=item B<--debug>
+
+Makes B go to debug mode. It means a LOT of extra
+output, and also implies B<--noupdate> and B<--nomail> unless
+specified differently on the command line.
+
+=item B<--quiet>
+
+Makes B really silent. It will normally produce a
+small log of it's activities, but with B<--quiet>, it will only output
+error messages. If B<--debug> was given, B<--quiet> is turned off
+unconditionally.
+
+=item B<--monotone>=I
+
+Gives the name or path to mtn(1) or both. The default is simply
+F.
+
+=back
+
+=head1 NOTES
+
+You might notice that a series of logs for some branch may span over
+other branches. This is because some development may actually go
+through those other branches by virtue of 'monotone propagate' and
+other means to move changes from one branch to another.
+
+This behavior isn't entirely deterministic, as it depends on when the
+last run of B was done, and what head revisions
+were active at that time. It might be seen as a bug, but if
+corrected, it might miss out on development that moves entirely to
+another branch and moves back later in time, thereby creating a hole
+in the branch currently looked at. This has actually happened in the
+development of monotone itself.
+
+For now, it's assumed that a little too much information is better
+than (unjust) lack of information.
+
+=head1 BUGS
+
+Fewer than before.
+
+=head1 SEE ALSO
+
+L
+
+=head1 AUTHOR
+
+Richard Levitte,
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2005 by Richard Levitte
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+=over 3
+
+=item 1.
+
+Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+=item 2.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+=back
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=cut