2 # Copyright (c) 2011 David Bremner
3 # License: same as notmuch
7 use File::Temp qw(tempdir);
12 my $NMBGIT = $ENV{NMBGIT} || $ENV{HOME}.'/.nmbug';
14 $NMBGIT .= '/.git' if (-d $NMBGIT.'/.git');
16 my $TAGPREFIX = $ENV{NMBPREFIX} || 'notmuch::';
19 my $EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391';
23 my $ESCAPE_CHAR = '%';
24 my $NO_ESCAPE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.
26 my $MUST_ENCODE = qr{[^\Q$NO_ESCAPE\E]};
27 my $ESCAPED_RX = qr{$ESCAPE_CHAR([A-Fa-f0-9]{2})};
30 archive => \&do_archive,
31 checkout => \&do_checkout,
32 commit => \&do_commit,
39 status => \&do_status,
42 # Convert prefix into form suitable for literal matching against
43 # notmuch dump --format=batch-tag output.
44 my $ENCPREFIX = encode_for_fs ($TAGPREFIX);
45 $ENCPREFIX =~ s/:/%3a/g;
47 my $subcommand = shift || usage ();
49 if (!exists $command{$subcommand}) {
53 &{$command{$subcommand}}(@ARGV);
56 my $envref = (ref $_[0] eq 'HASH') ? shift : {};
57 my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef;
58 my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : undef;
61 $envref->{GIT_DIR} ||= $NMBGIT;
62 spawn ($envref, defined $ioref ? $ioref : (), defined $dir ? $dir : (), @_);
66 my $fh = git_pipe (@_);
67 my $str = join ('', <$fh>);
69 die "'git @_' exited with nonzero value\n";
76 my $envref = (ref $_[0] eq 'HASH') ? shift : {};
77 my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef;
78 my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : '-|';
82 if (open my $child, $dir) {
86 while (my ($key, $value) = each %{$envref}) {
90 if (defined $ioref && $dir eq '-|') {
91 open my $fh, '|-', @_ or die "open |- @_: $!";
92 foreach my $line (@{$ioref}) {
93 print $fh $line, "\n";
98 open STDIN, '<', '/dev/null' or die "reopening stdin: $!"
110 my $fh = spawn ('-|', qw/notmuch search --output=tags/, "*")
111 or die 'error dumping tags';
115 push @tags, $_ if (m/^$prefix/);
118 die "'notmuch search --output=tags *' exited with nonzero value\n";
125 system ('git', "--git-dir=$NMBGIT", 'archive', 'HEAD');
131 return scalar (@{$status->{added}} ) + scalar (@{$status->{deleted}} ) == 0;
138 my $status = compute_status ();
140 if ( is_committed ($status) ) {
141 print "Nothing to commit\n";
145 my $index = read_tree ('HEAD');
147 update_index ($index, $status);
149 my $tree = git ( { GIT_INDEX_FILE => $index }, 'write-tree')
150 or die 'no output from write-tree';
152 my $parent = git ( 'rev-parse', 'HEAD' )
153 or die 'no output from rev-parse';
155 my $commit = git ([ @args ], 'commit-tree', $tree, '-p', $parent)
156 or die 'commit-tree';
158 git ('update-ref', 'HEAD', $commit);
160 unlink $index || die "unlink: $!";
166 my $index = $NMBGIT.'/nmbug.index';
167 git ({ GIT_INDEX_FILE => $index }, 'read-tree', '--empty');
168 git ({ GIT_INDEX_FILE => $index }, 'read-tree', $treeish);
176 my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index },
177 '|-', qw/git update-index --index-info/)
178 or die 'git update-index';
180 foreach my $pair (@{$status->{deleted}}) {
181 index_tags_for_msg ($git, $pair->{id}, 'D', $pair->{tag});
184 foreach my $pair (@{$status->{added}}) {
185 index_tags_for_msg ($git, $pair->{id}, 'A', $pair->{tag});
187 unless (close $git) {
188 die "'git update-index --index-info' exited with nonzero value\n";
195 my $remote = shift || 'origin';
197 git ('fetch', $remote);
203 system ('notmuch', @args) == 0 or die "notmuch @args failed: $?";
209 my $index = $NMBGIT.'/nmbug.index';
211 my $query = join ' ', map ("tag:\"$_\"", get_tags ($TAGPREFIX));
213 my $fh = spawn ('-|', qw/notmuch dump --format=batch-tag --/, $query)
214 or die "notmuch dump: $!";
216 git ('read-tree', '--empty');
217 my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index },
218 '|-', qw/git update-index --index-info/)
219 or die 'git update-index';
224 my ($rest,$id) = split(/ -- id:/);
226 if ($id =~ s/^"(.*)"\s*$/$1/) {
227 # xapian quoted string, dequote.
231 #strip prefixes from tags before writing
232 my @tags = grep { s/^[+]$ENCPREFIX//; } split (' ', $rest);
233 index_tags_for_msg ($git,$id, 'A', @tags);
235 unless (close $git) {
236 die "'git update-index --index-info' exited with nonzero value\n";
239 die "'notmuch dump --format=batch-tag -- $query' exited with nonzero value\n";
244 # update the git index to either create or delete an empty file.
245 # Neither argument should be encoded/escaped.
246 sub index_tags_for_msg {
251 my $hash = $EMPTYBLOB;
252 my $blobmode = '100644';
256 $hash = '0000000000000000000000000000000000000000';
259 foreach my $tag (@_) {
260 my $tagpath = 'tags/' . encode_for_fs ($msgid) . '/' . encode_for_fs ($tag);
261 print $fh "$blobmode $hash\t$tagpath\n";
267 do_sync (action => 'checkout');
275 my $status = compute_status ();
276 my ($A_action, $D_action);
278 if ($args{action} eq 'checkout') {
286 foreach my $pair (@{$status->{added}}) {
288 notmuch ('tag', $A_action.$TAGPREFIX.$pair->{tag},
292 foreach my $pair (@{$status->{deleted}}) {
293 notmuch ('tag', $D_action.$TAGPREFIX.$pair->{tag},
300 sub insist_committed {
302 my $status=compute_status();
303 if ( !is_committed ($status) ) {
304 print "Uncommitted changes to $TAGPREFIX* tags in notmuch
306 For a summary of changes, run 'nmbug status'
307 To save your changes, run 'nmbug commit' before merging/pull
308 To discard your changes, run 'nmbug checkout'
317 my $remote = shift || 'origin';
319 git ( 'fetch', $remote);
328 my $tempwork = tempdir ('/tmp/nmbug-merge.XXXXXX', CLEANUP => 1);
330 git ( { GIT_WORK_TREE => $tempwork }, 'checkout', '-f', 'HEAD');
332 git ( { GIT_WORK_TREE => $tempwork }, 'merge', 'FETCH_HEAD');
339 # we don't want output trapping here, because we want the pager.
340 system ( 'git', "--git-dir=$NMBGIT", 'log', '--name-status', @_);
345 my $remote = shift || 'origin';
347 git ('push', $remote, 'master');
352 my $status = compute_status ();
355 foreach my $pair (@{$status->{added}}) {
356 $output{$pair->{id}} ||= {};
357 $output{$pair->{id}}{$pair->{tag}} = 'A'
360 foreach my $pair (@{$status->{deleted}}) {
361 $output{$pair->{id}} ||= {};
362 $output{$pair->{id}}{$pair->{tag}} = 'D'
365 foreach my $pair (@{$status->{missing}}) {
366 $output{$pair->{id}} ||= {};
367 $output{$pair->{id}}{$pair->{tag}} = 'U'
370 if (is_unmerged ()) {
371 foreach my $pair (diff_refs ('A')) {
372 $output{$pair->{id}} ||= {};
373 $output{$pair->{id}}{$pair->{tag}} ||= ' ';
374 $output{$pair->{id}}{$pair->{tag}} .= 'a';
377 foreach my $pair (diff_refs ('D')) {
378 $output{$pair->{id}} ||= {};
379 $output{$pair->{id}}{$pair->{tag}} ||= ' ';
380 $output{$pair->{id}}{$pair->{tag}} .= 'd';
384 foreach my $id (sort keys %output) {
385 foreach my $tag (sort keys %{$output{$id}}) {
386 printf "%s\t%s\t%s\n", $output{$id}{$tag}, $id, $tag;
394 return 0 if (! -f $NMBGIT.'/FETCH_HEAD');
396 my $fetch_head = git ('rev-parse', 'FETCH_HEAD');
397 my $base = git ( 'merge-base', 'HEAD', 'FETCH_HEAD');
399 return ($base ne $fetch_head);
410 my $index = index_tags ();
412 my @maybe_deleted = diff_index ($index, 'D');
414 foreach my $pair (@maybe_deleted) {
416 my $id = $pair->{id};
418 my $fh = spawn ('-|', qw/notmuch search --output=files/,"id:$id")
419 or die "searching for $id";
421 push @missing, $pair;
423 push @deleted, $pair;
426 die "'notmuch search --output=files id:$id' exited with nonzero value\n";
431 @added = diff_index ($index, 'A');
433 unlink $index || die "unlink $index: $!";
435 return { added => [@added], deleted => [@deleted], missing => [@missing] };
443 my $fh = git_pipe ({ GIT_INDEX_FILE => $index },
444 qw/diff-index --cached/,
445 "--diff-filter=$filter", qw/--name-only HEAD/ );
447 my @lines = unpack_diff_lines ($fh);
449 die "'git diff-index --cached --diff-filter=$filter --name-only HEAD' ",
450 "exited with nonzero value\n";
458 my $ref1 = shift || 'HEAD';
459 my $ref2 = shift || 'FETCH_HEAD';
461 my $fh= git_pipe ( 'diff', "--diff-filter=$filter", '--name-only',
464 my @lines = unpack_diff_lines ($fh);
466 die "'git diff --diff-filter=$filter --name-only $ref1 $ref2' ",
467 "exited with nonzero value\n";
473 sub unpack_diff_lines {
479 my ($id,$tag) = m|tags/ ([^/]+) / ([^/]+) |x;
481 $id = decode_from_fs ($id);
482 $tag = decode_from_fs ($tag);
484 push @found, { id => $id, tag => $tag };
494 $str =~ s/($MUST_ENCODE)/"$ESCAPE_CHAR".sprintf ("%02x",ord ($1))/ge;
502 $str =~ s/$ESCAPED_RX/ chr (hex ($1))/eg;
516 pod2usage ( -verbose => 2 );
524 nmbug - manage notmuch tags about notmuch
528 nmbug subcommand [options]
530 B<nmbug help> for more help
534 =head2 Most common commands
538 =item B<commit> [message]
540 Commit appropriately prefixed tags from the notmuch database to
541 git. Any extra arguments are used (one per line) as a commit message.
543 =item B<push> [remote]
545 push local nmbug git state to remote repo
547 =item B<pull> [remote]
549 pull (merge) remote repo changes to notmuch. B<pull> is equivalent to
550 B<fetch> followed by B<merge>.
554 =head2 Other Useful Commands
560 Update the notmuch database from git. This is mainly useful to discard
561 your changes in notmuch relative to git.
563 =item B<fetch> [remote]
565 Fetch changes from the remote repo (see merge to bring those changes
568 =item B<help> [subcommand]
570 print help [for subcommand]
572 =item B<log> [parameters]
574 A simple wrapper for git log. After running C<nmbug fetch>, you can
575 inspect the changes with C<nmbug log HEAD..FETCH_HEAD>
579 Merge changes from FETCH_HEAD into HEAD, and load the result into
584 Show pending updates in notmuch or git repo. See below for more
585 information about the output format.
589 =head2 Less common commands
595 Dump a tar archive (using git archive) of the current nmbug tag set.
601 B<nmbug status> prints lines of the form
605 where n is a single character representing notmuch database status
611 Tag is present in notmuch database, but not committed to nmbug
612 (equivalently, tag has been deleted in nmbug repo, e.g. by a pull, but
613 not restored to notmuch database).
617 Tag is present in nmbug repo, but not restored to notmuch database
618 (equivalently, tag has been deleted in notmuch)
622 Message is unknown (missing from local notmuch database)
626 The second character (if present) represents a difference between remote
627 git and local. Typically C<nmbug fetch> needs to be run to update this.
634 Tag is present in remote, but not in local git.
639 Tag is present in local git, but not in remote git.
646 Each tag $tag for message with Message-Id $id is written to
649 tags/encode($id)/encode($tag)
651 The encoding preserves alphanumerics, and the characters "+-_@=.:,"
652 (not the quotes). All other octets are replaced with '%' followed by
653 a two digit hex number.
657 B<NMBGIT> specifies the location of the git repository used by nmbug.
658 If not specified $HOME/.nmbug is used.
660 B<NMBPREFIX> specifies the prefix in the notmuch database for tags of
661 interest to nmbug. If not specified 'notmuch::' is used.