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 my $subcommand = shift || usage ();
44 if (!exists $command{$subcommand}) {
48 &{$command{$subcommand}}(@ARGV);
51 my $envref = (ref $_[0] eq 'HASH') ? shift : {};
52 my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef;
53 my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : undef;
56 $envref->{GIT_DIR} ||= $NMBGIT;
57 spawn ($envref, defined $ioref ? $ioref : (), defined $dir ? $dir : (), @_);
61 my $fh = git_pipe (@_);
62 my $str = join ('', <$fh>);
64 die "'git @_' exited with nonzero value\n";
71 my $envref = (ref $_[0] eq 'HASH') ? shift : {};
72 my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef;
73 my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : '-|';
77 if (open my $child, $dir) {
81 while (my ($key, $value) = each %{$envref}) {
85 if (defined $ioref && $dir eq '-|') {
86 open my $fh, '|-', @_ or die "open |- @_: $!";
87 foreach my $line (@{$ioref}) {
88 print $fh $line, "\n";
93 open STDIN, '<', '/dev/null' or die "reopening stdin: $!"
105 my $fh = spawn ('-|', qw/notmuch search --output=tags/, "*")
106 or die 'error dumping tags';
110 push @tags, $_ if (m/^$prefix/);
113 die "'notmuch search --output=tags *' exited with nonzero value\n";
120 system ('git', "--git-dir=$NMBGIT", 'archive', 'HEAD');
126 return scalar (@{$status->{added}} ) + scalar (@{$status->{deleted}} ) == 0;
133 my $status = compute_status ();
135 if ( is_committed ($status) ) {
136 print "Nothing to commit\n";
140 my $index = read_tree ('HEAD');
142 update_index ($index, $status);
144 my $tree = git ( { GIT_INDEX_FILE => $index }, 'write-tree')
145 or die 'no output from write-tree';
147 my $parent = git ( 'rev-parse', 'HEAD' )
148 or die 'no output from rev-parse';
150 my $commit = git ([ @args ], 'commit-tree', $tree, '-p', $parent)
151 or die 'commit-tree';
153 git ('update-ref', 'HEAD', $commit);
155 unlink $index || die "unlink: $!";
161 my $index = $NMBGIT.'/nmbug.index';
162 git ({ GIT_INDEX_FILE => $index }, 'read-tree', '--empty');
163 git ({ GIT_INDEX_FILE => $index }, 'read-tree', $treeish);
171 my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index },
172 '|-', qw/git update-index --index-info/)
173 or die 'git update-index';
175 foreach my $pair (@{$status->{deleted}}) {
176 index_tags_for_msg ($git, $pair->{id}, 'D', $pair->{tag});
179 foreach my $pair (@{$status->{added}}) {
180 index_tags_for_msg ($git, $pair->{id}, 'A', $pair->{tag});
182 unless (close $git) {
183 die "'git update-index --index-info' exited with nonzero value\n";
190 my $remote = shift || 'origin';
192 git ('fetch', $remote);
198 system ('notmuch', @args) == 0 or die "notmuch @args failed: $?";
204 my $index = $NMBGIT.'/nmbug.index';
206 my $query = join ' ', map ("tag:$_", get_tags ($TAGPREFIX));
208 my $fh = spawn ('-|', qw/notmuch dump --/, $query)
209 or die "notmuch dump: $!";
211 git ('read-tree', '--empty');
212 my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index },
213 '|-', qw/git update-index --index-info/)
214 or die 'git update-index';
217 m/ ( [^ ]* ) \s+ \( ([^\)]* ) \) /x || die 'syntax error in dump';
218 my ($id,$rest) = ($1,$2);
220 #strip prefixes before writing
221 my @tags = grep { s/^$TAGPREFIX//; } split (' ', $rest);
222 index_tags_for_msg ($git,$id, 'A', @tags);
224 unless (close $git) {
225 die "'git update-index --index-info' exited with nonzero value\n";
228 die "'notmuch dump -- $query' exited with nonzero value\n";
233 sub index_tags_for_msg {
238 my $hash = $EMPTYBLOB;
239 my $blobmode = '100644';
243 $hash = '0000000000000000000000000000000000000000';
246 foreach my $tag (@_) {
247 my $tagpath = 'tags/' . encode_for_fs ($msgid) . '/' . encode_for_fs ($tag);
248 print $fh "$blobmode $hash\t$tagpath\n";
254 do_sync (action => 'checkout');
262 my $status = compute_status ();
263 my ($A_action, $D_action);
265 if ($args{action} eq 'checkout') {
273 foreach my $pair (@{$status->{added}}) {
275 notmuch ('tag', $A_action.$TAGPREFIX.$pair->{tag},
279 foreach my $pair (@{$status->{deleted}}) {
280 notmuch ('tag', $D_action.$TAGPREFIX.$pair->{tag},
287 sub insist_committed {
289 my $status=compute_status();
290 if ( !is_committed ($status) ) {
291 print "Uncommitted changes to $TAGPREFIX* tags in notmuch
293 For a summary of changes, run 'nmbug status'
294 To save your changes, run 'nmbug commit' before merging/pull
295 To discard your changes, run 'nmbug checkout'
304 my $remote = shift || 'origin';
306 git ( 'fetch', $remote);
315 my $tempwork = tempdir ('/tmp/nmbug-merge.XXXXXX', CLEANUP => 1);
317 git ( { GIT_WORK_TREE => $tempwork }, 'checkout', '-f', 'HEAD');
319 git ( { GIT_WORK_TREE => $tempwork }, 'merge', 'FETCH_HEAD');
326 # we don't want output trapping here, because we want the pager.
327 system ( 'git', "--git-dir=$NMBGIT", 'log', '--name-status', @_);
332 my $remote = shift || 'origin';
334 git ('push', $remote);
339 my $status = compute_status ();
342 foreach my $pair (@{$status->{added}}) {
343 $output{$pair->{id}} ||= {};
344 $output{$pair->{id}}{$pair->{tag}} = 'A'
347 foreach my $pair (@{$status->{deleted}}) {
348 $output{$pair->{id}} ||= {};
349 $output{$pair->{id}}{$pair->{tag}} = 'D'
352 foreach my $pair (@{$status->{missing}}) {
353 $output{$pair->{id}} ||= {};
354 $output{$pair->{id}}{$pair->{tag}} = 'U'
357 if (is_unmerged ()) {
358 foreach my $pair (diff_refs ('A')) {
359 $output{$pair->{id}} ||= {};
360 $output{$pair->{id}}{$pair->{tag}} ||= ' ';
361 $output{$pair->{id}}{$pair->{tag}} .= 'a';
364 foreach my $pair (diff_refs ('D')) {
365 $output{$pair->{id}} ||= {};
366 $output{$pair->{id}}{$pair->{tag}} ||= ' ';
367 $output{$pair->{id}}{$pair->{tag}} .= 'd';
371 foreach my $id (sort keys %output) {
372 foreach my $tag (sort keys %{$output{$id}}) {
373 printf "%s\t%s\t%s\n", $output{$id}{$tag}, $id, $tag;
381 return 0 if (! -f $NMBGIT.'/FETCH_HEAD');
383 my $fetch_head = git ('rev-parse', 'FETCH_HEAD');
384 my $base = git ( 'merge-base', 'HEAD', 'FETCH_HEAD');
386 return ($base ne $fetch_head);
397 my $index = index_tags ();
399 my @maybe_deleted = diff_index ($index, 'D');
401 foreach my $pair (@maybe_deleted) {
403 my $id = $pair->{id};
405 my $fh = spawn ('-|', qw/notmuch search --output=files/,"id:$id")
406 or die "searching for $id";
408 push @missing, $pair;
410 push @deleted, $pair;
413 die "'notmuch search --output=files id:$id' exited with nonzero value\n";
418 @added = diff_index ($index, 'A');
420 unlink $index || die "unlink $index: $!";
422 return { added => [@added], deleted => [@deleted], missing => [@missing] };
430 my $fh = git_pipe ({ GIT_INDEX_FILE => $index },
431 qw/diff-index --cached/,
432 "--diff-filter=$filter", qw/--name-only HEAD/ );
434 my @lines = unpack_diff_lines ($fh);
436 die "'git diff-index --cached --diff-filter=$filter --name-only HEAD' ",
437 "exited with nonzero value\n";
445 my $ref1 = shift || 'HEAD';
446 my $ref2 = shift || 'FETCH_HEAD';
448 my $fh= git_pipe ( 'diff', "--diff-filter=$filter", '--name-only',
451 my @lines = unpack_diff_lines ($fh);
453 die "'git diff --diff-filter=$filter --name-only $ref1 $ref2' ",
454 "exited with nonzero value\n";
460 sub unpack_diff_lines {
466 my ($id,$tag) = m|tags/ ([^/]+) / ([^/]+) |x;
468 $id = decode_from_fs ($id);
469 $tag = decode_from_fs ($tag);
471 push @found, { id => $id, tag => $tag };
481 $str =~ s/($MUST_ENCODE)/"$ESCAPE_CHAR".sprintf ("%02x",ord ($1))/ge;
489 $str =~ s/$ESCAPED_RX/ chr (hex ($1))/eg;
503 pod2usage ( -verbose => 2 );
511 nmbug - manage notmuch tags about notmuch
515 nmbug subcommand [options]
517 B<nmbug help> for more help
521 =head2 Most common commands
525 =item B<commit> [message]
527 Commit appropriately prefixed tags from the notmuch database to
528 git. Any extra arguments are used (one per line) as a commit message.
530 =item B<push> [remote]
532 push local nmbug git state to remote repo
534 =item B<pull> [remote]
536 pull (merge) remote repo changes to notmuch. B<pull> is equivalent to
537 B<fetch> followed by B<merge>.
541 =head2 Other Useful Commands
547 Update the notmuch database from git. This is mainly useful to discard
548 your changes in notmuch relative to git.
550 =item B<fetch> [remote]
552 Fetch changes from the remote repo (see merge to bring those changes
555 =item B<help> [subcommand]
557 print help [for subcommand]
559 =item B<log> [parameters]
561 A simple wrapper for git log. After running C<nmbug fetch>, you can
562 inspect the changes with C<nmbug log HEAD..FETCH_HEAD>
566 Merge changes from FETCH_HEAD into HEAD, and load the result into
571 Show pending updates in notmuch or git repo. See below for more
572 information about the output format.
576 =head2 Less common commands
582 Dump a tar archive (using git archive) of the current nmbug tag set.
588 B<nmbug status> prints lines of the form
592 where n is a single character representing notmuch database status
598 Tag is present in notmuch database, but not committed to nmbug
599 (equivalently, tag has been deleted in nmbug repo, e.g. by a pull, but
600 not restored to notmuch database).
604 Tag is present in nmbug repo, but not restored to notmuch database
605 (equivalently, tag has been deleted in notmuch)
609 Message is unknown (missing from local notmuch database)
613 The second character (if present) represents a difference between remote
614 git and local. Typically C<nmbug fetch> needs to be run to update this.
621 Tag is present in remote, but not in local git.
626 Tag is present in local git, but not in remote git.
633 Each tag $tag for message with Message-Id $id is written to
636 tags/encode($id)/encode($tag)
638 The encoding preserves alphanumerics, and the characters "+-_@=.:,"
639 (not the quotes). All other octets are replaced with '%' followed by
640 a two digit hex number.
644 B<NMBGIT> specifies the location of the git repository used by nmbug.
645 If not specified $HOME/.nmbug is used.
647 B<NMBPREFIX> specifies the prefix in the notmuch database for tags of
648 interest to nmbug. If not specified 'notmuch::' is used.