--- /dev/null
+Return-Path: <bremner@tethera.net>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+ by olra.theworths.org (Postfix) with ESMTP id A6E8F429E21\r
+ for <notmuch@notmuchmail.org>; Sun, 6 Nov 2011 17:00:14 -0800 (PST)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -2.3\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-2.3 tagged_above=-999 required=5\r
+ tests=[RCVD_IN_DNSWL_MED=-2.3] autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+ by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+ with ESMTP id H8lQC-APrdDh for <notmuch@notmuchmail.org>;\r
+ Sun, 6 Nov 2011 17:00:13 -0800 (PST)\r
+Received: from tempo.its.unb.ca (tempo.its.unb.ca [131.202.1.21])\r
+ (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))\r
+ (No client certificate requested)\r
+ by olra.theworths.org (Postfix) with ESMTPS id 2AAA9431FB6\r
+ for <notmuch@notmuchmail.org>; Sun, 6 Nov 2011 17:00:13 -0800 (PST)\r
+Received: from zancas.localnet\r
+ (fctnnbsc36w-156034074106.pppoe-dynamic.High-Speed.nb.bellaliant.net\r
+ [156.34.74.106]) (authenticated bits=0)\r
+ by tempo.its.unb.ca (8.13.8/8.13.8) with ESMTP id pA7105mB008829\r
+ (version=TLSv1/SSLv3 cipher=AES256-SHA bits=256 verify=NO);\r
+ Sun, 6 Nov 2011 21:00:08 -0400\r
+Received: from bremner by zancas.localnet with local (Exim 4.76)\r
+ (envelope-from <bremner@tethera.net>)\r
+ id 1RNDZE-0002d1-Qw; Sun, 06 Nov 2011 21:00:04 -0400\r
+From: David Bremner <david@tethera.net>\r
+To: notmuch@notmuchmail.org\r
+Subject: [PATCH] contrib/nmbug: new script for sharing tags with a given\r
+ prefix.\r
+Date: Sun, 6 Nov 2011 20:59:46 -0400\r
+Message-Id: <1320627586-10068-1-git-send-email-david@tethera.net>\r
+X-Mailer: git-send-email 1.7.6.3\r
+In-Reply-To: <1319906707-10141-2-git-send-email-david@tethera.net>\r
+References: <1319906707-10141-2-git-send-email-david@tethera.net>\r
+Cc: David Bremner <bremner@debian.org>\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+ <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Mon, 07 Nov 2011 01:00:14 -0000\r
+\r
+From: David Bremner <bremner@debian.org>\r
+\r
+The main idea is consider the notmuch database as analogous to the\r
+work-tree. A bare git repo is maintained in the users home directory,\r
+with a tree of the form tags/$message-id/$tag\r
+\r
+Like notmuch and git, we have a set of subcommnds, mainly modelled on\r
+git.\r
+\r
+The most important commands are\r
+\r
+ commit xapian -> git\r
+ checkout git -> xapian\r
+ merge fetched git + git -> xapian\r
+ status find differences between xapian, git, and remote git.\r
+\r
+There are also some convenience wrappers around git commands.\r
+\r
+In order to encode tags (viewed as octet sequences) into filenames,\r
+we whitelist a smallish set of characters and %hex escape anything outside.\r
+\r
+The prefix is omitted in git, which lets one save and restore to\r
+different prefixes (although this is only lightly tested).\r
+---\r
+\r
+Many things have changed, time for a repost. It no long needs the\r
+"restore --match" patches. \r
+\r
+ contrib/nmbug | 565 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++\r
+ 1 files changed, 565 insertions(+), 0 deletions(-)\r
+ create mode 100755 contrib/nmbug\r
+\r
+diff --git a/contrib/nmbug b/contrib/nmbug\r
+new file mode 100755\r
+index 0000000..2128b95\r
+--- /dev/null\r
++++ b/contrib/nmbug\r
+@@ -0,0 +1,565 @@\r
++#!/usr/bin/env perl\r
++# Copyright (c) 2011 David Bremner\r
++# License: same as notmuch\r
++\r
++use strict;\r
++use File::Path qw(remove_tree make_path);\r
++use File::Temp qw(tempdir tempfile);\r
++use File::Basename;\r
++use Pod::Usage;\r
++\r
++no encoding;\r
++\r
++my $NMBGIT = $ENV{NMBGIT} || $ENV{HOME}."/.nmbug";\r
++\r
++$NMBGIT .= '/.git' if (-d $NMBGIT.'/.git');\r
++\r
++my $TAGPREFIX = $ENV{NMBPREFIX} || "notmuch::";\r
++\r
++# magic hashes for git\r
++my $EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391';\r
++my $EMPTYTREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';\r
++\r
++# for encoding\r
++\r
++my $ESCAPE_CHAR='%';\r
++my $NO_ESCAPE= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-_@=.:,";\r
++my $MUST_ENCODE=qr{[^\Q$NO_ESCAPE\E]};\r
++my $ESCAPED_RX=qr{$ESCAPE_CHAR[A-Fa-f0-9]{2}};\r
++\r
++my %command=(\r
++ archive => \&do_archive,\r
++ checkout =>\&do_checkout,\r
++ commit => \&do_commit,\r
++ fetch => \&do_fetch,\r
++ help => \&do_help,\r
++ log => \&do_log,\r
++ merge => \&do_merge,\r
++ pull => \&do_pull,\r
++ push => \&do_push,\r
++ status => \&do_status,\r
++ );\r
++\r
++my $subcommand=shift;\r
++\r
++if (!exists $command{$subcommand}){\r
++ usage();\r
++}\r
++\r
++&{$command{$subcommand}}(@ARGV);\r
++\r
++sub get_tags {\r
++ my $prefix=shift;\r
++ my @tags;\r
++ open my $fh, 'notmuch search --output=tags "*"|' or die "error dumping tags";\r
++ while (<$fh>) {\r
++ chomp();\r
++ push @tags, $_ if (m/^$prefix/);\r
++ }\r
++ return @tags;\r
++}\r
++\r
++sub do_archive {\r
++ system ('git', "--git-dir=$NMBGIT", 'archive', 'HEAD');\r
++}\r
++\r
++sub is_committed(){\r
++ my $status = compute_status();\r
++ return scalar (@{$status->{added}} ) + scalar (@{$status->{deleted}} )==0\r
++}\r
++\r
++sub do_commit {\r
++ my @args=@_;\r
++\r
++ if ( is_committed() ){\r
++ print "Nothing to commit\n";\r
++ return;\r
++ }\r
++\r
++ my $index=index_tags();\r
++\r
++ my $tree= git ('write-tree', { GIT_INDEX_FILE=>$index })\r
++ or die "no output from write-tree";\r
++\r
++ my $parent = git ( 'rev-parse', 'HEAD' )\r
++ or die "no output from rev-parse";\r
++\r
++ my $commit = git ('commit-tree', $tree, '-p', $parent, {}, @args);\r
++\r
++ git ('update-ref', 'HEAD', $commit);\r
++\r
++ unlink $index || die "unlink: $!";\r
++\r
++}\r
++\r
++sub do_fetch {\r
++ my $remote = shift || "origin";\r
++\r
++ git ('fetch', $remote);\r
++}\r
++\r
++sub git {\r
++\r
++ return subcommand('git',@_);\r
++\r
++}\r
++\r
++sub subcommand {\r
++ die 'command and subcommand needed' unless (scalar(@_) >= 2);\r
++\r
++ return run (@_);\r
++}\r
++\r
++# arguments are any number of strings for ARGV, an optional hash ref\r
++# for settings for the environment, followed by any number of strings as \r
++# as lines for stdin.\r
++\r
++sub run {\r
++\r
++ my $command=shift \r
++ or die "command not specified";\r
++\r
++ my @args;\r
++\r
++\r
++ while ( scalar(@_) && !ref($_[0]) ) {\r
++ push @args, shift;\r
++ };\r
++\r
++ my %SETENV={};\r
++ if(ref($_[0]) eq 'HASH'){\r
++ my $ref=shift;\r
++ %SETENV=%{$ref};\r
++ }\r
++\r
++ $SETENV{GIT_DIR} ||= $NMBGIT;\r
++\r
++ my @input=@_;\r
++ my @output;\r
++\r
++ my $pid = open my $child, '-|';\r
++ if ($pid){\r
++ while(<$child>){\r
++ chomp();\r
++ push @output,$_;\r
++ }\r
++ close ($child);\r
++ } else {\r
++\r
++ # setup child environment\r
++ while (my ($key,$val) = each %SETENV) {\r
++ $ENV{$key}=$val;\r
++ }\r
++\r
++ read_from(@input);\r
++ exec($command,@args) || die "exec $command @args: $!";\r
++ }\r
++\r
++ if (wantarray()) {\r
++ return @output;\r
++ }\r
++ elsif (defined wantarray()) {\r
++ return join("\n",@output);\r
++ }\r
++ else {\r
++ return;\r
++ }\r
++}\r
++\r
++sub read_from {\r
++ close STDIN;\r
++ if (!scalar(@_)){\r
++ open STDIN, '<', '/dev/null' || die "reopening stdin: $!";\r
++ } else {\r
++ my ($fh,$tempfile) = tempfile();\r
++ foreach my $line (@_){\r
++ print $fh $line;\r
++ }\r
++ close $fh || die "closing";\r
++\r
++ open STDIN, '<', $tempfile or\r
++ die "reopening stdin: $!";\r
++ }\r
++}\r
++\r
++sub notmuch {\r
++ my @args=@_;\r
++ system ('notmuch', @args) == 0 or die "notmuch @args failed: $?"\r
++}\r
++\r
++\r
++sub index_tags {\r
++\r
++ my $index=$NMBGIT."/nmbug.index";\r
++\r
++ my $query = join " ", map ("tag:$_", get_tags($TAGPREFIX));\r
++ open my $fh, "notmuch dump -- $query|" or die "notmuch dump: $!";\r
++\r
++ git ('read-tree', $EMPTYTREE);\r
++ open my $git, "|GIT_DIR=$NMBGIT GIT_INDEX_FILE=$index git update-index --index-info" or die "git update-index";\r
++\r
++ while (<$fh>) {\r
++ m/ ( [^ ]* ) \s+ \( ([^\)]* ) \) /x || die "syntax error in dump";\r
++ my ($id,$rest) = ($1,$2);\r
++ index_tags_for_msg ($git,$id, split(" ", $rest));\r
++ }\r
++\r
++ close $git;\r
++ return $index;\r
++}\r
++\r
++sub index_tags_for_msg {\r
++ my $fh=shift;\r
++ my $msgid = shift;\r
++ my @tags=@_;\r
++\r
++ foreach my $tag (@tags){\r
++ # insist prefix is there, but remove it before writing\r
++ next unless ($tag =~ s/^$TAGPREFIX//);\r
++ my $tagpath = 'tags/' . encode_for_fs($msgid) . '/' . encode_for_fs ($tag);\r
++ print $fh "100644 blob $EMPTYBLOB\t$tagpath\n";\r
++ }\r
++}\r
++\r
++sub do_checkout {\r
++ do_sync (action => 'checkout');\r
++}\r
++\r
++sub do_sync {\r
++\r
++ my %args=@_;\r
++\r
++ my $status=compute_status();\r
++ my ($A_action, $D_action);\r
++\r
++ if ($args{action} eq 'checkout') {\r
++ $A_action = '-';\r
++ $D_action = '+';\r
++ } else {\r
++ $A_action = '+';\r
++ $D_action = '-';\r
++ }\r
++\r
++ foreach my $pair (@{$status->{added}}){\r
++\r
++ notmuch ('tag', $A_action.$TAGPREFIX.$pair->{tag},\r
++ 'id:'.$pair->{id});\r
++ }\r
++\r
++ foreach my $pair (@{$status->{deleted}}){\r
++ notmuch ('tag', $D_action.$TAGPREFIX.$pair->{tag},\r
++ 'id:'.$pair->{id});\r
++ }\r
++\r
++}\r
++\r
++sub insist_committed {\r
++\r
++ if ( !is_committed () ){\r
++ print "Uncommitted changes to $TAGPREFIX* tags in notmuch\r
++\r
++For a summary of changes, run 'nmbug status'\r
++To save your changes, run 'nmbug commit' before merging/pull\r
++To discard your changes, run 'nmbug checkout'\r
++";\r
++ exit(1);\r
++ }\r
++\r
++}\r
++\r
++sub do_pull {\r
++ my $remote = shift || "origin";\r
++\r
++ git ( 'fetch', $remote);\r
++\r
++ do_merge();\r
++}\r
++\r
++sub do_merge {\r
++ insist_committed();\r
++\r
++ my $tempwork= tempdir ("/tmp/nmbug-merge.XXXXXX", CLEANUP=>1);\r
++\r
++ git ( 'checkout', '-f', 'HEAD', { GIT_WORK_TREE=> $tempwork });\r
++\r
++ git ( 'merge', 'FETCH_HEAD', { GIT_WORK_TREE=> $tempwork });\r
++\r
++ do_checkout();\r
++}\r
++\r
++sub do_log {\r
++ # we don't want output trapping here, because we want the pager.\r
++ system ( 'git', "--git-dir=$NMBGIT", 'log', '--name-status', @_);\r
++}\r
++\r
++sub do_push {\r
++ my $remote = shift || "origin";\r
++\r
++ git ('push', $remote);\r
++}\r
++\r
++sub do_status {\r
++ my $status = compute_status();\r
++ \r
++ foreach my $pair (@{$status->{added}}){\r
++ printf "A\t%s\t%s\n",$pair->{id}, $pair->{tag};\r
++ }\r
++\r
++ foreach my $pair (@{$status->{deleted}}){\r
++ printf "D\t%s\t%s\n",$pair->{id}, $pair->{tag};\r
++ }\r
++\r
++ foreach my $id (@{$status->{missing}}){\r
++ print "U\t$id\n",\r
++ }\r
++\r
++ if (is_unmerged ()) {\r
++ foreach my $pair (diff_refs('A')){\r
++ printf "a\t%s\t%s\n",$pair->{id}, $pair->{tag};\r
++ }\r
++\r
++ foreach my $pair (diff_refs('D')){\r
++ printf "d\t%s\t%s\n",$pair->{id}, $pair->{tag};\r
++ }\r
++ }\r
++\r
++}\r
++\r
++sub is_unmerged {\r
++ my $fetch_head = git ('rev-parse', 'FETCH_HEAD');\r
++ my $base = git ( 'merge-base', 'HEAD', 'FETCH_HEAD');\r
++\r
++ return ($base ne $fetch_head);\r
++\r
++}\r
++sub compute_status {\r
++ my %args=@_;\r
++\r
++ my @added;\r
++ my @deleted;\r
++ my @missing;\r
++\r
++ my $index=index_tags();\r
++\r
++ my @maybe_deleted = diff_index($index,'D');\r
++\r
++ foreach my $pair (@maybe_deleted){\r
++\r
++ my $id = $pair->{id};\r
++\r
++ open my $fh, "notmuch search --output=files id:$id |"\r
++ or die "searching for $id";\r
++ if (!<$fh>) {\r
++ push @missing, $id;\r
++ } else {\r
++ push @deleted, $pair;\r
++ }\r
++ }\r
++\r
++\r
++ @added = diff_index ($index, 'A');\r
++\r
++ unlink $index || die "unlink $index: $!";\r
++\r
++ return { added => [@added], deleted => [@deleted], missing=> [@missing] };\r
++}\r
++\r
++sub diff_index {\r
++ my $index=shift;\r
++ my $filter=shift;\r
++\r
++ my @lines=git( qw/diff-index --cached/,\r
++ "--diff-filter=$filter", qw/--name-only HEAD/,\r
++ {GIT_INDEX_FILE=>$index} );\r
++\r
++ return unpack_diff_lines(@lines);\r
++}\r
++\r
++sub diff_refs {\r
++ my $filter=shift;\r
++ my $ref1 = shift || 'HEAD';\r
++ my $ref2 = shift || 'FETCH_HEAD';\r
++\r
++ my @lines=git( 'diff', "--diff-filter=$filter", '--name-only',\r
++ $ref1, $ref2);\r
++\r
++ return unpack_diff_lines(@lines);\r
++}\r
++\r
++\r
++sub unpack_diff_lines {\r
++ my @found;\r
++\r
++ foreach (@_){\r
++ chomp();\r
++ my ($id,$tag) = m@tags/ ([^/]+) / ([^/]+) @x;\r
++\r
++ $id = decode_from_fs($id);\r
++ $tag = decode_from_fs($tag);\r
++\r
++ push @found, { id => $id, tag => $tag };\r
++ }\r
++\r
++ return @found;\r
++}\r
++\r
++sub encode_for_fs{\r
++ my $str=shift;\r
++\r
++ $str=~ s/($MUST_ENCODE)/"$ESCAPE_CHAR".sprintf("%02x",ord($1))/ge;\r
++ return $str;\r
++}\r
++\r
++sub decode_from_fs{\r
++ my $str=shift;\r
++\r
++ $str=~ s/$ESCAPED_RX/ hex($1)/eg;\r
++\r
++ return $str;\r
++\r
++}\r
++\r
++\r
++sub usage {\r
++ pod2usage();\r
++ exit(1);\r
++}\r
++\r
++sub do_help {\r
++ pod2usage( -verbose=>2 );\r
++ exit(0);\r
++}\r
++\r
++__END__\r
++\r
++=head1 NAME\r
++\r
++nmbug - manage notmuch tags about notmuch\r
++\r
++=head1 SYNOPSIS\r
++\r
++nmbug subcommand [options]\r
++\r
++B<nmbug help> for more help\r
++\r
++=head1 OPTIONS\r
++\r
++=head2 Most common commands\r
++\r
++=over 8\r
++\r
++=item B<commit> [message]\r
++\r
++Commit appropriately prefixed tags from the notmuch database to\r
++git. Any extra arguments are used (one per line) as a commit message.\r
++\r
++=item B<push> [remote]\r
++\r
++push local nmbug git state to remote repo\r
++\r
++=item B<pull> [remote]\r
++\r
++pull (merge) remote repo changes to notmuch. B<pull> is equivalent to\r
++B<fetch> followed by B<merge>.\r
++\r
++=back\r
++\r
++=head2 Other Useful Commands\r
++\r
++=over 8\r
++\r
++=item B<checkout>\r
++\r
++Update the notmuch database from git. This is mainly useful to discard\r
++your changes in notmuch relative to git.\r
++\r
++=item B<fetch> [remote]\r
++\r
++Fetch changes from the remote repo (see merge to bring those changes\r
++into notmuch). \r
++\r
++=item B<help> [subcommand]\r
++\r
++print help [for subcommand]\r
++\r
++=item B<log> [parameters]\r
++\r
++A simple wrapper for git log. After running C<nmbug fetch>, you can\r
++inspect the changes with C<nmbug log HEAD..FETCH_HEAD>\r
++\r
++=item B<merge> \r
++\r
++Merge changes from FETCH_HEAD into HEAD, and load the result into\r
++notmuch.\r
++\r
++=item B<status>\r
++\r
++Show pending updates in notmuch or git repo. See below for more\r
++information about the output format.\r
++\r
++=back\r
++\r
++=head2 Less common commands\r
++\r
++=over 8\r
++\r
++=item B<archive>\r
++\r
++Dump a tar archive (using git archive) of the current nmbug tag set.\r
++\r
++=back\r
++\r
++\r
++=head1 STATUS FORMAT\r
++\r
++B<nmbug status> prints lines of the form\r
++\r
++ c Message-Id tag\r
++\r
++where c is\r
++\r
++=over 8\r
++\r
++=item B<A>\r
++\r
++Tag is present in notmuch database, but not committed to nmbug\r
++(equivalently, tag has been deleted in nmbug repo, e.g. by a pull, but\r
++not restored to notmuch database).\r
++\r
++=item B<a>\r
++\r
++Tag is fetched, but not merged into notmuch.\r
++\r
++=item B<D>\r
++\r
++Tag is present in nmbug repo, but not restored to notmuch database\r
++(equivalently, tag has been deleted in notmuch)\r
++\r
++=item B<d>\r
++\r
++Tag deletion is fetched, but not merged into notmuch.\r
++\r
++=item B<U>\r
++\r
++Message is unknown (missing from local notmuch database)\r
++\r
++=back\r
++\r
++=head1 DUMP FORMAT\r
++\r
++Each tag $tag for message with Message-Id $id is written to\r
++an empty file\r
++\r
++ tags/encode($id)/encode($tag)\r
++\r
++The encoding preserves alphanumerics, and the characters "+-_@=.:,"\r
++(not the quotes). All other octets are replaced with '%' followed by\r
++a two digit hex number.\r
++\r
++=head1 ENVIRONMENT\r
++\r
++B<NMBGIT> specifies the location of the git repository used by nmbug. \r
++If not specified $HOME/.nmbug is used.\r
++\r
++B<NMBPREFIX> specifies the prefix in the notmuch database for tags of\r
++interest to nmbug. If not specified 'notmuch::' is used.\r
+-- \r
+1.7.6.3\r
+\r