From: David Bremner Date: Sat, 9 Aug 2014 16:21:57 +0000 (-0300) Subject: Merge tag 'debian/0.18.1-2' X-Git-Tag: 0.19_rc1~108 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=f92342cb76fa3e1fa2f1c2e727f8ddf1a5c21b7d;hp=01c8bf89a456cae9221bb57a130758a7a6dc005b Merge tag 'debian/0.18.1-2' uploaded to Debian unstable --- 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": "\n\n...", + "footer": "", + ... + }, + ... + }, + 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 <) { 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 -# 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=''' +header_template = config['meta'].get('header', ''' @@ -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; + }}

{title}

-

-Generated: {date}
{blurb}

Views

-'''.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='\n\n', +''') + +footer_template = config['meta'].get('footer', ''' +
+

Generated: {datetime} + + +''') + +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 . +;; +;; Authors: Austin Clements +;; David Edmondson + +(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 "") 'widget-backward) - (define-key map (kbd "M-TAB") 'notmuch-show-previous-button) - (define-key map (kbd "") '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 "") 'widget-backward) + (define-key map (kbd "M-TAB") 'notmuch-show-previous-button) + (define-key map (kbd "") '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 (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, ¬much)) + NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) 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]=\" [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]="\" \ + [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]=\" [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=" $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 +#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