[PATCH] contrib/nmbug: new script for sharing tags with a given prefix.
authorDavid Bremner <david@tethera.net>
Mon, 7 Nov 2011 00:59:46 +0000 (20:59 +2000)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:40:01 +0000 (09:40 -0800)
f9/0727c077c94edf39ca70caf12b76ab0b9c2a20 [new file with mode: 0644]

diff --git a/f9/0727c077c94edf39ca70caf12b76ab0b9c2a20 b/f9/0727c077c94edf39ca70caf12b76ab0b9c2a20
new file mode 100644 (file)
index 0000000..92b255e
--- /dev/null
@@ -0,0 +1,660 @@
+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