aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml23
-rw-r--r--Makefile.local19
-rw-r--r--NEWS24
-rw-r--r--bindings/ruby/defs.h3
-rw-r--r--bindings/ruby/init.c1
-rw-r--r--bindings/ruby/query.c19
-rwxr-xr-xconfigure29
-rwxr-xr-xdevel/news2wiki.pl3
-rwxr-xr-xdevel/nmbug/nmbug18
-rwxr-xr-xdevel/nmbug/nmbug-status67
-rw-r--r--doc/.gitignore1
-rw-r--r--doc/Makefile.local29
-rw-r--r--doc/doxygen.cfg7
-rw-r--r--emacs/Makefile.local3
-rw-r--r--emacs/notmuch-hello.el11
-rw-r--r--emacs/notmuch-jump.el173
-rw-r--r--emacs/notmuch-lib.el35
-rw-r--r--emacs/notmuch-mua.el3
-rw-r--r--emacs/notmuch-show.el185
-rw-r--r--emacs/notmuch-tree.el25
-rw-r--r--emacs/notmuch.el23
-rw-r--r--lib/database.cc63
-rw-r--r--lib/message-file.c8
-rw-r--r--lib/message.cc18
-rw-r--r--lib/notmuch-private.h14
-rw-r--r--lib/notmuch.h15
-rw-r--r--lib/sha1.c4
-rw-r--r--lib/thread.cc4
-rw-r--r--notmuch-config.c19
-rw-r--r--notmuch-dump.c2
-rw-r--r--notmuch-new.c15
-rw-r--r--performance-test/Makefile.local3
-rw-r--r--test/.gitignore10
-rw-r--r--test/Makefile.local35
-rwxr-xr-xtest/T010-help-test.sh2
-rwxr-xr-xtest/T020-compact.sh2
-rwxr-xr-xtest/T150-tagging.sh4
-rwxr-xr-xtest/T260-thread-order.sh86
-rwxr-xr-xtest/T380-atomicity.sh2
-rw-r--r--test/emacs.expected-output/notmuch-hello2
-rw-r--r--test/emacs.expected-output/notmuch-hello-long-names2
-rw-r--r--test/gen-threads.py33
-rw-r--r--test/test-databases/Makefile.local2
-rw-r--r--test/test-lib-common.sh4
-rw-r--r--test/test-lib.el11
-rw-r--r--test/test-lib.sh2
-rw-r--r--util/string-util.c8
-rw-r--r--util/string-util.h11
48 files changed, 829 insertions, 253 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..dbd6434e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: c
+before_install:
+ - sudo apt-get update -qq
+ - sudo apt-get install dtach libxapian-dev libgmime-2.6-dev libtalloc-dev python-sphinx
+
+ # Notmuch requires zlib 1.2.5.2, unfortunately travis runs on Ubuntu 12.04LTS which
+ # ships with zlib 1.2.3.3. We need to update to zlib 1.2.5.2 to be able to build.
+ # TODO: Watch https://github.com/travis-ci/travis-ci/issues/2046 and remove
+ # this hack once travis-ci switches to Ubuntu 14.04
+ - wget 'https://github.com/notmuch/travis-files/raw/master/zlib1g-dev_1.2.8.dfsg-1ubuntu1_amd64.deb'
+ - wget 'https://github.com/notmuch/travis-files/raw/master/zlib1g_1.2.8.dfsg-1ubuntu1_amd64.deb'
+ - sudo dpkg -i zlib1g-dev_1.2.8.dfsg-1ubuntu1_amd64.deb zlib1g_1.2.8.dfsg-1ubuntu1_amd64.deb
+ - sudo apt-get install -f
+
+script:
+ - ./configure
+ - make test
+
+notifications:
+ irc:
+ channels:
+ - "chat.freenode.net#notmuch"
+ on_success: change
diff --git a/Makefile.local b/Makefile.local
index 4f8f4640..81ee3477 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -10,10 +10,10 @@
# repository), we let git append identification of the actual commit.
PACKAGE=notmuch
-IS_GIT=$(shell if [ -d .git ] ; then echo yes ; else echo no; fi)
+IS_GIT=$(shell if [ -d ${srcdir}/.git ] ; then echo yes ; else echo no; fi)
ifeq ($(IS_GIT),yes)
-DATE:=$(shell git log --date=short -1 --pretty=format:%cd)
+DATE:=$(shell git --git-dir=${srcdir}/.git log --date=short -1 --pretty=format:%cd)
else
DATE:=$(shell date +%F)
endif
@@ -21,7 +21,7 @@ endif
VERSION:=$(shell cat ${srcdir}/version)
ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),)
ifeq ($(IS_GIT),yes)
-VERSION:=$(shell git describe --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/)
+VERSION:=$(shell git --git-dir=${srcdir}/.git describe --abbrev=7 --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/)
# Write the file 'version.stamp' in case its contents differ from $(VERSION)
FILE_VERSION:=$(shell test -f version.stamp && read vs < version.stamp || vs=; echo $$vs)
ifneq ($(FILE_VERSION),$(VERSION))
@@ -201,11 +201,11 @@ verify-source-tree-and-version: verify-no-dirty-code
verify-no-dirty-code: release-checks
ifeq ($(IS_GIT),yes)
@printf "Checking that source tree is clean..."
-ifneq ($(shell git ls-files -m),)
+ifneq ($(shell git --git-dir=${srcdir}/.git ls-files -m),)
@echo "No"
@echo "The following files have been modified since the most recent git commit:"
@echo ""
- @git ls-files -m
+ @git --git-dir=${srcdir}/.git ls-files -m
@echo ""
@echo "The release will be made from the committed state, but perhaps you meant"
@echo "to commit this code first? Please clean this up to make it more clear."
@@ -262,6 +262,10 @@ clean:
distclean: clean
rm -rf $(DISTCLEAN)
+.PHONY: dataclean
+dataclean: distclean
+ rm -rf $(DATACLEAN)
+
notmuch_client_srcs = \
command-line-arguments.c\
debugger.c \
@@ -331,9 +335,10 @@ install-desktop:
desktop-file-install --mode 0644 --dir "$(DESTDIR)$(desktop_dir)" notmuch.desktop
SRCS := $(SRCS) $(notmuch_client_srcs)
-CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) version.stamp
+CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
+CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
-DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config
+DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config
DEPS := $(SRCS:%.c=.deps/%.d)
DEPS := $(DEPS:%.cc=.deps/%.d)
diff --git a/NEWS b/NEWS
index eb8174cd..f7aaedf9 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,27 @@
+Notmuch 0.19 (UNRELEASED)
+=========================
+
+Library changes
+---------------
+
+Add return status to notmuch_database_close and
+notmuch_database_destroy
+
+nmbug-status
+------------
+
+`nmbug-status` can now optionally load header and footer templates
+from the config file. Use something like:
+
+ {
+ "meta": {
+ "header": "<!DOCTYPE html>\n<html lang="en">\n...",
+ "footer": "</body></html>",
+ ...
+ },
+ ...
+ },
+
Notmuch 0.18.1 (2014-06-25)
===========================
diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h
index 5b44585a..f4901a04 100644
--- a/bindings/ruby/defs.h
+++ b/bindings/ruby/defs.h
@@ -231,6 +231,9 @@ notmuch_rb_query_search_messages (VALUE self);
VALUE
notmuch_rb_query_count_messages (VALUE self);
+VALUE
+notmuch_rb_query_count_threads (VALUE self);
+
/* threads.c */
VALUE
notmuch_rb_threads_destroy (VALUE self);
diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c
index 663271d4..ab3f22df 100644
--- a/bindings/ruby/init.c
+++ b/bindings/ruby/init.c
@@ -271,6 +271,7 @@ Init_notmuch (void)
rb_define_method (notmuch_rb_cQuery, "search_threads", notmuch_rb_query_search_threads, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "search_messages", notmuch_rb_query_search_messages, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "count_messages", notmuch_rb_query_count_messages, 0); /* in query.c */
+ rb_define_method (notmuch_rb_cQuery, "count_threads", notmuch_rb_query_count_threads, 0); /* in query.c */
/*
* Document-class: Notmuch::Threads
diff --git a/bindings/ruby/query.c b/bindings/ruby/query.c
index 1658edee..a7dacba3 100644
--- a/bindings/ruby/query.c
+++ b/bindings/ruby/query.c
@@ -182,3 +182,22 @@ notmuch_rb_query_count_messages (VALUE self)
*/
return UINT2NUM(notmuch_query_count_messages(query));
}
+
+/*
+ * call-seq: QUERY.count_threads => Fixnum
+ *
+ * Return an estimate of the number of threads matching a search
+ */
+VALUE
+notmuch_rb_query_count_threads (VALUE self)
+{
+ notmuch_query_t *query;
+
+ Data_Get_Notmuch_Query (self, query);
+
+ /* Xapian exceptions are not handled properly.
+ * (function may return 0 after printing a message)
+ * Thus there is nothing we can do here...
+ */
+ return UINT2NUM(notmuch_query_count_threads(query));
+}
diff --git a/configure b/configure
index 99ab74dc..86ba2f7c 100755
--- a/configure
+++ b/configure
@@ -43,8 +43,8 @@ fi
# Set several defaults (optionally specified by the user in
# environment variables)
-CC=${CC:-gcc}
-CXX=${CXX:-g++}
+CC=${CC:-cc}
+CXX=${CXX:-c++}
CFLAGS=${CFLAGS:--O2}
CPPFLAGS=${CPPFLAGS:-}
CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)}
@@ -417,6 +417,15 @@ else
have_emacs=0
fi
+printf "Checking if doxygen is available... "
+if command -v doxygen > /dev/null 2>&1; then
+ printf "Yes.\n"
+ have_doxygen=1
+else
+ printf "No (so will not install api docs)\n"
+ have_doxygen=0
+fi
+
printf "Checking if sphinx is available and supports nroff output... "
if hash sphinx-build > /dev/null 2>&1 && python -m sphinx.writers.manpage > /dev/null 2>&1 ; then
printf "Yes.\n"
@@ -829,6 +838,9 @@ HAVE_SPHINX=${have_sphinx}
# Whether there's a rst2man binary available for building documentation
HAVE_RST2MAN=${have_rst2man}
+# Whether there's a doxygen binary available for building api documentation
+HAVE_DOXYGEN=${have_doxygen}
+
# The directory to which desktop files should be installed
desktop_dir = \$(prefix)/share/applications
@@ -944,3 +956,16 @@ CONFIGURE_CXXFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS) \\
CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS)
EOF
+
+# construct the sh.config
+cat > sh.config <<EOF
+# This sh.config was automatically generated by the ./configure
+# script of notmuch.
+
+# Whether the Xapian version in use supports compaction
+NOTMUCH_HAVE_XAPIAN_COMPACT=${have_xapian_compact}
+
+# Whether there's either sphinx or rst2man available for building
+# documentation
+NOTMUCH_HAVE_MAN=$((have_sphinx || have_rst2man))
+EOF
diff --git a/devel/news2wiki.pl b/devel/news2wiki.pl
index 8066ba7f..d966babf 100755
--- a/devel/news2wiki.pl
+++ b/devel/news2wiki.pl
@@ -32,8 +32,7 @@ while (<I>)
{
warn "$ARGV[0]:$.: tab(s) in line!\n" if /\t/;
warn "$ARGV[0]:$.: trailing whitespace\n" if /\s\s$/;
- # The date part in regex recognizes wip version dates like: (201x-xx-xx).
- if (/^Notmuch\s+(\S+)\s+\((\w\w\w\w-\w\w-\w\w)\)\s*$/) {
+ if (/^Notmuch\s+(\S+)\s+\((\d\d\d\d-\d\d-\d\d|UNRELEASED)\)\s*$/) {
# open O... autocloses previously opened file.
open O, '>', "$ARGV[1]/release-$1.mdwn" or die $!;
print "+ release-$1.mdwn...\n";
diff --git a/devel/nmbug/nmbug b/devel/nmbug/nmbug
index b18ded7b..998ee6b4 100755
--- a/devel/nmbug/nmbug
+++ b/devel/nmbug/nmbug
@@ -63,13 +63,20 @@ sub git_pipe {
spawn ($envref, defined $ioref ? $ioref : (), defined $dir ? $dir : (), @_);
}
-sub git {
+sub git_with_status {
my $fh = git_pipe (@_);
my $str = join ('', <$fh>);
- unless (close $fh) {
+ close $fh;
+ my $status = $?;
+ chomp($str);
+ return ($str, $status);
+}
+
+sub git {
+ my ($str, $status) = git_with_status (@_);
+ if ($status) {
die "'git @_' exited with nonzero value\n";
}
- chomp($str);
return $str;
}
@@ -423,7 +430,10 @@ sub do_status {
sub is_unmerged {
my $commit = shift || '@{upstream}';
- my $fetch_head = git ('rev-parse', $commit);
+ my ($fetch_head, $status) = git_with_status ('rev-parse', $commit);
+ if ($status) {
+ return 0;
+ }
my $base = git ( 'merge-base', 'HEAD', $commit);
return ($base ne $fetch_head);
diff --git a/devel/nmbug/nmbug-status b/devel/nmbug/nmbug-status
index 03621bd5..f0809f19 100755
--- a/devel/nmbug/nmbug-status
+++ b/devel/nmbug/nmbug-status
@@ -1,10 +1,30 @@
#!/usr/bin/python
#
# Copyright (c) 2011-2012 David Bremner <david@tethera.net>
-# License: Same as notmuch
+#
# dependencies
# - python 2.6 for json
# - argparse; either python 2.7, or install separately
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/ .
+
+"""Generate HTML for one or more notmuch searches.
+
+Messages matching each search are grouped by thread. Each message
+that contains both a subject and message-id will have the displayed
+subject link to the Gmane view of the message.
+"""
from __future__ import print_function
from __future__ import unicode_literals
@@ -242,7 +262,7 @@ class HtmlPage (Page):
def _slug(self, string):
return self._slug_regexp.sub('-', string)
-parser = argparse.ArgumentParser()
+parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--text', help='output plain text format',
action='store_true')
parser.add_argument('--config', help='load config from given file',
@@ -256,9 +276,7 @@ args = parser.parse_args()
config = read_config(path=args.config)
-_PAGES['text'] = Page()
-_PAGES['html'] = HtmlPage(
- header='''<!DOCTYPE html>
+header_template = config['meta'].get('header', '''<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
@@ -295,22 +313,43 @@ _PAGES['html'] = HtmlPage(
tbody:nth-child(4n+3) tr td {{
background-color: #bce;
}}
+ hr {{
+ border: 0;
+ height: 1px;
+ color: #ccc;
+ background-color: #ccc;
+ }}
</style>
</head>
<body>
<h2>{title}</h2>
-<p>
-Generated: {date}<br />
{blurb}
</p>
<h3>Views</h3>
-'''.format(date=datetime.datetime.utcnow().date(),
- title=config['meta']['title'],
- blurb=config['meta']['blurb'],
- encoding=_ENCODING,
- inter_message_padding='0.25em',
- border_radius='0.5em'),
- footer='</body>\n</html>\n',
+''')
+
+footer_template = config['meta'].get('footer', '''
+<hr>
+<p>Generated: {datetime}
+</body>
+</html>
+''')
+
+now = datetime.datetime.utcnow()
+context = {
+ 'date': now,
+ 'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
+ 'title': config['meta']['title'],
+ 'blurb': config['meta']['blurb'],
+ 'encoding': _ENCODING,
+ 'inter_message_padding': '0.25em',
+ 'border_radius': '0.5em',
+ }
+
+_PAGES['text'] = Page()
+_PAGES['html'] = HtmlPage(
+ header=header_template.format(**context),
+ footer=footer_template.format(**context),
)
if args.list_views:
diff --git a/doc/.gitignore b/doc/.gitignore
index a60fb31e..f0cbb9c2 100644
--- a/doc/.gitignore
+++ b/doc/.gitignore
@@ -1,2 +1,3 @@
+*.pyc
docdeps.mk
_build
diff --git a/doc/Makefile.local b/doc/Makefile.local
index bbd46100..08874139 100644
--- a/doc/Makefile.local
+++ b/doc/Makefile.local
@@ -12,10 +12,12 @@ mkdocdeps := python $(srcdir)/$(dir)/mkdocdeps.py
# Internal variables.
ALLSPHINXOPTS := -d $(DOCBUILDDIR)/doctrees $(SPHINXOPTS) $(srcdir)/$(dir)
+APIMAN := $(DOCBUILDDIR)/man/man3/notmuch.3
+DOXYFILE := $(srcdir)/$(dir)/doxygen.cfg
.PHONY: sphinx-html sphinx-texinfo sphinx-info
-.PHONY: install-man build-man
+.PHONY: install-man build-man apidocs install-apidocs
%.gz: %
rm -f $@ && gzip --stdout $^ > $@
@@ -56,6 +58,25 @@ else
endif
touch ${MAN_ROFF_FILES} $@
+install-man: install-apidocs
+
+ifeq ($(HAVE_DOXYGEN),1)
+MAN_GZIP_FILES += ${APIMAN}.gz
+apidocs: $(APIMAN)
+install-apidocs: apidocs
+ mkdir -p "$(DESTDIR)$(mandir)/man3"
+ install -m0644 $(DOCBUILDDIR)/man/man3/*.3.gz $(DESTDIR)/$(mandir)/man3
+
+$(APIMAN): $(dir)/config.dox $(srcdir)/$(dir)/doxygen.cfg $(srcdir)/lib/notmuch.h
+ mkdir -p $(DOCBUILDDIR)/man/man3
+ doxygen $(DOXYFILE)
+ rm $(DOCBUILDDIR)/man/man3/_*.3
+ perl -pi -e 's/^[.]RI "\\fI/.RI "\\fP/' $(APIMAN)
+else
+apidocs:
+install-apidocs:
+endif
+
# Do not try to build or install man pages if a man page converter is
# not available.
ifeq ($(HAVE_SPHINX)$(HAVE_RST2MAN),00)
@@ -74,8 +95,12 @@ install-man: ${MAN_GZIP_FILES}
cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz
endif
+$(dir)/config.dox: version.stamp
+ echo "PROJECT_NAME = \"Notmuch $(VERSION)\"" > $@
+ echo "INPUT=${srcdir}/lib/notmuch.h" >> $@
+
$(dir)/docdeps.mk: $(dir)/conf.py $(dir)/mkdocdeps.py
$(mkdocdeps) $(srcdir)/doc $(DOCBUILDDIR) $@
CLEAN := $(CLEAN) $(DOCBUILDDIR) $(dir)/docdeps.mk $(DOCBUILDDIR)/.roff.stamp
-CLEAN := $(CLEAN) $(MAN_GZIP_FILES) $(MAN_ROFF_FILES) $(dir)/conf.pyc
+CLEAN := $(CLEAN) $(MAN_GZIP_FILES) $(MAN_ROFF_FILES) $(dir)/conf.pyc $(dir)/config.dox
diff --git a/doc/doxygen.cfg b/doc/doxygen.cfg
index bfbfcab3..42b63394 100644
--- a/doc/doxygen.cfg
+++ b/doc/doxygen.cfg
@@ -4,11 +4,11 @@
# Project related configuration options
#---------------------------------------------------------------------------
DOXYFILE_ENCODING = UTF-8
-PROJECT_NAME = "Notmuch 0.18"
+@INCLUDE = "doc/config.dox"
PROJECT_NUMBER =
PROJECT_BRIEF =
PROJECT_LOGO =
-OUTPUT_DIRECTORY =
+OUTPUT_DIRECTORY = doc/_build
CREATE_SUBDIRS = NO
OUTPUT_LANGUAGE = English
BRIEF_MEMBER_DESC = YES
@@ -96,7 +96,6 @@ WARN_LOGFILE =
#---------------------------------------------------------------------------
# configuration options related to the input files
#---------------------------------------------------------------------------
-INPUT = lib/notmuch.h
INPUT_ENCODING = UTF-8
FILE_PATTERNS =
RECURSIVE = NO
@@ -228,8 +227,6 @@ MAN_LINKS = NO
#---------------------------------------------------------------------------
GENERATE_XML = NO
XML_OUTPUT = xml
-XML_SCHEMA =
-XML_DTD =
XML_PROGRAMLISTING = YES
#---------------------------------------------------------------------------
# configuration options related to the DOCBOOK output
diff --git a/emacs/Makefile.local b/emacs/Makefile.local
index c0d6b190..1109cfa6 100644
--- a/emacs/Makefile.local
+++ b/emacs/Makefile.local
@@ -18,7 +18,8 @@ emacs_sources := \
$(dir)/notmuch-tag.el \
$(dir)/coolj.el \
$(dir)/notmuch-print.el \
- $(dir)/notmuch-version.el
+ $(dir)/notmuch-version.el \
+ $(dir)/notmuch-jump.el \
$(dir)/notmuch-version.el: $(dir)/Makefile.local version.stamp
$(dir)/notmuch-version.el: $(srcdir)/$(dir)/notmuch-version.el.tmpl
diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el
index 3de52386..65d06276 100644
--- a/emacs/notmuch-hello.el
+++ b/emacs/notmuch-hello.el
@@ -85,6 +85,7 @@ searches so they still work in customize."
(group :format "%v" :inline t (const :format " Query: " :query) (string :format "%v")))
(checklist :inline t
:format "%v"
+ (group :format "%v" :inline t (const :format "Shortcut key: " :key) (key-sequence :format "%v"))
(group :format "%v" :inline t (const :format "Count-Query: " :count-query) (string :format "%v"))
(group :format "%v" :inline t (const :format "" :sort-order)
(choice :tag " Sort Order"
@@ -92,8 +93,13 @@ searches so they still work in customize."
(const :tag "Oldest-first" oldest-first)
(const :tag "Newest-first" newest-first))))))
-(defcustom notmuch-saved-searches '((:name "inbox" :query "tag:inbox")
- (:name "unread" :query "tag:unread"))
+(defcustom notmuch-saved-searches
+ `((:name "inbox" :query "tag:inbox" :key ,(kbd "i"))
+ (:name "unread" :query "tag:unread" :key ,(kbd "u"))
+ (:name "flagged" :query "tag:flagged" :key ,(kbd "f"))
+ (:name "sent" :query "tag:sent" :key ,(kbd "t"))
+ (:name "drafts" :query "tag:draft" :key ,(kbd "d"))
+ (:name "all mail" :query "*" :key ,(kbd "a")))
"A list of saved searches to display.
The saved search can be given in 3 forms. The preferred way is as
@@ -101,6 +107,7 @@ a plist. Supported properties are
:name Name of the search (required).
:query Search to run (required).
+ :key Optional shortcut key for `notmuch-jump-search'.
:count-query Optional extra query to generate the count
shown. If not present then the :query property
is used.
diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el
new file mode 100644
index 00000000..05bbce5e
--- /dev/null
+++ b/emacs/notmuch-jump.el
@@ -0,0 +1,173 @@
+;; notmuch-jump.el --- User-friendly shortcut keys
+;;
+;; Copyright © Austin Clements
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: Austin Clements <aclements@csail.mit.edu>
+;; David Edmondson <dme@dme.org>
+
+(eval-when-compile (require 'cl))
+
+(require 'notmuch-lib)
+(require 'notmuch-hello)
+
+;;;###autoload
+(defun notmuch-jump-search ()
+ "Jump to a saved search by shortcut key.
+
+This prompts for and performs a saved search using the shortcut
+keys configured in the :key property of `notmuch-saved-searches'.
+Typically these shortcuts are a single key long, so this is a
+fast way to jump to a saved search from anywhere in Notmuch."
+ (interactive)
+
+ ;; Build the action map
+ (let (action-map)
+ (dolist (saved-search notmuch-saved-searches)
+ (let* ((saved-search (notmuch-hello-saved-search-to-plist saved-search))
+ (key (plist-get saved-search :key)))
+ (when key
+ (let ((name (plist-get saved-search :name))
+ (query (plist-get saved-search :query))
+ (oldest-first
+ (case (plist-get saved-search :sort-order)
+ (newest-first nil)
+ (oldest-first t)
+ (otherwise (default-value notmuch-search-oldest-first)))))
+ (push (list key name
+ `(lambda () (notmuch-search ',query ',oldest-first)))
+ action-map)))))
+ (setq action-map (nreverse action-map))
+
+ (if action-map
+ (notmuch-jump action-map "Search: ")
+ (error "To use notmuch-jump, please customize shortcut keys in notmuch-saved-searches."))))
+
+(defvar notmuch-jump--action nil)
+
+(defun notmuch-jump (action-map prompt)
+ "Interactively prompt for one of the keys in ACTION-MAP.
+
+Displays a summary of all bindings in ACTION-MAP in the
+minibuffer, reads a key from the minibuffer, and performs the
+corresponding action. The prompt can be canceled with C-g or
+RET. PROMPT must be a string to use for the prompt. PROMPT
+should include a space at the end.
+
+ACTION-MAP must be a list of triples of the form
+ (KEY LABEL ACTION)
+where KEY is a key binding, LABEL is a string label to display in
+the buffer, and ACTION is a nullary function to call. LABEL may
+be null, in which case the action will still be bound, but will
+not appear in the pop-up buffer.
+"
+
+ (let* ((items (notmuch-jump--format-actions action-map))
+ ;; Format the table of bindings and the full prompt
+ (table
+ (with-temp-buffer
+ (notmuch-jump--insert-items (window-body-width) items)
+ (buffer-string)))
+ (full-prompt
+ (concat table "\n\n"
+ (propertize prompt 'face 'minibuffer-prompt)))
+ ;; By default, the minibuffer applies the minibuffer face to
+ ;; the entire prompt. However, we want to clearly
+ ;; distinguish bindings (which we put in the prompt face
+ ;; ourselves) from their labels, so disable the minibuffer's
+ ;; own re-face-ing.
+ (minibuffer-prompt-properties
+ (notmuch-plist-delete
+ (copy-sequence minibuffer-prompt-properties)
+ 'face))
+ ;; Build the keymap with our bindings
+ (minibuffer-map (notmuch-jump--make-keymap action-map))
+ ;; The bindings save the the action in notmuch-jump--action
+ (notmuch-jump--action nil))
+ ;; Read the action
+ (read-from-minibuffer full-prompt nil minibuffer-map)
+
+ ;; If we got an action, do it
+ (when notmuch-jump--action
+ (funcall notmuch-jump--action))))
+
+(defun notmuch-jump--format-actions (action-map)
+ "Format the actions in ACTION-MAP.
+
+Returns a list of strings, one for each item with a label in
+ACTION-MAP. These strings can be inserted into a tabular
+buffer."
+
+ ;; Compute the maximum key description width
+ (let ((key-width 1))
+ (dolist (entry action-map)
+ (setq key-width
+ (max key-width
+ (string-width (format-kbd-macro (first entry))))))
+ ;; Format each action
+ (mapcar (lambda (entry)
+ (let ((key (format-kbd-macro (first entry)))
+ (desc (second entry)))
+ (concat
+ (propertize key 'face 'minibuffer-prompt)
+ (make-string (- key-width (length key)) ? )
+ " " desc)))
+ action-map)))
+
+(defun notmuch-jump--insert-items (width items)
+ "Make a table of ITEMS up to WIDTH wide in the current buffer."
+ (let* ((nitems (length items))
+ (col-width (+ 3 (apply #'max (mapcar #'string-width items))))
+ (ncols (if (> (* col-width nitems) width)
+ (max 1 (/ width col-width))
+ ;; Items fit on one line. Space them out
+ (setq col-width (/ width nitems))
+ (length items))))
+ (while items
+ (dotimes (col ncols)
+ (when items
+ (let ((item (pop items)))
+ (insert item)
+ (when (and items (< col (- ncols 1)))
+ (insert (make-string (- col-width (string-width item)) ? ))))))
+ (when items
+ (insert "\n")))))
+
+(defvar notmuch-jump-minibuffer-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map minibuffer-local-map)
+ ;; Make this like a special-mode keymap, with no self-insert-command
+ (suppress-keymap map)
+ map)
+ "Base keymap for notmuch-jump's minibuffer keymap.")
+
+(defun notmuch-jump--make-keymap (action-map)
+ "Translate ACTION-MAP into a minibuffer keymap."
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map notmuch-jump-minibuffer-map)
+ (dolist (action action-map)
+ (define-key map (first action)
+ `(lambda () (interactive)
+ (setq notmuch-jump--action ',(third action))
+ (exit-minibuffer))))
+ map))
+
+(unless (fboundp 'window-body-width)
+ ;; Compatibility for Emacs pre-24
+ (defun window-body-width (&optional window)
+ (let ((edges (window-inside-edges window)))
+ (- (caddr edges) (car edges)))))
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
index 2941da3e..19269e3c 100644
--- a/emacs/notmuch-lib.el
+++ b/emacs/notmuch-lib.el
@@ -25,8 +25,8 @@
(require 'mm-decode)
(require 'cl)
-(defvar notmuch-command "notmuch"
- "Command to run the notmuch binary.")
+(autoload 'notmuch-jump-search "notmuch-jump"
+ "Jump to a saved search by shortcut key." t)
(defgroup notmuch nil
"Notmuch mail reader for Emacs."
@@ -66,6 +66,16 @@
"Graphical attributes for displaying text"
:group 'notmuch)
+(defcustom notmuch-command "notmuch"
+ "Name of the notmuch binary.
+
+This can be a relative or absolute path to the notmuch binary.
+If this is a relative path, it will be searched for in all of the
+directories given in `exec-path' (which is, by default, based on
+$PATH)."
+ :type 'string
+ :group 'notmuch-external)
+
(defcustom notmuch-search-oldest-first t
"Show the oldest mail first when searching.
@@ -77,7 +87,11 @@ search."
:group 'notmuch-search)
(defcustom notmuch-poll-script nil
- "An external script to incorporate new mail into the notmuch database.
+ "[Deprecated] Command to run to incorporate new mail into the notmuch database.
+
+This option has been deprecated in favor of \"notmuch new\"
+hooks (see man notmuch-hooks). To change the path to the notmuch
+binary, customize `notmuch-command'.
This variable controls the action invoked by
`notmuch-poll-and-refresh-this-buffer' (bound by default to 'G')
@@ -93,10 +107,7 @@ the user's needs:
1. Invoke a program to transfer mail to the local mail store
2. Invoke \"notmuch new\" to incorporate the new mail
-3. Invoke one or more \"notmuch tag\" commands to classify the mail
-
-Note that the recommended way of achieving the same is using
-\"notmuch new\" hooks."
+3. Invoke one or more \"notmuch tag\" commands to classify the mail"
:type '(choice (const :tag "notmuch new" nil)
(const :tag "Disabled" "")
(string :tag "Custom script"))
@@ -130,6 +141,7 @@ For example, if you wanted to remove an \"inbox\" tag and add an
(define-key map "m" 'notmuch-mua-new-mail)
(define-key map "=" 'notmuch-refresh-this-buffer)
(define-key map "G" 'notmuch-poll-and-refresh-this-buffer)
+ (define-key map "j" 'notmuch-jump-search)
map)
"Keymap shared by all notmuch modes.")
@@ -464,6 +476,15 @@ This replaces spaces, percents, and double quotes in STR with
(setq list (cdr list)))
(nreverse out)))
+(defun notmuch-plist-delete (plist property)
+ (let* ((xplist (cons nil plist))
+ (pred xplist))
+ (while (cdr pred)
+ (when (eq (cadr pred) property)
+ (setcdr pred (cdddr pred)))
+ (setq pred (cddr pred)))
+ (cdr xplist)))
+
(defun notmuch-split-content-type (content-type)
"Split content/type into 'content' and 'type'"
(split-string content-type "/"))
diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el
index 95e4a4d3..2c588860 100644
--- a/emacs/notmuch-mua.el
+++ b/emacs/notmuch-mua.el
@@ -346,7 +346,8 @@ the From: address first."
(message-forward-make-body cur)
;; `message-forward-make-body' shows the User-agent header. Hide
;; it again.
- (message-hide-headers)))
+ (message-hide-headers)
+ (set-buffer-modified-p nil)))
(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
"Compose a reply to the message identified by QUERY-STRING.
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
index df10d4ba..7549fbb2 100644
--- a/emacs/notmuch-show.el
+++ b/emacs/notmuch-show.el
@@ -46,6 +46,7 @@
(declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
(declare-function notmuch-tree "notmuch-tree"
(&optional query query-context target buffer-name open-target))
+(declare-function notmuch-tree-get-message-properties "notmuch-tree" nil)
(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
"Headers that should be shown in a message, in this order.
@@ -180,10 +181,21 @@ each attachment handler is logged in buffers with names beginning
)
"List of Mailing List Archives to use when stashing links.
-These URIs are concatenated with the current message's
-Message-Id in `notmuch-show-stash-mlarchive-link'."
+This list is used for generating a Mailing List Archive reference
+URI with the current message's Message-Id in
+`notmuch-show-stash-mlarchive-link'.
+
+If the cdr of the alist element is not a function, the cdr is
+expected to contain a URI that is concatenated with the current
+message's Message-Id to create a ML archive reference URI.
+
+If the cdr is a function, the function is called with the
+Message-Id as the argument, and the function is expected to
+return the ML archive reference URI."
:type '(alist :key-type (string :tag "Name")
- :value-type (string :tag "URL"))
+ :value-type (choice
+ (string :tag "URL")
+ (function :tag "Function returning the URL")))
:group 'notmuch-show)
(defcustom notmuch-show-stash-mlarchive-link-default "Gmane"
@@ -211,6 +223,10 @@ For example, if you wanted to remove an \"unread\" tag and add a
:type '(repeat string)
:group 'notmuch-show)
+(defcustom notmuch-show-mark-read-function #'notmuch-show-seen-current-message
+ "Function to control which messages are marked read."
+ :type 'function
+ :group 'notmuch-show)
(defmacro with-current-notmuch-show-message (&rest body)
"Evaluate body with current buffer set to the text of current message"
@@ -1145,6 +1161,8 @@ function is used."
(let ((inhibit-read-only t))
(notmuch-show-mode)
+ (add-hook 'post-command-hook #'notmuch-show-command-hook nil t)
+
;; Don't track undo information for this buffer
(set 'buffer-undo-list t)
@@ -1186,6 +1204,15 @@ This includes:
- the current message."
(list (notmuch-show-get-message-id) (notmuch-show-get-message-ids-for-open-messages)))
+(defun notmuch-show-get-query ()
+ "Return the current query in this show buffer"
+ (if notmuch-show-query-context
+ (concat notmuch-show-thread-id
+ " and ("
+ notmuch-show-query-context
+ ")")
+ notmuch-show-thread-id))
+
(defun notmuch-show-apply-state (state)
"Apply STATE to the current buffer.
@@ -1264,46 +1291,46 @@ reset based on the original query."
(fset 'notmuch-show-part-map notmuch-show-part-map)
(defvar notmuch-show-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map notmuch-common-keymap)
- (define-key map "Z" 'notmuch-tree-from-show-current-query)
- (define-key map (kbd "<C-tab>") 'widget-backward)
- (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
- (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
- (define-key map (kbd "TAB") 'notmuch-show-next-button)
- (define-key map "f" 'notmuch-show-forward-message)
- (define-key map "r" 'notmuch-show-reply-sender)
- (define-key map "R" 'notmuch-show-reply)
- (define-key map "|" 'notmuch-show-pipe-message)
- (define-key map "w" 'notmuch-show-save-attachments)
- (define-key map "V" 'notmuch-show-view-raw-message)
- (define-key map "c" 'notmuch-show-stash-map)
- (define-key map "h" 'notmuch-show-toggle-visibility-headers)
- (define-key map "*" 'notmuch-show-tag-all)
- (define-key map "-" 'notmuch-show-remove-tag)
- (define-key map "+" 'notmuch-show-add-tag)
- (define-key map "X" 'notmuch-show-archive-thread-then-exit)
- (define-key map "x" 'notmuch-show-archive-message-then-next-or-exit)
- (define-key map "A" 'notmuch-show-archive-thread-then-next)
- (define-key map "a" 'notmuch-show-archive-message-then-next-or-next-thread)
- (define-key map "N" 'notmuch-show-next-message)
- (define-key map "P" 'notmuch-show-previous-message)
- (define-key map "n" 'notmuch-show-next-open-message)
- (define-key map "p" 'notmuch-show-previous-open-message)
- (define-key map (kbd "M-n") 'notmuch-show-next-thread-show)
- (define-key map (kbd "M-p") 'notmuch-show-previous-thread-show)
- (define-key map (kbd "DEL") 'notmuch-show-rewind)
- (define-key map " " 'notmuch-show-advance-and-archive)
- (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
- (define-key map (kbd "RET") 'notmuch-show-toggle-message)
- (define-key map "#" 'notmuch-show-print-message)
- (define-key map "!" 'notmuch-show-toggle-elide-non-matching)
- (define-key map "$" 'notmuch-show-toggle-process-crypto)
- (define-key map "<" 'notmuch-show-toggle-thread-indentation)
- (define-key map "t" 'toggle-truncate-lines)
- (define-key map "." 'notmuch-show-part-map)
- map)
- "Keymap for \"notmuch show\" buffers.")
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map notmuch-common-keymap)
+ (define-key map "Z" 'notmuch-tree-from-show-current-query)
+ (define-key map (kbd "<C-tab>") 'widget-backward)
+ (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
+ (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
+ (define-key map (kbd "TAB") 'notmuch-show-next-button)
+ (define-key map "f" 'notmuch-show-forward-message)
+ (define-key map "r" 'notmuch-show-reply-sender)
+ (define-key map "R" 'notmuch-show-reply)
+ (define-key map "|" 'notmuch-show-pipe-message)
+ (define-key map "w" 'notmuch-show-save-attachments)
+ (define-key map "V" 'notmuch-show-view-raw-message)
+ (define-key map "c" 'notmuch-show-stash-map)
+ (define-key map "h" 'notmuch-show-toggle-visibility-headers)
+ (define-key map "*" 'notmuch-show-tag-all)
+ (define-key map "-" 'notmuch-show-remove-tag)
+ (define-key map "+" 'notmuch-show-add-tag)
+ (define-key map "X" 'notmuch-show-archive-thread-then-exit)
+ (define-key map "x" 'notmuch-show-archive-message-then-next-or-exit)
+ (define-key map "A" 'notmuch-show-archive-thread-then-next)
+ (define-key map "a" 'notmuch-show-archive-message-then-next-or-next-thread)
+ (define-key map "N" 'notmuch-show-next-message)
+ (define-key map "P" 'notmuch-show-previous-message)
+ (define-key map "n" 'notmuch-show-next-open-message)
+ (define-key map "p" 'notmuch-show-previous-open-message)
+ (define-key map (kbd "M-n") 'notmuch-show-next-thread-show)
+ (define-key map (kbd "M-p") 'notmuch-show-previous-thread-show)
+ (define-key map (kbd "DEL") 'notmuch-show-rewind)
+ (define-key map " " 'notmuch-show-advance-and-archive)
+ (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
+ (define-key map (kbd "RET") 'notmuch-show-toggle-message)
+ (define-key map "#" 'notmuch-show-print-message)
+ (define-key map "!" 'notmuch-show-toggle-elide-non-matching)
+ (define-key map "$" 'notmuch-show-toggle-process-crypto)
+ (define-key map "<" 'notmuch-show-toggle-thread-indentation)
+ (define-key map "t" 'toggle-truncate-lines)
+ (define-key map "." 'notmuch-show-part-map)
+ map)
+ "Keymap for \"notmuch show\" buffers.")
(fset 'notmuch-show-mode-map notmuch-show-mode-map)
(defun notmuch-show-mode ()
@@ -1448,8 +1475,18 @@ an error if there is no part containing point."
(notmuch-show-set-message-properties props)))
(defun notmuch-show-get-prop (prop &optional props)
+ "Get property PROP from current message in show or tree mode.
+
+It gets property PROP from PROPS or, if PROPS is nil, the current
+message in either tree or show. This means that several utility
+functions in notmuch-show can be used directly by notmuch-tree as
+they just need the correct message properties."
(let ((props (or props
- (notmuch-show-get-message-properties))))
+ (cond ((eq major-mode 'notmuch-show-mode)
+ (notmuch-show-get-message-properties))
+ ((eq major-mode 'notmuch-tree-mode)
+ (notmuch-tree-get-message-properties))
+ (t nil)))))
(plist-get props prop)))
(defun notmuch-show-get-message-id (&optional bare)
@@ -1533,6 +1570,23 @@ marked as unread, i.e. the tag changes in
(apply 'notmuch-show-tag-message
(notmuch-tag-change-list notmuch-show-mark-read-tags unread))))
+(defun notmuch-show-seen-current-message (start end)
+ "Mark the current message read if it is open.
+
+We only mark it read once: if it is changed back then that is a
+user decision and we should not override it."
+ (when (and (notmuch-show-message-visible-p)
+ (not (notmuch-show-get-prop :seen)))
+ (notmuch-show-mark-read)
+ (notmuch-show-set-prop :seen t)))
+
+(defun notmuch-show-command-hook ()
+ (when (eq major-mode 'notmuch-show-mode)
+ ;; We need to redisplay to get window-start and window-end correct.
+ (redisplay)
+ (save-excursion
+ (funcall notmuch-show-mark-read-function (window-start) (window-end)))))
+
;; Functions for getting attributes of several messages in the current
;; thread.
@@ -1668,9 +1722,7 @@ If a prefix argument is given and this is the last message in the
thread, navigate to the next thread in the parent search buffer."
(interactive "P")
(if (notmuch-show-goto-message-next)
- (progn
- (notmuch-show-mark-read)
- (notmuch-show-message-adjust))
+ (notmuch-show-message-adjust)
(if pop-at-end
(notmuch-show-next-thread)
(goto-char (point-max)))))
@@ -1681,7 +1733,6 @@ thread, navigate to the next thread in the parent search buffer."
(if (= (point) (notmuch-show-message-top))
(notmuch-show-goto-message-previous)
(notmuch-show-move-to-message-top))
- (notmuch-show-mark-read)
(notmuch-show-message-adjust))
(defun notmuch-show-next-open-message (&optional pop-at-end)
@@ -1696,9 +1747,7 @@ to show, nil otherwise."
(while (and (setq r (notmuch-show-goto-message-next))
(not (notmuch-show-message-visible-p))))
(if r
- (progn
- (notmuch-show-mark-read)
- (notmuch-show-message-adjust))
+ (notmuch-show-message-adjust)
(if pop-at-end
(notmuch-show-next-thread)
(goto-char (point-max))))
@@ -1711,9 +1760,7 @@ to show, nil otherwise."
(while (and (setq r (notmuch-show-goto-message-next))
(not (notmuch-show-get-prop :match))))
(if r
- (progn
- (notmuch-show-mark-read)
- (notmuch-show-message-adjust))
+ (notmuch-show-message-adjust)
(goto-char (point-max)))))
(defun notmuch-show-open-if-matched ()
@@ -1724,8 +1771,7 @@ to show, nil otherwise."
(defun notmuch-show-goto-first-wanted-message ()
"Move to the first open message and mark it read"
(goto-char (point-min))
- (if (notmuch-show-message-visible-p)
- (notmuch-show-mark-read)
+ (unless (notmuch-show-message-visible-p)
(notmuch-show-next-open-message))
(when (eobp)
;; There are no matched non-excluded messages so open all matched
@@ -1733,8 +1779,7 @@ to show, nil otherwise."
(notmuch-show-mapc 'notmuch-show-open-if-matched)
(force-window-update)
(goto-char (point-min))
- (if (notmuch-show-message-visible-p)
- (notmuch-show-mark-read)
+ (unless (notmuch-show-message-visible-p)
(notmuch-show-next-open-message))))
(defun notmuch-show-previous-open-message ()
@@ -1744,7 +1789,6 @@ to show, nil otherwise."
(notmuch-show-goto-message-previous)
(notmuch-show-move-to-message-top))
(not (notmuch-show-message-visible-p))))
- (notmuch-show-mark-read)
(notmuch-show-message-adjust))
(defun notmuch-show-view-raw-message ()
@@ -2055,16 +2099,19 @@ This presumes that the message is available at the selected Mailing List Archive
If optional argument MLA is non-nil, use the provided key instead of prompting
the user (see `notmuch-show-stash-mlarchive-link-alist')."
(interactive)
- (notmuch-common-do-stash
- (concat (cdr (assoc
- (or mla
- (let ((completion-ignore-case t))
- (completing-read
- "Mailing List Archive: "
- notmuch-show-stash-mlarchive-link-alist
- nil t nil nil notmuch-show-stash-mlarchive-link-default)))
- notmuch-show-stash-mlarchive-link-alist))
- (notmuch-show-get-message-id t))))
+ (let ((url (cdr (assoc
+ (or mla
+ (let ((completion-ignore-case t))
+ (completing-read
+ "Mailing List Archive: "
+ notmuch-show-stash-mlarchive-link-alist
+ nil t nil nil
+ notmuch-show-stash-mlarchive-link-default)))
+ notmuch-show-stash-mlarchive-link-alist))))
+ (notmuch-common-do-stash
+ (if (functionp url)
+ (funcall url (notmuch-show-get-message-id t))
+ (concat url (notmuch-show-get-message-id t))))))
(defun notmuch-show-stash-mlarchive-link-and-go (&optional mla)
"Copy an ML Archive URI for the current message to the kill-ring and visit it.
diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el
index 7d5f4750..e859cc24 100644
--- a/emacs/notmuch-tree.el
+++ b/emacs/notmuch-tree.el
@@ -290,22 +290,6 @@ Some useful entries are:
(beginning-of-line)
(get-text-property (point) :notmuch-message-properties)))
-;; XXX This should really be a lib function but we are trying to
-;; reduce impact on the code base.
-(defun notmuch-show-get-prop (prop &optional props)
- "This is a tree view overridden version of notmuch-show-get-prop
-
-It gets property PROP from PROPS or, if PROPS is nil, the current
-message in either tree or show. This means that several functions
-in notmuch-show now work unchanged in tree as they just need the
-correct message properties."
- (let ((props (or props
- (cond ((eq major-mode 'notmuch-show-mode)
- (notmuch-show-get-message-properties))
- ((eq major-mode 'notmuch-tree-mode)
- (notmuch-tree-get-message-properties))))))
- (plist-get props prop)))
-
(defun notmuch-tree-set-message-properties (props)
(save-excursion
(beginning-of-line)
@@ -897,6 +881,15 @@ the same as for the function notmuch-tree."
(set-process-filter proc 'notmuch-tree-process-filter)
(set-process-query-on-exit-flag proc nil))))
+(defun notmuch-tree-get-query ()
+ "Return the current query in this tree buffer"
+ (if notmuch-tree-query-context
+ (concat notmuch-tree-basic-query
+ " and ("
+ notmuch-tree-query-context
+ ")")
+ notmuch-tree-basic-query))
+
(defun notmuch-tree (&optional query query-context target buffer-name open-target)
"Display threads matching QUERY in Tree View.
diff --git a/emacs/notmuch.el b/emacs/notmuch.el
index 1adea9c2..b44a907a 100644
--- a/emacs/notmuch.el
+++ b/emacs/notmuch.el
@@ -580,7 +580,8 @@ This function advances the next thread when finished."
(when notmuch-archive-tags
(notmuch-search-tag
(notmuch-tag-change-list notmuch-archive-tags unarchive) beg end))
- (notmuch-search-next-thread))
+ (when (eq beg end)
+ (notmuch-search-next-thread)))
(defun notmuch-search-update-result (result &optional pos)
"Replace the result object of the thread at POS (or point) by
@@ -649,12 +650,12 @@ of the result."
Here is an example of how to color search results based on tags.
(the following text would be placed in your ~/.emacs file):
- (setq notmuch-search-line-faces '((\"deleted\" . (:foreground \"red\"
- :background \"blue\"))
- (\"unread\" . (:foreground \"green\"))))
+ (setq notmuch-search-line-faces '((\"unread\" . (:foreground \"green\"))
+ (\"deleted\" . (:foreground \"red\"
+ :background \"blue\"))))
-The attributes defined for matching tags are merged, with later
-attributes overriding earlier. A message having both \"deleted\"
+The attributes defined for matching tags are merged, with earlier
+attributes overriding later. A message having both \"deleted\"
and \"unread\" tags with the above settings would have a green
foreground and blue background."
:type '(alist :key-type (string) :value-type (custom-face-edit))
@@ -862,6 +863,10 @@ PROMPT is the string to prompt with."
(concat "tag:" (notmuch-escape-boolean-term tag)))
(process-lines notmuch-command "search" "--output=tags" "*")))))
(let ((keymap (copy-keymap minibuffer-local-map))
+ (current-query (case major-mode
+ (notmuch-search-mode (notmuch-search-get-query))
+ (notmuch-show-mode (notmuch-show-get-query))
+ (notmuch-tree-mode (notmuch-tree-get-query))))
(minibuffer-completion-table
(completion-table-dynamic
(lambda (string)
@@ -879,7 +884,11 @@ PROMPT is the string to prompt with."
(define-key keymap (kbd "TAB") 'minibuffer-complete)
(let ((history-delete-duplicates t))
(read-from-minibuffer prompt nil keymap nil
- 'notmuch-search-history nil nil)))))
+ 'notmuch-search-history current-query nil)))))
+
+(defun notmuch-search-get-query ()
+ "Return the current query in this search buffer"
+ notmuch-search-query-string)
;;;###autoload
(put 'notmuch-search 'notmuch-doc "Search for messages.")
diff --git a/lib/database.cc b/lib/database.cc
index 1efb14d4..c7602906 100644
--- a/lib/database.cc
+++ b/lib/database.cc
@@ -54,9 +54,12 @@ typedef struct {
*
* Mail document
* -------------
- * A mail document is associated with a particular email message file
- * on disk. It is indexed with the following prefixed terms which the
- * database uses to construct threads, etc.:
+ * A mail document is associated with a particular email message. It
+ * is stored in one or more files on disk (though only one has its
+ * content indexed) and is uniquely identified by its "id" field
+ * (which is generally the message ID). It is indexed with the
+ * following prefixed terms which the database uses to construct
+ * threads, etc.:
*
* Single terms of given prefix:
*
@@ -356,7 +359,7 @@ _message_id_compressed (void *ctx, const char *message_id)
{
char *sha1, *compressed;
- sha1 = notmuch_sha1_of_string (message_id);
+ sha1 = _notmuch_sha1_of_string (message_id);
compressed = talloc_asprintf (ctx, "notmuch-sha1-%s", sha1);
free (sha1);
@@ -774,14 +777,17 @@ notmuch_database_open (const char *path,
return status;
}
-void
+notmuch_status_t
notmuch_database_close (notmuch_database_t *notmuch)
{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
try {
if (notmuch->xapian_db != NULL &&
notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE)
(static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->flush ();
} catch (const Xapian::Error &error) {
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
if (! notmuch->exception_reported) {
fprintf (stderr, "Error: A Xapian exception occurred flushing database: %s\n",
error.get_msg().c_str());
@@ -795,7 +801,9 @@ notmuch_database_close (notmuch_database_t *notmuch)
try {
notmuch->xapian_db->close();
} catch (const Xapian::Error &error) {
- /* do nothing */
+ /* don't clobber previous error status */
+ if (status == NOTMUCH_STATUS_SUCCESS)
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
}
}
@@ -809,6 +817,8 @@ notmuch_database_close (notmuch_database_t *notmuch)
notmuch->value_range_processor = NULL;
delete notmuch->date_range_processor;
notmuch->date_range_processor = NULL;
+
+ return status;
}
#if HAVE_XAPIAN_COMPACT
@@ -972,8 +982,15 @@ notmuch_database_compact (const char *path,
}
DONE:
- if (notmuch)
- notmuch_database_destroy (notmuch);
+ if (notmuch) {
+ notmuch_status_t ret2;
+
+ ret2 = notmuch_database_destroy (notmuch);
+
+ /* don't clobber previous error status */
+ if (ret == NOTMUCH_STATUS_SUCCESS && ret2 != NOTMUCH_STATUS_SUCCESS)
+ ret = ret2;
+ }
talloc_free (local);
@@ -991,11 +1008,15 @@ notmuch_database_compact (unused (const char *path),
}
#endif
-void
+notmuch_status_t
notmuch_database_destroy (notmuch_database_t *notmuch)
{
- notmuch_database_close (notmuch);
+ notmuch_status_t status;
+
+ status = notmuch_database_close (notmuch);
talloc_free (notmuch);
+
+ return status;
}
const char *
@@ -1356,7 +1377,7 @@ _notmuch_database_get_directory_db_path (const char *path)
int term_len = strlen (_find_prefix ("directory")) + strlen (path);
if (term_len > NOTMUCH_TERM_MAX)
- return notmuch_sha1_of_string (path);
+ return _notmuch_sha1_of_string (path);
else
return path;
}
@@ -1758,12 +1779,12 @@ _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
_my_talloc_free_for_g_hash, NULL);
this_message_id = notmuch_message_get_message_id (message);
- refs = notmuch_message_file_get_header (message_file, "references");
+ refs = _notmuch_message_file_get_header (message_file, "references");
last_ref_message_id = parse_references (message,
this_message_id,
parents, refs);
- in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
+ in_reply_to = _notmuch_message_file_get_header (message_file, "in-reply-to");
in_reply_to_message_id = parse_references (message,
this_message_id,
parents, in_reply_to);
@@ -1961,7 +1982,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
if (ret)
return ret;
- message_file = notmuch_message_file_open (filename);
+ message_file = _notmuch_message_file_open (filename);
if (message_file == NULL)
return NOTMUCH_STATUS_FILE_ERROR;
@@ -1982,9 +2003,9 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
* let's make sure that what we're looking at looks like an
* actual email message.
*/
- from = notmuch_message_file_get_header (message_file, "from");
- subject = notmuch_message_file_get_header (message_file, "subject");
- to = notmuch_message_file_get_header (message_file, "to");
+ from = _notmuch_message_file_get_header (message_file, "from");
+ subject = _notmuch_message_file_get_header (message_file, "subject");
+ to = _notmuch_message_file_get_header (message_file, "to");
if ((from == NULL || *from == '\0') &&
(subject == NULL || *subject == '\0') &&
@@ -1997,7 +2018,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
/* Now that we're sure it's mail, the first order of business
* is to find a message ID (or else create one ourselves). */
- header = notmuch_message_file_get_header (message_file, "message-id");
+ header = _notmuch_message_file_get_header (message_file, "message-id");
if (header && *header != '\0') {
message_id = _parse_message_id (message_file, header, NULL);
@@ -2018,7 +2039,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
if (message_id == NULL ) {
/* No message-id at all, let's generate one by taking a
* hash over the file's contents. */
- char *sha1 = notmuch_sha1_of_file (filename);
+ char *sha1 = _notmuch_sha1_of_file (filename);
/* If that failed too, something is really wrong. Give up. */
if (sha1 == NULL) {
@@ -2058,7 +2079,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
if (ret)
goto DONE;
- date = notmuch_message_file_get_header (message_file, "date");
+ date = _notmuch_message_file_get_header (message_file, "date");
_notmuch_message_set_header_values (message, date, from, subject);
ret = _notmuch_message_index_file (message, message_file);
@@ -2087,7 +2108,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
}
if (message_file)
- notmuch_message_file_close (message_file);
+ _notmuch_message_file_close (message_file);
ret2 = notmuch_database_end_atomic (notmuch);
if ((ret == NOTMUCH_STATUS_SUCCESS ||
diff --git a/lib/message-file.c b/lib/message-file.c
index 483ba1e9..eda1b748 100644
--- a/lib/message-file.c
+++ b/lib/message-file.c
@@ -99,19 +99,19 @@ _notmuch_message_file_open_ctx (void *ctx, const char *filename)
FAIL:
fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
- notmuch_message_file_close (message);
+ _notmuch_message_file_close (message);
return NULL;
}
notmuch_message_file_t *
-notmuch_message_file_open (const char *filename)
+_notmuch_message_file_open (const char *filename)
{
return _notmuch_message_file_open_ctx (NULL, filename);
}
void
-notmuch_message_file_close (notmuch_message_file_t *message)
+_notmuch_message_file_close (notmuch_message_file_t *message)
{
talloc_free (message);
}
@@ -297,7 +297,7 @@ _notmuch_message_file_get_combined_header (notmuch_message_file_t *message,
}
const char *
-notmuch_message_file_get_header (notmuch_message_file_t *message,
+_notmuch_message_file_get_header (notmuch_message_file_t *message,
const char *header)
{
const char *value;
diff --git a/lib/message.cc b/lib/message.cc
index d0b7351e..3f934265 100644
--- a/lib/message.cc
+++ b/lib/message.cc
@@ -193,14 +193,16 @@ _notmuch_message_create (const void *talloc_owner,
* There is already a document with message ID 'message_id' in the
* database. The returned message can be used to query/modify the
* document.
+ *
* NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
*
* No document with 'message_id' exists in the database. The
* returned message contains a newly created document (not yet
* added to the database) and a document ID that is known not to
- * exist in the database. The caller can modify the message, and a
- * call to _notmuch_message_sync will add * the document to the
- * database.
+ * exist in the database. This message is "blank"; that is, it
+ * contains only a message ID and no other metadata. The caller
+ * can modify the message, and a call to _notmuch_message_sync
+ * will add the document to the database.
*
* If an error occurs, this function will return NULL and *status
* will be set as appropriate. (The status pointer argument must
@@ -439,7 +441,7 @@ notmuch_message_get_header (notmuch_message_t *message, const char *header)
if (message->message_file == NULL)
return NULL;
- return notmuch_message_file_get_header (message->message_file, header);
+ return _notmuch_message_file_get_header (message->message_file, header);
}
/* Return the message ID from the In-Reply-To header of 'message'.
@@ -898,13 +900,13 @@ notmuch_message_get_tags (notmuch_message_t *message)
}
const char *
-notmuch_message_get_author (notmuch_message_t *message)
+_notmuch_message_get_author (notmuch_message_t *message)
{
return message->author;
}
void
-notmuch_message_set_author (notmuch_message_t *message,
+_notmuch_message_set_author (notmuch_message_t *message,
const char *author)
{
if (message->author)
@@ -971,7 +973,7 @@ void
_notmuch_message_close (notmuch_message_t *message)
{
if (message->message_file) {
- notmuch_message_file_close (message->message_file);
+ _notmuch_message_file_close (message->message_file);
message->message_file = NULL;
}
}
@@ -1032,6 +1034,8 @@ _notmuch_message_gen_terms (notmuch_message_t *message,
/* Create a gap between this an the next terms so they don't
* appear to be a phrase. */
message->termpos = term_gen->get_termpos () + 100;
+
+ _notmuch_message_invalidate_metadata (message, prefix_name);
}
term_gen->set_termpos (message->termpos);
diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h
index 703ae7bb..17f30613 100644
--- a/lib/notmuch-private.h
+++ b/lib/notmuch-private.h
@@ -316,11 +316,11 @@ _notmuch_message_clear_data (notmuch_message_t *message);
/* Set the author member of 'message' - this is the representation used
* when displaying the message */
void
-notmuch_message_set_author (notmuch_message_t *message, const char *author);
+_notmuch_message_set_author (notmuch_message_t *message, const char *author);
/* Get the author member of 'message' */
const char *
-notmuch_message_get_author (notmuch_message_t *message);
+_notmuch_message_get_author (notmuch_message_t *message);
/* message-file.c */
@@ -337,7 +337,7 @@ typedef struct _notmuch_message_file notmuch_message_file_t;
* Returns NULL if any error occurs.
*/
notmuch_message_file_t *
-notmuch_message_file_open (const char *filename);
+_notmuch_message_file_open (const char *filename);
/* Like notmuch_message_file_open but with 'ctx' as the talloc owner. */
notmuch_message_file_t *
@@ -345,7 +345,7 @@ _notmuch_message_file_open_ctx (void *ctx, const char *filename);
/* Close a notmuch message previously opened with notmuch_message_open. */
void
-notmuch_message_file_close (notmuch_message_file_t *message);
+_notmuch_message_file_close (notmuch_message_file_t *message);
/* Parse the message.
*
@@ -386,7 +386,7 @@ _notmuch_message_file_get_mime_message (notmuch_message_file_t *message,
* contain a header line matching 'header'.
*/
const char *
-notmuch_message_file_get_header (notmuch_message_file_t *message,
+_notmuch_message_file_get_header (notmuch_message_file_t *message,
const char *header);
/* index.cc */
@@ -455,10 +455,10 @@ _notmuch_message_add_reply (notmuch_message_t *message,
/* sha1.c */
char *
-notmuch_sha1_of_string (const char *str);
+_notmuch_sha1_of_string (const char *str);
char *
-notmuch_sha1_of_file (const char *filename);
+_notmuch_sha1_of_file (const char *filename);
/* string-list.c */
diff --git a/lib/notmuch.h b/lib/notmuch.h
index 350bed8b..3c5ec988 100644
--- a/lib/notmuch.h
+++ b/lib/notmuch.h
@@ -287,8 +287,16 @@ notmuch_database_open (const char *path,
*
* notmuch_database_close can be called multiple times. Later calls
* have no effect.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully closed the database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred; the
+ * database has been closed but there are no guarantees the
+ * changes to the database, if any, have been flushed to disk.
*/
-void
+notmuch_status_t
notmuch_database_close (notmuch_database_t *database);
/**
@@ -317,8 +325,11 @@ notmuch_database_compact (const char* path,
/**
* Destroy the notmuch database, closing it if necessary and freeing
* all associated resources.
+ *
+ * Return value as in notmuch_database_close if the database was open;
+ * notmuch_database_destroy itself has no failure modes.
*/
-void
+notmuch_status_t
notmuch_database_destroy (notmuch_database_t *database);
/**
diff --git a/lib/sha1.c b/lib/sha1.c
index cc481086..94060d57 100644
--- a/lib/sha1.c
+++ b/lib/sha1.c
@@ -50,7 +50,7 @@ _hex_of_sha1_digest (const unsigned char digest[SHA1_DIGEST_SIZE])
* should free() when finished.
*/
char *
-notmuch_sha1_of_string (const char *str)
+_notmuch_sha1_of_string (const char *str)
{
sha1_ctx sha1;
unsigned char digest[SHA1_DIGEST_SIZE];
@@ -74,7 +74,7 @@ notmuch_sha1_of_string (const char *str)
* file not found, etc.), this function returns NULL.
*/
char *
-notmuch_sha1_of_file (const char *filename)
+_notmuch_sha1_of_file (const char *filename)
{
FILE *file;
#define BLOCK_SIZE 4096
diff --git a/lib/thread.cc b/lib/thread.cc
index 8f53e122..8922403e 100644
--- a/lib/thread.cc
+++ b/lib/thread.cc
@@ -284,7 +284,7 @@ _thread_add_message (notmuch_thread_t *thread,
}
clean_author = _thread_cleanup_author (thread, author, from);
_thread_add_author (thread, clean_author);
- notmuch_message_set_author (message, clean_author);
+ _notmuch_message_set_author (message, clean_author);
}
g_object_unref (G_OBJECT (list));
}
@@ -373,7 +373,7 @@ _thread_add_matched_message (notmuch_thread_t *thread,
NOTMUCH_MESSAGE_FLAG_MATCH, 1);
}
- _thread_add_matched_author (thread, notmuch_message_get_author (hashed_message));
+ _thread_add_matched_author (thread, _notmuch_message_get_author (hashed_message));
}
static void
diff --git a/notmuch-config.c b/notmuch-config.c
index 4886d366..db487dbe 100644
--- a/notmuch-config.c
+++ b/notmuch-config.c
@@ -217,9 +217,10 @@ get_username_from_passwd_file (void *ctx)
* These default configuration settings are determined as
* follows:
*
- * database_path: $HOME/mail
+ * database_path: $MAILDIR, otherwise $HOME/mail
*
- * user_name: From /etc/passwd
+ * user_name: $NAME variable if set, otherwise
+ * read from /etc/passwd
*
* user_primary_mail: $EMAIL variable if set, otherwise
* constructed from the username and
@@ -322,14 +323,22 @@ notmuch_config_open (void *ctx,
if (notmuch_config_get_database_path (config) == NULL) {
- char *path = talloc_asprintf (config, "%s/mail",
- getenv ("HOME"));
+ char *path = getenv ("MAILDIR");
+ if (path)
+ path = talloc_strdup (config, path);
+ else
+ path = talloc_asprintf (config, "%s/mail",
+ getenv ("HOME"));
notmuch_config_set_database_path (config, path);
talloc_free (path);
}
if (notmuch_config_get_user_name (config) == NULL) {
- char *name = get_name_from_passwd_file (config);
+ char *name = getenv ("NAME");
+ if (name)
+ name = talloc_strdup (config, name);
+ else
+ name = get_name_from_passwd_file (config);
notmuch_config_set_user_name (config, name);
talloc_free (name);
}
diff --git a/notmuch-dump.c b/notmuch-dump.c
index 887a2082..9c6ad7f4 100644
--- a/notmuch-dump.c
+++ b/notmuch-dump.c
@@ -212,7 +212,7 @@ notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[])
int ret;
if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
+ NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
return EXIT_FAILURE;
char *output_file_name = NULL;
diff --git a/notmuch-new.c b/notmuch-new.c
index d269c7cd..56910056 100644
--- a/notmuch-new.c
+++ b/notmuch-new.c
@@ -923,6 +923,7 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[])
notmuch_bool_t timer_is_active = FALSE;
notmuch_bool_t no_hooks = FALSE;
notmuch_bool_t quiet = FALSE, verbose = FALSE;
+ notmuch_status_t status;
add_files_state.verbosity = VERBOSITY_NORMAL;
add_files_state.debug = FALSE;
@@ -1019,9 +1020,16 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[])
}
gettimeofday (&add_files_state.tv_start, NULL);
- notmuch_database_upgrade (notmuch,
- add_files_state.verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL,
- &add_files_state);
+ status = notmuch_database_upgrade (
+ notmuch,
+ add_files_state.verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL,
+ &add_files_state);
+ if (status) {
+ printf ("Upgrade failed: %s\n",
+ notmuch_status_to_string (status));
+ notmuch_database_destroy (notmuch);
+ return EXIT_FAILURE;
+ }
if (add_files_state.verbosity >= VERBOSITY_NORMAL)
printf ("Your notmuch database has now been upgraded to database format version %u.\n",
notmuch_database_get_version (notmuch));
@@ -1091,7 +1099,6 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[])
}
for (f = add_files_state.directory_mtimes->head; f && !interrupted; f = f->next) {
- notmuch_status_t status;
notmuch_directory_t *directory;
status = notmuch_database_get_directory (notmuch, f->filename, &directory);
if (status == NOTMUCH_STATUS_SUCCESS && directory) {
diff --git a/performance-test/Makefile.local b/performance-test/Makefile.local
index d97e56d9..3469aa3d 100644
--- a/performance-test/Makefile.local
+++ b/performance-test/Makefile.local
@@ -40,4 +40,5 @@ download-corpus:
wget -O ${TXZFILE} ${DEFAULT_URL}
CLEAN := $(CLEAN) $(dir)/tmp.* $(dir)/log.*
-DISTCLEAN := $(dir)/corpus $(dir)/notmuch.cache.*
+DISTCLEAN := $(DISTCLEAN) $(dir)/corpus $(dir)/notmuch.cache.*
+DATACLEAN := $(DATACLEAN) $(TXZFILE)
diff --git a/test/.gitignore b/test/.gitignore
index 97e02487..b3b706d8 100644
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -1,9 +1,9 @@
-test-results
-corpus.mail
-smtp-dummy
-symbol-test
arg-test
+corpus.mail
hex-xcode
-random-corpus
parse-time
+random-corpus
+smtp-dummy
+symbol-test
+test-results
tmp.*
diff --git a/test/Makefile.local b/test/Makefile.local
index d622eafe..916dd0bd 100644
--- a/test/Makefile.local
+++ b/test/Makefile.local
@@ -35,30 +35,19 @@ $(dir)/symbol-test: $(dir)/symbol-test.o lib/$(LINKER_NAME)
$(dir)/parse-time: $(dir)/parse-time.o parse-time-string/parse-time-string.o
$(call quiet,CC) $^ -o $@
-$(dir)/have-compact: Makefile.config
-ifeq ($(HAVE_XAPIAN_COMPACT),1)
- ln -sf /bin/true $@
-else
- ln -sf /bin/false $@
-endif
-
-$(dir)/have-man: Makefile.config
-ifeq ($(HAVE_SPHINX)$(HAVE_RST2MAN),00)
- ln -sf /bin/false $@
-else
- ln -sf /bin/true $@
-endif
-
.PHONY: test check
-TEST_BINARIES=$(dir)/arg-test \
- $(dir)/have-compact \
- $(dir)/have-man \
- $(dir)/hex-xcode \
- $(dir)/random-corpus \
- $(dir)/parse-time \
- $(dir)/smtp-dummy \
- $(dir)/symbol-test
+test_main_srcs=$(dir)/arg-test.c \
+ $(dir)/hex-xcode.c \
+ $(dir)/random-corpus.c \
+ $(dir)/parse-time.c \
+ $(dir)/smtp-dummy.c \
+ $(dir)/symbol-test.cc \
+
+test_srcs=$(test_main_srcs) $(dir)/database-test.c
+
+TEST_BINARIES := $(test_main_srcs:.c=)
+TEST_BINARIES := $(TEST_BINARIES:.cc=)
test-binaries: $(TEST_BINARIES)
@@ -67,7 +56,7 @@ test: all test-binaries
check: test
-SRCS := $(SRCS) $(smtp_dummy_srcs)
+SRCS := $(SRCS) $(test_srcs)
CLEAN += $(TEST_BINARIES) $(addsuffix .o,$(TEST_BINARIES)) \
$(dir)/database-test.o \
$(dir)/corpus.mail $(dir)/test-results $(dir)/tmp.*
diff --git a/test/T010-help-test.sh b/test/T010-help-test.sh
index 77410bc5..caf8bdb0 100755
--- a/test/T010-help-test.sh
+++ b/test/T010-help-test.sh
@@ -7,7 +7,7 @@ test_expect_success 'notmuch --help' 'notmuch --help'
test_expect_success 'notmuch help' 'notmuch help'
test_expect_success 'notmuch --version' 'notmuch --version'
-if ${TEST_DIRECTORY}/have-man; then
+if [ $NOTMUCH_HAVE_MAN -eq 1 ]; then
test_expect_success 'notmuch --help tag' 'notmuch --help tag'
test_expect_success 'notmuch help tag' 'notmuch help tag'
else
diff --git a/test/T020-compact.sh b/test/T020-compact.sh
index 77bb9632..507f7698 100755
--- a/test/T020-compact.sh
+++ b/test/T020-compact.sh
@@ -10,7 +10,7 @@ notmuch tag +tag1 \*
notmuch tag +tag2 subject:Two
notmuch tag -tag1 +tag3 subject:Three
-if ! ${TEST_DIRECTORY}/have-compact; then
+if [ $NOTMUCH_HAVE_XAPIAN_COMPACT -eq 0 ]; then
test_begin_subtest "Compact unsupported: error message"
output=$(notmuch compact --quiet 2>&1)
test_expect_equal "$output" "notmuch was compiled against a xapian version lacking compaction support.
diff --git a/test/T150-tagging.sh b/test/T150-tagging.sh
index dc118f33..45471ac8 100755
--- a/test/T150-tagging.sh
+++ b/test/T150-tagging.sh
@@ -247,8 +247,8 @@ ${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \
notmuch dump --format=batch-tag | sed 's/^.* -- /+common_tag -- /' | \
sort > EXPECTED
-notmuch dump --format=batch-tag | sed 's/^.* -- / -- /' | \
- notmuch restore --format=batch-tag
+notmuch dump --format=batch-tag | sed 's/^.* -- / -- /' > INTERMEDIATE_STEP
+notmuch restore --format=batch-tag < INTERMEDIATE_STEP
notmuch tag --batch < EXPECTED
diff --git a/test/T260-thread-order.sh b/test/T260-thread-order.sh
index 6c3a4b3f..b435d79f 100755
--- a/test/T260-thread-order.sh
+++ b/test/T260-thread-order.sh
@@ -2,31 +2,75 @@
test_description="threading when messages received out of order"
. ./test-lib.sh
-test_begin_subtest "Adding initial child message"
-generate_message [body]=foo "[in-reply-to]=\<parent-id\>" [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
-output=$(NOTMUCH_NEW)
-test_expect_equal "$output" "Added 1 new message to the database."
+# Generate all single-root four message thread structures. We'll use
+# this for multiple tests below.
+THREADS=$(python ${TEST_DIRECTORY}/gen-threads.py 4)
+nthreads=$(wc -l <<< "$THREADS")
-test_begin_subtest "Searching returns the message"
-output=$(notmuch search foo | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; brokenthreadtest (inbox unread)"
+test_begin_subtest "Messages with one parent get linked in all delivery orders"
+# In the first variant, this delivers messages that reference only
+# their immediate parent. Hence, we should only expect threads to be
+# fully joined at the end.
+for ((n = 0; n < 4; n++)); do
+ # Deliver the n'th message of every thread
+ thread=0
+ while read -a parents; do
+ parent=${parents[$n]}
+ generate_message \
+ [id]=m$n@t$thread [in-reply-to]="\<m$parent@t$thread\>" \
+ [subject]=p$thread [from]=m$n
+ thread=$((thread + 1))
+ done <<< "$THREADS"
+ notmuch new > /dev/null
+done
+output=$(notmuch search --sort=newest-first '*' | notmuch_search_sanitize)
+expected=$(for ((i = 0; i < $nthreads; i++)); do
+ echo "thread:XXX 2001-01-05 [4/4] m3, m2, m1, m0; p$i (inbox unread)"
+ done)
+test_expect_equal "$output" "$expected"
-test_begin_subtest "Adding second child message"
-generate_message [body]=foo "[in-reply-to]=\<parent-id\>" [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
-output=$(NOTMUCH_NEW)
-test_expect_equal "$output" "Added 1 new message to the database."
+test_begin_subtest "Messages with all parents get linked in all delivery orders"
+test_subtest_known_broken
+# Here we do the same thing as the previous test, but each message
+# references all of its parents. Since every message references the
+# root of the thread, each thread should always be fully joined. This
+# is currently broken because of the bug detailed in
+# id:8738h7kv2q.fsf@qmul.ac.uk.
+rm ${MAIL_DIR}/*
+notmuch new > /dev/null
+output=""
+expected=""
+for ((n = 0; n < 4; n++)); do
+ # Deliver the n'th message of every thread
+ thread=0
+ while read -a parents; do
+ references=""
+ parent=${parents[$n]}
+ while [[ $parent != None ]]; do
+ references="<m$parent@t$thread> $references"
+ parent=${parents[$parent]}
+ done
-test_begin_subtest "Searching returns both messages in one thread"
-output=$(notmuch search foo | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2000-01-01 [2/2] Notmuch Test Suite; brokenthreadtest (inbox unread)"
+ generate_message \
+ [id]=m$n@t$thread [references]="'$references'" \
+ [subject]=p$thread [from]=m$n
+ thread=$((thread + 1))
+ done <<< "$THREADS"
+ notmuch new > /dev/null
-test_begin_subtest "Adding parent message"
-generate_message [body]=foo [id]=parent-id [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
-output=$(NOTMUCH_NEW)
-test_expect_equal "$output" "Added 1 new message to the database."
+ output="$output
+$(notmuch search --sort=newest-first '*' | notmuch_search_sanitize)"
-test_begin_subtest "Searching returns all three messages in one thread"
-output=$(notmuch search foo | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2000-01-01 [3/3] Notmuch Test Suite; brokenthreadtest (inbox unread)"
+ # Construct expected output
+ template="thread:XXX 2001-01-05 [$((n+1))/$((n+1))]"
+ for ((m = n; m > 0; m--)); do
+ template="$template m$m,"
+ done
+ expected="$expected
+$(for ((i = 0; i < $nthreads; i++)); do
+ echo "$template m0; p$i (inbox unread)"
+ done)"
+done
+test_expect_equal "$output" "$expected"
test_done
diff --git a/test/T380-atomicity.sh b/test/T380-atomicity.sh
index 1c786fa2..2daef906 100755
--- a/test/T380-atomicity.sh
+++ b/test/T380-atomicity.sh
@@ -64,7 +64,7 @@ if test_require_external_prereq gdb; then
# -tty /dev/null works around a conflict between the 'timeout' wrapper
# and gdb's attempt to control the TTY.
export MAIL_DIR
- gdb -tty /dev/null -batch -x $TEST_DIRECTORY/atomicity.gdb notmuch >/dev/null 2>/dev/null
+ gdb -tty /dev/null -batch -x $TEST_DIRECTORY/atomicity.gdb notmuch 1>gdb.out 2>&1
# Get the final, golden output
notmuch search '*' > expected
diff --git a/test/emacs.expected-output/notmuch-hello b/test/emacs.expected-output/notmuch-hello
index 2d698917..9ba4cfc1 100644
--- a/test/emacs.expected-output/notmuch-hello
+++ b/test/emacs.expected-output/notmuch-hello
@@ -2,7 +2,7 @@
Saved searches: [edit]
- 52 inbox 52 unread
+ 52 inbox 52 unread 52 all mail
Search: .
diff --git a/test/emacs.expected-output/notmuch-hello-long-names b/test/emacs.expected-output/notmuch-hello-long-names
index 486d0d9a..1c8d6eb6 100644
--- a/test/emacs.expected-output/notmuch-hello-long-names
+++ b/test/emacs.expected-output/notmuch-hello-long-names
@@ -2,7 +2,7 @@
Saved searches: [edit]
- 52 inbox 52 unread
+ 52 inbox 52 unread 52 all mail
Search: .
diff --git a/test/gen-threads.py b/test/gen-threads.py
new file mode 100644
index 00000000..9fbb8474
--- /dev/null
+++ b/test/gen-threads.py
@@ -0,0 +1,33 @@
+# Generate all possible single-root message thread structures of size
+# argv[1]. Each output line is a thread structure, where the n'th
+# field is either a number giving the parent of message n or "None"
+# for the root.
+
+import sys
+from itertools import chain, combinations
+
+def subsets(s):
+ return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
+
+nodes = set(range(int(sys.argv[1])))
+
+# Queue of (tree, free, to_expand) where tree is a {node: parent}
+# dictionary, free is a set of unattached nodes, and to_expand is
+# itself a queue of nodes in the tree that need to be expanded.
+# The queue starts with all single-node trees.
+queue = [({root: None}, nodes - {root}, (root,)) for root in nodes]
+
+# Process queue
+while queue:
+ tree, free, to_expand = queue.pop()
+
+ if len(to_expand) == 0:
+ # Only print full-sized trees
+ if len(free) == 0:
+ print(" ".join(map(str, [msg[1] for msg in sorted(tree.items())])))
+ else:
+ # Expand node to_expand[0] with each possible set of children
+ for children in subsets(free):
+ ntree = dict(tree, **{child: to_expand[0] for child in children})
+ nfree = free.difference(children)
+ queue.append((ntree, nfree, to_expand[1:] + tuple(children)))
diff --git a/test/test-databases/Makefile.local b/test/test-databases/Makefile.local
index 0572e784..ff333a1d 100644
--- a/test/test-databases/Makefile.local
+++ b/test/test-databases/Makefile.local
@@ -11,4 +11,4 @@ test_databases := $(dir)/database-v1.tar.xz
download-test-databases: ${test_databases}
-DISTCLEAN := $(DISTCLEAN) ${test_databases}
+DATACLEAN := $(DATACLEAN) ${test_databases}
diff --git a/test/test-lib-common.sh b/test/test-lib-common.sh
index 892991e2..4903038d 100644
--- a/test/test-lib-common.sh
+++ b/test/test-lib-common.sh
@@ -38,6 +38,10 @@ find_notmuch_path ()
# test/ subdirectory and are run in 'trash directory' subdirectory.
TEST_DIRECTORY=$(pwd)
notmuch_path=`find_notmuch_path "$TEST_DIRECTORY"`
+
+# configure output
+. $notmuch_path/sh.config
+
if test -n "$valgrind"
then
make_symlink () {
diff --git a/test/test-lib.el b/test/test-lib.el
index 437f83f4..36afe630 100644
--- a/test/test-lib.el
+++ b/test/test-lib.el
@@ -52,11 +52,13 @@
(defun test-output (&optional filename)
"Save current buffer to file FILENAME. Default FILENAME is OUTPUT."
+ (notmuch-post-command)
(write-region (point-min) (point-max) (or filename "OUTPUT")))
(defun test-visible-output (&optional filename)
"Save visible text in current buffer to file FILENAME. Default
FILENAME is OUTPUT."
+ (notmuch-post-command)
(let ((text (visible-buffer-string)))
(with-temp-file (or filename "OUTPUT") (insert text))))
@@ -166,6 +168,15 @@ nothing."
(t
(notmuch-test-report-unexpected output expected)))))
+(defun notmuch-post-command ()
+ (run-hooks 'post-command-hook))
+
+(defmacro notmuch-test-progn (&rest body)
+ (cons 'progn
+ (mapcar
+ (lambda (x) `(prog1 ,x (notmuch-post-command)))
+ body)))
+
;; For historical reasons, we hide deleted tags by default in the test
;; suite
(setq notmuch-tag-deleted-formats
diff --git a/test/test-lib.sh b/test/test-lib.sh
index 17deaaba..b9b8fe8c 100644
--- a/test/test-lib.sh
+++ b/test/test-lib.sh
@@ -1138,7 +1138,7 @@ test_emacs () {
rm -f OUTPUT
touch OUTPUT
- ${TEST_EMACSCLIENT} --socket-name="$EMACS_SERVER" --eval "(progn $@)"
+ ${TEST_EMACSCLIENT} --socket-name="$EMACS_SERVER" --eval "(notmuch-test-progn $@)"
}
test_python() {
diff --git a/util/string-util.c b/util/string-util.c
index 3e7066cd..a90501ee 100644
--- a/util/string-util.c
+++ b/util/string-util.c
@@ -37,6 +37,14 @@ strtok_len (char *s, const char *delim, size_t *len)
return *len ? s : NULL;
}
+const char *
+strtok_len_c (const char *s, const char *delim, size_t *len)
+{
+ /* strtok_len is already const-safe, but we can't express both
+ * versions in the C type system. */
+ return strtok_len ((char*)s, delim, len);
+}
+
char *
sanitize_string (const void *ctx, const char *str)
{
diff --git a/util/string-util.h b/util/string-util.h
index 8a3ad19e..e409cb3d 100644
--- a/util/string-util.h
+++ b/util/string-util.h
@@ -3,6 +3,10 @@
#include <string.h>
+#ifdef __cplusplus
+extern "C" {
+#endif
+
/* like strtok(3), but without state, and doesn't modify s. Return
* value is indicated by pointer and length, not null terminator.
*
@@ -19,6 +23,9 @@
char *strtok_len (char *s, const char *delim, size_t *len);
+/* Const version of strtok_len. */
+const char *strtok_len_c (const char *s, const char *delim, size_t *len);
+
/* Return a talloced string with str sanitized.
*
* Whitespace characters (tabs and newlines) are replaced with spaces,
@@ -57,4 +64,8 @@ int
parse_boolean_term (void *ctx, const char *str,
char **prefix_out, char **term_out);
+#ifdef __cplusplus
+}
+#endif
+
#endif